Skip to content

Stop running every agent CLI from background version checks - #99

Open
jparker2006 wants to merge 2 commits into
davidondrej:mainfrom
jparker2006:fix-96-version-sync
Open

jparker2006 wants to merge 2 commits into
davidondrej:mainfrom
jparker2006:fix-96-version-sync

Conversation

@jparker2006

Copy link
Copy Markdown
Contributor

Problem. startClaudeVersionSync (started from routes.ts for every user) called macHarnessVersions() 30 s after every launch and every 5 minutes. That runs --version on codex, claude, opencode, fx, and openclaw, before sign-in, forever, though the sync only uses Claude's version and can only do anything for an account on a shared VM. When no standalone Codex CLI exists, findCliExecutable("codex") falls back to the binary inside /Applications/Codex.app or ChatGPT.app, so this could make macOS trash a flagged Codex.app again within 5 minutes of the user restoring it. The same full macHarnessVersions() was also reached from automatic sandbox paths: spare prewarms, sidebar preloads, thread-view wakes, and OpenCode's login check.

Repro. New test/services/cloudroom-harness-versions.test.ts puts fake codex, claude, opencode, fx, and openclaw on a temp PATH (plus CODEX_HOME). Each one touches a marker file when it runs. On origin/main the 4 background tests fail, and every one of them shows codex ran:

× the Claude version sync runs no CLI before a VM is connected
  expected [ 'claude', 'codex', 'fx', …(2) ] to deeply equal []
× the Claude version sync runs only Claude's CLI for a VM account
  expected [ 'claude', 'codex', 'fx', …(2) ] to deeply equal [ 'claude' ]
× OpenCode's login check runs only OpenCode
  expected [ 'claude', 'codex', 'fx', …(2) ] to deeply equal [ 'opencode' ]
× prewarms, preloads, and view wakes run no CLI; a wake for work does, and later background wakes reuse it
  expected [ 'claude', 'codex', 'fx', …(2) ] to deeply equal []

A 5th test stubs findCliExecutable("codex") to null, the way #97 makes it behave, with a fake codex on PATH. Without the codex change, codex login falls back to the bare name and runs the fake.

Fix.

  • startClaudeVersionSync resolves vm() first. That is teleportClient(), which only stats and reads the saved cloudroom.json and throws unless a VM connection is saved. Then it runs only claude --version (versionOf(claudeBinary())). If you're signed out or sandbox-only, each tick is now one file stat and spawns nothing. routes.ts is unchanged: the per-tick check also covers an account that signs in later.
  • New lastMacHarnessVersions() returns what the last user-action read found ({} if nothing) and never spawns anything. Automatic sandbox paths use it.
  • versionOf is exported, so a caller can run just the one binary it needs.
  • Pairs with Never run the Codex inside Codex.app or ChatGPT.app #97. startMacCodexLogin (codex login) and macCodexLoginExpired (codex app-server) used findCliExecutable("codex") ?? "codex". With Never run the Codex inside Codex.app or ChatGPT.app #97, a null lookup means the only Codex is inside Codex.app or ChatGPT.app (including through a PATH symlink), so the bare "codex" would let the OS PATH lookup run that same binary. Now a null lookup means not installed: sign-in returns a finished run with the existing "Codex is not installed…" error and spawns nothing, and the expiry check returns false (signed in), which is what it already did when the spawn failed. Both run only after a click: Connect / Sign in → POST /account/codex/login or POST /mac/codex-login. The polled GET /account/codex status never reaches macCodexLoginExpired; it runs only when action === "login".

Audit of macHarnessVersions() callers (sandboxes.ts):

Call site What triggers it Verdict Change
macOpencodeLogin (~180) Automatic: login copy on launch (start() → syncLogins), every warm/preload (beforeWake), the onboarding agent step (GET /mac/agents). Also on Connect. Automatic Runs only opencode --version, never codex/claude/fx/openclaw
SandboxDirectory.connection wake (~676), no trigger Sending work: new thread, message delivery, start/retry, fork, teleport User action Unchanged (runs the CLIs, cached for 60 s)
same, thread_typing User typing in a cloud thread User action Unchanged
same, thread_view useCloudThreadWake fires 500 ms after any cloud thread is shown, including on launch with a restored route and on every window refocus. Opening the terminal uses it too. Automatic lastMacHarnessVersions()
preloadOne (~763) Sidebar preload on render, scroll, and refocus Automatic (prewarm) lastMacHarnessVersions()
warm (~837) app_launch (start()), app_activity (10 s account poll while visible), composer (onboarding project pick) Automatic (prewarm) lastMacHarnessVersions()

Tests.

  • cd gui/apps/server && npx vitest run --config vitest.config.ts test/services/cloudroom-harness-versions.test.ts: 5 passed. Before the fix, all 5 fail (output above).
  • npx vitest run --config vitest.config.ts test/services/cloudroom-{harness-versions,sandboxes,account,onboarding,pi,claude-token}.test.ts test/services/cloudroom.test.ts: 7 files, 52 tests passed.
  • npx tsc --noEmit (server): clean.
  • Full server suite (npx vitest run --config vitest.config.ts --maxWorkers=2): 2725 passed, 22 failed in 13 files. The same 22 fail on origin/main in this sandbox (identical failure set), mostly because the plugin SDK dist isn't built (@get-bb/plugin-sdk/... dist is not built), plus marketplace and plugin fixtures. None of them touch cloudroom code.

What users may notice. Prewarmed spares, sidebar-preloaded sandboxes, and passive view wakes now send the Mac's harness versions only after a user action in the same app session has read them. Until then they send {}, the same thing a Mac with no CLIs sends. The wake that sends work still reads and sends real versions, so a thread started fresh still matches the Mac. Two cases can lag: an existing thread woken only by viewing it, and a brand-new thread landing on a spare. Both may keep the cloud's current version until a later work wake.

Not covered here (reported, not edited; outside this PR's files): other background agent-CLI runs I found during the audit.

  • refreshMacMcp → macMcpServers() runs codex mcp list --json once a minute from the 1.5 s CloudroomService timer (uploadMacConfig). That happens when you're signed in with sandboxes and the account has cloud MCP servers. Same Codex.app risk as cloudroom identifies Codex as Malware #96, unless findCliExecutable stops resolving into app bundles.
  • downloadCloudLogins → macClaudeSignedIn() runs claude auth status --json from syncLogins at launch.
  • macOpencodeLogin still runs opencode --version, and for OpenCode 2 opencode auth export, from the automatic paths above. No .app bundle is involved, but it is still a background run.

macOS-only behavior (the Codex.app fallback path, Gatekeeper) is not exercised here: the tests run on Linux with fakes on PATH.

Refs #96

The Claude version sync now runs only claude --version, and only once a VM
connection is saved. Prewarms, sidebar preloads, and view wakes reuse the
versions the last user action read instead of running the CLIs.

Refs davidondrej#96
With davidondrej#97 a null lookup means no usable Codex; a bare "codex" could still
reach one inside Codex.app through the OS PATH lookup. Sign-in now fails with
the not-installed error, and the expiry check counts it as signed in.

Refs davidondrej#96
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.

1 participant