diff --git a/src/main/claude/agent-runner.ts b/src/main/claude/agent-runner.ts index 08f2727c2..f60bbe721 100644 --- a/src/main/claude/agent-runner.ts +++ b/src/main/claude/agent-runner.ts @@ -145,11 +145,14 @@ function resolveBundledToolsBinDir(): string | null { * 3. Deduplicates all entries * 4. Writes the result back to `process.env.PATH` * - * Called once before the first `createCodingTools()` — subsequent calls are no-ops. + * Idempotent: enriches once and caches the result, so it is safe to call from + * multiple entry points. Call at app startup (before the sandbox bootstrap / + * Lima detection) and before the first `createCodingTools()` — subsequent calls + * are no-ops. */ let pathEnriched = false; -async function enrichProcessPathForBuild(): Promise { +export async function enrichProcessPathForBuild(): Promise { if (pathEnriched) return; pathEnriched = true; diff --git a/src/main/index.ts b/src/main/index.ts index dd69b199a..c4ab869c1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -38,6 +38,7 @@ import { SandboxSync } from './sandbox/sandbox-sync'; import { WSLBridge } from './sandbox/wsl-bridge'; import { LimaBridge } from './sandbox/lima-bridge'; import { getSandboxBootstrap } from './sandbox/sandbox-bootstrap'; +import { enrichProcessPathForBuild } from './claude/agent-runner'; import type { MCPServerConfig } from './mcp/mcp-manager'; import type { ClientEvent, @@ -759,6 +760,14 @@ function sendToRenderer(event: ServerEvent) { app .whenReady() .then(async () => { + // Enrich PATH before anything else so Homebrew-installed CLIs (notably + // `limactl`) are discoverable by the sandbox bootstrap and the + // `sandbox.checkLima` IPC. In the packaged macOS app, GUI processes inherit + // a stripped launchd PATH that excludes /opt/homebrew/bin and + // /usr/local/bin, which otherwise makes `which limactl` fail and reports + // the sandbox as unavailable even when Lima is installed. + await enrichProcessPathForBuild(); + // Apply dev logs setting from config const enableDevLogs = configStore.get('enableDevLogs'); setDevLogsEnabled(enableDevLogs); diff --git a/tests/startup-path-enrichment.test.ts b/tests/startup-path-enrichment.test.ts new file mode 100644 index 000000000..28980d910 --- /dev/null +++ b/tests/startup-path-enrichment.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; + +const read = (rel: string): string => readFileSync(path.resolve(process.cwd(), rel), 'utf8'); + +/** + * Regression guard for the macOS sandbox-detection bug. + * + * In the packaged macOS app, GUI processes inherit a stripped launchd PATH that + * excludes /opt/homebrew/bin and /usr/local/bin. The sandbox/Lima detection + * (`which limactl`) runs at startup and from the `sandbox.checkLima` IPC, so it + * must run AFTER the login-shell PATH has been restored — otherwise Lima is + * reported as unavailable even when it is installed. The PATH enrichment used to + * run only before the first `createCodingTools()` call (an agent task), which is + * too late for the sandbox detection. + */ +describe('PATH enrichment is wired into app startup, before sandbox detection', () => { + const agentRunner = read('src/main/claude/agent-runner.ts'); + const indexTs = read('src/main/index.ts'); + + it('exports enrichProcessPathForBuild so the startup path can reuse it', () => { + expect(agentRunner).toMatch(/export\s+async\s+function\s+enrichProcessPathForBuild/); + }); + + it('keeps the enrichment idempotent so repeated calls are safe', () => { + expect(agentRunner).toContain('let pathEnriched = false;'); + expect(agentRunner).toContain('if (pathEnriched) return;'); + }); + + it('imports the enrichment into the main entrypoint', () => { + expect(indexTs).toMatch( + /import\s*\{\s*enrichProcessPathForBuild\s*\}\s*from\s*['"]\.\/claude\/agent-runner['"]/ + ); + }); + + it('awaits the enrichment during the whenReady startup sequence', () => { + const readyIdx = indexTs.indexOf('.whenReady()'); + const enrichIdx = indexTs.indexOf('await enrichProcessPathForBuild()'); + expect(readyIdx).toBeGreaterThan(-1); + expect(enrichIdx).toBeGreaterThan(-1); + // The enrichment must be invoked from the startup sequence, not only from + // the lazy agent-task path, so it precedes any Lima detection. + expect(enrichIdx).toBeGreaterThan(readyIdx); + }); +});