Skip to content

hardening(desktop): close remaining Electron P1 gaps#246

Merged
Whiteks1 merged 1 commit intomainfrom
codex/desktop-electron-p1-hardening
Apr 2, 2026
Merged

hardening(desktop): close remaining Electron P1 gaps#246
Whiteks1 merged 1 commit intomainfrom
codex/desktop-electron-p1-hardening

Conversation

@Whiteks1
Copy link
Copy Markdown
Owner

@Whiteks1 Whiteks1 commented Mar 31, 2026

Summary

This PR hardens QuantLab Desktop in three areas:

  • enables sandboxing for the Electron renderer
  • makes the embedded research_ui startup path more resilient
  • bounds client-side caching and stops the refresh loop after repeated API failures

Changes

  • Turn on BrowserWindow sandboxing in desktop/main.js.
  • Improve Python interpreter resolution for research_ui startup by validating absolute paths more strictly and retrying common spawn/exit failure modes with the next candidate.
  • Keep the renderer refresh loop paused after repeated API failures until the user retries the workspace runtime.
  • Replace the unbounded run-detail cache with LruCache, and keep its set() behavior compatible with Map chaining.

Validation

  • node --check desktop/main.js
  • node --check desktop/renderer/app.js
  • node --check desktop/renderer/modules/utils.js
  • git diff --check
  • npm run smoke
  • .\.venv\Scripts\python.exe -m pytest test/test_research_ui_server.py

Notes

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Mar 31, 2026

Reviewer's Guide

Hardens the QuantLab Desktop Electron shell, adds a more resilient multi-candidate Python startup path for the embedded research_ui server, and makes the renderer’s refresh loop and caching safer for long-running sessions.

Sequence diagram for research_ui multi-candidate Python startup

sequenceDiagram
  participant MainProcess as MainProcess
  participant ResearchUiStarter as startResearchUiServer
  participant PythonResolver as resolvePythonCandidates
  participant Launcher as launchResearchUiProcess
  participant Child as PythonChildProcess
  participant Binder as bindResearchUiProcess
  participant Server as research_ui_server

  MainProcess->>ResearchUiStarter: startResearchUiServer(forceRestart)
  alt forceRestart
    ResearchUiStarter->>MainProcess: stopResearchUiServer(force)
  end
  ResearchUiStarter->>ResearchUiStarter: detectResearchUiServerUrl
  alt existing external server
    ResearchUiStarter->>MainProcess: markResearchUiReady(source external)
    ResearchUiStarter-->>MainProcess: return
  else no existing server
    ResearchUiStarter->>MainProcess: updateWorkspaceState(status starting)
    ResearchUiStarter->>MainProcess: scheduleResearchStartupTimeout
    ResearchUiStarter->>PythonResolver: resolvePythonCandidates
    PythonResolver-->>ResearchUiStarter: candidates[]
    alt no candidates
      ResearchUiStarter->>MainProcess: clearResearchStartupTimer
      ResearchUiStarter->>MainProcess: updateWorkspaceState(status error, message no interpreter)
      ResearchUiStarter-->>MainProcess: return
    else at least one candidate
      ResearchUiStarter->>Launcher: launchResearchUiProcess(candidates, index 0)
      Launcher->>Launcher: appendLog(startup with candidates[0])
      Launcher->>Child: spawn(pythonCommand, SERVER_SCRIPT)
      Launcher->>Binder: bindResearchUiProcess(Child, pythonCommand, candidates, index)
      Binder->>MainProcess: researchServerProcess = Child
      Binder->>MainProcess: researchServerOwned = true
      Binder->>Child: monitorResearchUiStartup
      Child-->>Binder: stdout URL line
      Binder->>Binder: extractServerUrl(line)
      alt URL discovered
        Binder->>MainProcess: isResearchUiReachable(url)
        MainProcess-->>Binder: reachable
        alt reachable and Child is current process
          Binder->>MainProcess: markResearchUiReady(url, source managed)
        end
      end
      Child-->>Binder: stderr spawn error
      Binder->>Binder: handle error
      alt error.code in [EACCES, EPERM, ENOENT] and more candidates
        Binder->>Launcher: appendLog(spawn error, retry next)
        Binder->>Launcher: launchResearchUiProcess(candidates, index+1)
      else fatal error or last candidate
        Binder->>MainProcess: clearResearchStartupTimer
        Binder->>MainProcess: updateWorkspaceState(status error, message from error)
      end
      Child-->>Binder: exit(code, signal)
      alt Child is current researchServerProcess
        Binder->>MainProcess: researchServerProcess = null
        Binder->>MainProcess: researchServerOwned = false
        Binder->>MainProcess: clearResearchStartupTimer
        Binder->>MainProcess: updateWorkspaceState(status stopped)
      end
    end
  end
Loading

Sequence diagram for renderer refresh loop with pause and retry

sequenceDiagram
  actor User
  participant Renderer as RendererApp
  participant Refresh as ensureRefreshLoop
  participant Snapshot as refreshSnapshot

  Renderer->>Refresh: ensureRefreshLoop()
  alt workspace has serverUrl
    Refresh->>Snapshot: refreshSnapshot()
    loop every refreshIntervalMs
      Refresh->>Snapshot: refreshSnapshot()
      alt success
        Snapshot->>Renderer: update snapshot
        Snapshot->>Renderer: snapshotStatus = {status ok, consecutiveErrors 0, refreshPaused false}
      else error
        Snapshot->>Renderer: consecutiveErrors++
        alt consecutiveErrors < maxConsecutiveRefreshErrors
          Snapshot->>Renderer: snapshotStatus = {status error, refreshPaused false}
        else consecutiveErrors >= maxConsecutiveRefreshErrors
          Snapshot->>Renderer: snapshotStatus = {status error, refreshPaused true}
          Snapshot->>Renderer: stopRefreshLoop()
        end
        Renderer->>Renderer: renderWorkspaceState(runtime alert warns, may show paused message)
      end
    end
  else no serverUrl
    Refresh->>Renderer: stopRefreshLoop()
  end

  User->>Renderer: clicks Retry API (runtime alert)
  Renderer->>Renderer: retryWorkspaceRuntime()
  alt workspace status error
    Renderer->>Renderer: restartWorkspaceServer()
    Renderer->>Renderer: update workspace state
  end
  Renderer->>Renderer: snapshotStatus.consecutiveErrors = 0
  Renderer->>Renderer: snapshotStatus.refreshPaused = false
  Renderer->>Snapshot: refreshSnapshot()
  alt refreshSnapshot succeeds
    Snapshot->>Renderer: snapshotStatus.status = ok
    Renderer->>Refresh: ensureRefreshLoop() if no error and no timer
  else still failing
    Snapshot->>Renderer: snapshotStatus.status = error
  end
  Renderer->>Renderer: renderWorkspaceState(updated runtime alert)
Loading

Class diagram for LruCache and its use in renderer state

classDiagram
  class LruCache {
    -Number maxSize
    -Map cache
    +LruCache(maxSize)
    +clear()
    +has(key)
    +get(key)
    +set(key, value)
  }

  class AppState {
    +Object workspace
    +Object snapshot
    +LruCache detailCache
    +Object experimentsWorkspace
    +Map experimentConfigPreviewCache
    +Boolean isSubmittingLaunch
    +String launchFeedback
    +Number refreshTimer
    +Boolean isStepbitSubmitting
    +SnapshotStatus snapshotStatus
    +Boolean isRetryingWorkspace
  }

  class SnapshotStatus {
    +String status
    +String error
    +String lastSuccessAt
    +Number consecutiveErrors
    +Boolean refreshPaused
  }

  AppState --> LruCache : uses
  AppState *-- SnapshotStatus : embeds
Loading

File-Level Changes

Change Details Files
Make research_ui startup more resilient by trying multiple Python interpreter candidates and guarding process lifecycle callbacks against stale child processes.
  • Replace single resolvePythonCommand helper with resolvePythonCandidates that builds a de-duplicated list of candidate Python commands (local .venv, $PYTHON, platform default) and filters unusable absolute paths via fs.accessSync.
  • Introduce launchResearchUiProcess and bindResearchUiProcess helpers to encapsulate child-process spawn, stdio wiring, and lifecycle event handlers while passing through the active candidate and index.
  • Update stdout/ stderr and exit/error handlers to check researchServerProcess identity before acting, avoiding updates from stale processes after restarts.
  • On spawn error, detect permission/not-found errors (EACCES, EPERM, ENOENT) and automatically retry the next Python candidate, logging the failure and only surfacing an error to the workspace state when all candidates are exhausted.
  • In startResearchUiServer, use the new candidate resolution flow, short-circuit with a clear error when no usable interpreters are found, and keep the existing path that reuses a pre-running external research_ui instance when available.
desktop/main.js
Enable Electron sandboxing in the main BrowserWindow for stronger renderer isolation.
  • Toggle the BrowserWindow webPreferences.sandbox flag from false to true while keeping contextIsolation enabled and nodeIntegration disabled, tightening the renderer process security model.
desktop/main.js
Harden the renderer’s workspace refresh loop by bounding run-detail memory usage, pausing on repeated API failures, and cleaning up timers and subscriptions on window unload.
  • Introduce a new LruCache utility that wraps a Map with configurable max size, promoting keys on get and evicting the oldest entry on overflow, and use it for the detailCache with a configurable maxDetailCacheEntries limit.
  • Extend CONFIG with maxDetailCacheEntries and maxConsecutiveRefreshErrors to configure cache size and error tolerance for the refresh loop.
  • Track an unsubscribeWorkspaceState handle, assign it when registering window.quantlabDesktop.onWorkspaceState, and invoke it during beforeunload along with stopping the refresh loop and clearing the workspace persist timer to avoid dangling listeners/timers.
  • Refactor refresh timer management by adding stopRefreshLoop, using it from beforeunload and refreshSnapshot when serverUrl is missing, and guarding ensureRefreshLoop against a missing serverUrl.
  • Enhance snapshotStatus to track consecutiveErrors and refreshPaused, incrementing/error-handling in refreshSnapshot so that, after maxConsecutiveRefreshErrors failures, the refresh loop is stopped and the UI reflects the paused state.
  • Adjust runtime alert messaging to append a hint when automatic refresh has been paused and wire retryWorkspaceRuntime to reset consecutiveErrors/refreshPaused, call refreshSnapshot, and re-establish the refresh loop when conditions are healthy.
desktop/renderer/app.js
desktop/renderer/modules/utils.js

Assessment against linked issues

Issue Objective Addressed Explanation
#220 Add Electron single-instance protection so QuantLab Desktop only runs one shell instance at a time, preventing duplicate local servers. The changes in desktop/main.js focus on improving research_ui startup (Python interpreter fallback, child process handling) and enabling sandboxing on the BrowserWindow, but there is no addition of Electron single-instance logic (e.g., app.requestSingleInstanceLock or second-instance handling).
#220 Harden research_ui startup and runtime by avoiding duplicate research_ui/server.py processes when a local server is already alive, and by surfacing boot/API failures clearly in the renderer with a retry affordance.
#220 Add a desktop-local smoke test or script under desktop/ that validates QuantLab Desktop startup assumptions without requiring Stepbit repo changes. The diff only shows changes to desktop/main.js, desktop/renderer/app.js, and desktop/renderer/modules/utils.js. No new smoke test file or startup validation script under desktop/ is introduced, and no test runner wiring is added, so the explicit smoke-test requirement is not implemented in the shown changes.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 security issues, and 1 other issue

Security issues:

  • Detected calls to child_process from a function argument candidateIndex. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)
  • Detected calls to child_process from a function argument candidates. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)
Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="desktop/renderer/modules/utils.js" line_range="221-218" />
<code_context>
+    return value;
+  }
+
+  set(key, value) {
+    if (this.cache.has(key)) {
+      this.cache.delete(key);
+    }
+    this.cache.set(key, value);
+    while (this.cache.size > this.maxSize) {
+      const oldestKey = this.cache.keys().next().value;
+      this.cache.delete(oldestKey);
+    }
+    return value;
+  }
+}
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Align `LruCache.set` return value with Map semantics to avoid surprises for callers.

`LruCache.set` currently returns `value`, but `Map.prototype.set` returns the map instance. Since this replaced a `Map`, any callers relying on chaining (e.g. `cache.set(k1, v1).set(k2, v2)`) will now fail silently. Returning `this` instead would preserve Map-like semantics and avoid surprising breakages.
</issue_to_address>

### Comment 2
<location path="desktop/main.js" line_range="721" />
<code_context>
  const child = spawn(pythonCommand, [SERVER_SCRIPT], {
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `candidateIndex`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

### Comment 3
<location path="desktop/main.js" line_range="721" />
<code_context>
  const child = spawn(pythonCommand, [SERVER_SCRIPT], {
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `candidates`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Align LruCache.set return value with Map semantics to avoid surprises for callers.

LruCache.set currently returns value, but Map.prototype.set returns the map instance. Since this replaced a Map, any callers relying on chaining (e.g. cache.set(k1, v1).set(k2, v2)) will now fail silently. Returning this instead would preserve Map-like semantics and avoid surprising breakages.

function launchResearchUiProcess(candidates, candidateIndex = 0) {
const pythonCommand = candidates[candidateIndex];
appendLog(`[startup] launching research_ui with ${pythonCommand}`);
const child = spawn(pythonCommand, [SERVER_SCRIPT], {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument candidateIndex. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

@Whiteks1 Whiteks1 merged commit 4ece253 into main Apr 2, 2026
1 of 2 checks passed
@Whiteks1 Whiteks1 deleted the codex/desktop-electron-p1-hardening branch April 2, 2026 08:03
@Whiteks1
Copy link
Copy Markdown
Owner Author

Whiteks1 commented Apr 2, 2026

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 security issues, 1 other issue, and left some high level feedback:

Security issues:

  • Detected calls to child_process from a function argument candidateIndex. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)
  • Detected calls to child_process from a function argument candidates. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)

General comments:

  • In resolvePythonCandidates, consider checking executability (e.g., fs.constants.X_OK in addition to R_OK) for absolute paths so we avoid selecting a Python path that is readable but not runnable.
  • The Python startup retry logic in launchResearchUiProcess/bindResearchUiProcess only reacts to specific error.code values; you might want to also handle failure modes that surface as immediate non‑zero exits (e.g., check exit codes for a quick retry to the next candidate).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `resolvePythonCandidates`, consider checking executability (e.g., `fs.constants.X_OK` in addition to `R_OK`) for absolute paths so we avoid selecting a Python path that is readable but not runnable.
- The Python startup retry logic in `launchResearchUiProcess`/`bindResearchUiProcess` only reacts to specific `error.code` values; you might want to also handle failure modes that surface as immediate non‑zero exits (e.g., check exit codes for a quick retry to the next candidate).

## Individual Comments

### Comment 1
<location path="desktop/renderer/app.js" line_range="313-317" />
<code_context>
 }

 function ensureRefreshLoop() {
+  if (!state.workspace.serverUrl) return;
   refreshSnapshot();
   if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs);
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Guard the refresh loop against restarting when automatic refresh has been paused after repeated errors

Because `ensureRefreshLoop` is still called from the workspace state subscription, a new interval will be started whenever `serverUrl` is set, even if `snapshotStatus.refreshPaused` is true. To honor the intended "pause until user intervenes" behavior, add a guard here, e.g. `if (state.snapshotStatus.refreshPaused) return;`, so the loop only restarts when explicitly reset in `retryWorkspaceRuntime`.

```suggestion
function ensureRefreshLoop() {
  if (!state.workspace.serverUrl) return;
  if (state.snapshotStatus?.refreshPaused) return;
  refreshSnapshot();
  if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs);
}
```
</issue_to_address>

### Comment 2
<location path="desktop/main.js" line_range="721" />
<code_context>
  const child = spawn(pythonCommand, [SERVER_SCRIPT], {
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `candidateIndex`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

### Comment 3
<location path="desktop/main.js" line_range="721" />
<code_context>
  const child = spawn(pythonCommand, [SERVER_SCRIPT], {
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `candidates`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines 313 to 317
function ensureRefreshLoop() {
if (!state.workspace.serverUrl) return;
refreshSnapshot();
if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Guard the refresh loop against restarting when automatic refresh has been paused after repeated errors

Because ensureRefreshLoop is still called from the workspace state subscription, a new interval will be started whenever serverUrl is set, even if snapshotStatus.refreshPaused is true. To honor the intended "pause until user intervenes" behavior, add a guard here, e.g. if (state.snapshotStatus.refreshPaused) return;, so the loop only restarts when explicitly reset in retryWorkspaceRuntime.

Suggested change
function ensureRefreshLoop() {
if (!state.workspace.serverUrl) return;
refreshSnapshot();
if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs);
}
function ensureRefreshLoop() {
if (!state.workspace.serverUrl) return;
if (state.snapshotStatus?.refreshPaused) return;
refreshSnapshot();
if (!state.refreshTimer) state.refreshTimer = window.setInterval(refreshSnapshot, CONFIG.refreshIntervalMs);
}

function launchResearchUiProcess(candidates, candidateIndex = 0) {
const pythonCommand = candidates[candidateIndex];
appendLog(`[startup] launching research_ui with ${pythonCommand}`);
const child = spawn(pythonCommand, [SERVER_SCRIPT], {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument candidateIndex. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

function launchResearchUiProcess(candidates, candidateIndex = 0) {
const pythonCommand = candidates[candidateIndex];
appendLog(`[startup] launching research_ui with ${pythonCommand}`);
const child = spawn(pythonCommand, [SERVER_SCRIPT], {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument candidates. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

@Whiteks1
Copy link
Copy Markdown
Owner Author

Whiteks1 commented Apr 2, 2026

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 security issues, 2 other issues, and left some high level feedback:

Security issues:

  • Detected calls to child_process from a function argument candidateIndex. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)
  • Detected calls to child_process from a function argument candidates. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed. (link)

General comments:

  • In resolvePythonCandidates, consider checking execute permission (fs.constants.X_OK in addition to or instead of R_OK) for absolute paths so that unusable but readable interpreter paths are filtered out before spawn is attempted.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `resolvePythonCandidates`, consider checking execute permission (`fs.constants.X_OK` in addition to or instead of `R_OK`) for absolute paths so that unusable but readable interpreter paths are filtered out before spawn is attempted.

## Individual Comments

### Comment 1
<location path="desktop/main.js" line_range="621-630" />
<code_context>
+function resolvePythonCandidates() {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider checking execute permission (X_OK) for Python binaries on POSIX platforms.

Right now `resolvePythonCandidates` only checks `fs.constants.R_OK` for absolute paths. On POSIX you can have readable but non-executable files, which will reliably cause `EACCES` on spawn and force the retry logic. To avoid repeatedly trying unusable interpreters, also check `X_OK` on non-Windows platforms, e.g. `fs.accessSync(candidate, fs.constants.R_OK | fs.constants.X_OK)` behind a platform guard.

Suggested implementation:

```javascript
function resolvePythonCandidates() {
  const isWindows = process.platform === "win32";
  const localVenv = path.join(
    PROJECT_ROOT,
    ".venv",
    isWindows ? "Scripts" : "bin",
    isWindows ? "python.exe" : "python",
  );


```

```javascript
      // On POSIX require both read and execute permissions for Python binaries, to avoid
      // repeatedly trying interpreters that will fail with EACCES on spawn.
      const accessMode = process.platform === "win32"
        ? fs.constants.R_OK
        : fs.constants.R_OK | fs.constants.X_OK;

      fs.accessSync(candidate, accessMode);

```

I assumed there is an existing `fs.accessSync(candidate, fs.constants.R_OK);` call inside `resolvePythonCandidates` where absolute-path Python candidates are validated. If the call site differs, apply the same pattern:

1. Compute `accessMode` with `R_OK` on Windows and `R_OK | X_OK` on non-Windows:
   `const accessMode = process.platform === "win32" ? fs.constants.R_OK : fs.constants.R_OK | fs.constants.X_OK;`
2. Pass `accessMode` to `fs.accessSync` instead of `fs.constants.R_OK`.

No other logic (candidate enumeration, error handling, or retry behavior) needs to change for this improvement.
</issue_to_address>

### Comment 2
<location path="desktop/renderer/modules/utils.js" line_range="199-208" />
<code_context>
   return rows;
 }

+export class LruCache {
+  constructor(maxSize = 50) {
+    this.maxSize = Math.max(1, Number(maxSize) || 50);
+    this.cache = new Map();
+  }
+
+  clear() {
+    this.cache.clear();
+  }
+
+  has(key) {
+    return this.cache.has(key);
+  }
+
+  get(key) {
+    if (!this.cache.has(key)) return undefined;
+    const value = this.cache.get(key);
+    this.cache.delete(key);
+    this.cache.set(key, value);
+    return value;
+  }
+
</code_context>
<issue_to_address>
**nitpick (bug_risk):** LruCache.set returns the value instead of the cache instance, which may be surprising.

`set` currently returns `value`, while `Map.prototype.set` (and most caches) return `this` for chaining. If callers later assume `Map`-like behavior and chain calls or use the return as the cache, this could fail in a non-obvious way. Unless there’s a strong reason to return the stored value, consider returning `this` instead.
</issue_to_address>

### Comment 3
<location path="desktop/main.js" line_range="721" />
<code_context>
  const child = spawn(pythonCommand, [SERVER_SCRIPT], {
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `candidateIndex`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

### Comment 4
<location path="desktop/main.js" line_range="721" />
<code_context>
  const child = spawn(pythonCommand, [SERVER_SCRIPT], {
</code_context>
<issue_to_address>
**security (javascript.lang.security.detect-child-process):** Detected calls to child_process from a function argument `candidates`. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

*Source: opengrep*
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +621 to +630
function resolvePythonCandidates() {
const isWindows = process.platform === "win32";
const localVenv = path.join(PROJECT_ROOT, ".venv", isWindows ? "Scripts" : "bin", isWindows ? "python.exe" : "python");
if (localVenv && require("fs").existsSync(localVenv)) {
return localVenv;
}
return process.env.PYTHON || (isWindows ? "python" : "python3");
}

function extractServerUrl(line) {
const match = String(line).match(/URL:\s*(http:\/\/[^\s]+)/i);
return match ? match[1] : null;
}

async function startResearchUiServer({ forceRestart = false } = {}) {
if (forceRestart) {
stopResearchUiServer({ force: true });
}

if (researchServerProcess) return;

const existingUrl = await detectResearchUiServerUrl();
if (existingUrl) {
researchServerOwned = false;
markResearchUiReady(existingUrl, "external");
appendLog(`[startup] reusing existing research_ui server at ${existingUrl}`);
return;
}

updateWorkspaceState({
status: "starting",
serverUrl: null,
error: null,
source: "managed",
const candidates = [
localVenv,
process.env.PYTHON || "",
isWindows ? "python" : "python3",
]
.map((value) => String(value || "").trim())
.filter(Boolean);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Consider checking execute permission (X_OK) for Python binaries on POSIX platforms.

Right now resolvePythonCandidates only checks fs.constants.R_OK for absolute paths. On POSIX you can have readable but non-executable files, which will reliably cause EACCES on spawn and force the retry logic. To avoid repeatedly trying unusable interpreters, also check X_OK on non-Windows platforms, e.g. fs.accessSync(candidate, fs.constants.R_OK | fs.constants.X_OK) behind a platform guard.

Suggested implementation:

function resolvePythonCandidates() {
  const isWindows = process.platform === "win32";
  const localVenv = path.join(
    PROJECT_ROOT,
    ".venv",
    isWindows ? "Scripts" : "bin",
    isWindows ? "python.exe" : "python",
  );
      // On POSIX require both read and execute permissions for Python binaries, to avoid
      // repeatedly trying interpreters that will fail with EACCES on spawn.
      const accessMode = process.platform === "win32"
        ? fs.constants.R_OK
        : fs.constants.R_OK | fs.constants.X_OK;

      fs.accessSync(candidate, accessMode);

I assumed there is an existing fs.accessSync(candidate, fs.constants.R_OK); call inside resolvePythonCandidates where absolute-path Python candidates are validated. If the call site differs, apply the same pattern:

  1. Compute accessMode with R_OK on Windows and R_OK | X_OK on non-Windows:
    const accessMode = process.platform === "win32" ? fs.constants.R_OK : fs.constants.R_OK | fs.constants.X_OK;
  2. Pass accessMode to fs.accessSync instead of fs.constants.R_OK.

No other logic (candidate enumeration, error handling, or retry behavior) needs to change for this improvement.

Comment on lines +199 to +208
export class LruCache {
constructor(maxSize = 50) {
this.maxSize = Math.max(1, Number(maxSize) || 50);
this.cache = new Map();
}

clear() {
this.cache.clear();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick (bug_risk): LruCache.set returns the value instead of the cache instance, which may be surprising.

set currently returns value, while Map.prototype.set (and most caches) return this for chaining. If callers later assume Map-like behavior and chain calls or use the return as the cache, this could fail in a non-obvious way. Unless there’s a strong reason to return the stored value, consider returning this instead.

function launchResearchUiProcess(candidates, candidateIndex = 0) {
const pythonCommand = candidates[candidateIndex];
appendLog(`[startup] launching research_ui with ${pythonCommand}`);
const child = spawn(pythonCommand, [SERVER_SCRIPT], {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument candidateIndex. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

function launchResearchUiProcess(candidates, candidateIndex = 0) {
const pythonCommand = candidates[candidateIndex];
appendLog(`[startup] launching research_ui with ${pythonCommand}`);
const child = spawn(pythonCommand, [SERVER_SCRIPT], {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (javascript.lang.security.detect-child-process): Detected calls to child_process from a function argument candidates. This could lead to a command injection if the input is user controllable. Try to avoid calls to child_process, and if it is needed ensure user input is correctly sanitized or sandboxed.

Source: opengrep

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hardening(desktop): make shell startup single-instance and operationally visible

1 participant