From b8bfd89ebbbb90e94d2b9c77f53ed5562c2d2693 Mon Sep 17 00:00:00 2001 From: Neil Date: Thu, 10 Sep 2026 01:46:32 -0700 Subject: [PATCH] fix(agent-hooks): keep Orca's own CODEX_HOME out of the host probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Orca exports CODEX_HOME/ORCA_CODEX_HOME for its managed Codex accounts. The probe inherited them, so it reported Orca's own account home as if the host user had redirected there — which broke the Node 18 managed-hook smoke and would have installed hooks into the managed account home on any client that runs the relay in-process. The child now gets the installer's HOME pinned and both variables stripped; a redirect the user's profile or launcher sets is unaffected, since the login shell re-exports it. --- .../codex-app-server-home-probe.test.ts | 50 ++++++++++++++++++- .../codex-app-server-home-probe.ts | 17 +++++++ src/main/agent-hooks/managed-hook-runtime.ts | 6 ++- 3 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/main/agent-hooks/codex-app-server-home-probe.test.ts b/src/main/agent-hooks/codex-app-server-home-probe.test.ts index ee73c7dfdf7..658ca5d765f 100644 --- a/src/main/agent-hooks/codex-app-server-home-probe.test.ts +++ b/src/main/agent-hooks/codex-app-server-home-probe.test.ts @@ -1,8 +1,11 @@ -import { afterEach, describe, expect, it } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { probeCodexHomeViaAppServer } from './codex-app-server-home-probe' +import { + buildCodexProbeEnvironment, + probeCodexHomeViaAppServer +} from './codex-app-server-home-probe' // The probe speaks to a real child over pipes; `/bin/sh` is the shell it uses // on every host that reaches this code (Windows remotes never install hooks). @@ -122,3 +125,46 @@ sleep 30 expect(Date.now() - startedAt).toBeLessThan(5_000) }) }) + +describe('buildCodexProbeEnvironment', () => { + afterEach(() => { + vi.unstubAllEnvs() + }) + + it('drops the CODEX_HOME Orca injected for its own managed accounts', () => { + // Orca exports these for its managed Codex accounts. Reading one back would + // report Orca's own answer as if it were the host user's configuration. + vi.stubEnv('CODEX_HOME', '/orca/codex-accounts/abc/home') + vi.stubEnv('ORCA_CODEX_HOME', '/orca/codex-accounts/abc/home') + + const env = buildCodexProbeEnvironment('/home/dev') + + expect(env.CODEX_HOME).toBeUndefined() + expect(env.ORCA_CODEX_HOME).toBeUndefined() + }) + + it('pins HOME to the home the installer resolved', () => { + expect(buildCodexProbeEnvironment('/home/dev').HOME).toBe('/home/dev') + }) + + onPosix('asks Codex under the installer s home, not Orca s injected one', async () => { + vi.stubEnv('CODEX_HOME', '/orca/codex-accounts/abc/home') + const fake = await withFakeCodex( + `#!/bin/sh +read -r _line +home="\${CODEX_HOME:-$HOME/.codex}" +printf '{"id":1,"result":{"codexHome":"%s"}}\\n' "$home" +sleep 5 +` + ) + + await expect( + probeCodexHomeViaAppServer({ + loginShell: '/bin/sh', + loginShellFlag: '-c', + env: { ...buildCodexProbeEnvironment('/home/dev'), PATH: fake.PATH }, + timeoutMs: 10_000 + }) + ).resolves.toBe('/home/dev/.codex') + }) +}) diff --git a/src/main/agent-hooks/codex-app-server-home-probe.ts b/src/main/agent-hooks/codex-app-server-home-probe.ts index 3282263933e..74d061ee65b 100644 --- a/src/main/agent-hooks/codex-app-server-home-probe.ts +++ b/src/main/agent-hooks/codex-app-server-home-probe.ts @@ -20,6 +20,23 @@ const PROBE_TIMEOUT_MS = 12_000 const MAX_STDOUT_BYTES = 64 * 1024 const INITIALIZE_ID = 1 +/** + * The environment the probe hands the child. + * + * `HOME` is pinned to the home the installer resolved, so parent and child + * agree on which account is being configured. `CODEX_HOME`/`ORCA_CODEX_HOME` + * are dropped because Orca injects them for its own managed accounts — reading + * one back would report Orca's own answer as if it were the host user's. A + * value the user's profile or launcher wrapper sets is unaffected: the login + * shell re-exports it, which is the whole point of the probe. + */ +export function buildCodexProbeEnvironment(home: string): NodeJS.ProcessEnv { + const env: NodeJS.ProcessEnv = { ...process.env, HOME: home } + delete env.CODEX_HOME + delete env.ORCA_CODEX_HOME + return env +} + /** * `codex app-server` exits without answering when stdin reaches EOF, so the * request is written and the pipe is left open until the reply lands. diff --git a/src/main/agent-hooks/managed-hook-runtime.ts b/src/main/agent-hooks/managed-hook-runtime.ts index 64880d24cc6..d447e75c83a 100644 --- a/src/main/agent-hooks/managed-hook-runtime.ts +++ b/src/main/agent-hooks/managed-hook-runtime.ts @@ -3,7 +3,10 @@ import { basename } from 'node:path' import { homedir, userInfo } from 'node:os' import { promisify } from 'node:util' import { installRemoteManagedAgentHooks } from './remote-managed-hook-installers' -import { probeCodexHomeViaAppServer } from './codex-app-server-home-probe' +import { + buildCodexProbeEnvironment, + probeCodexHomeViaAppServer +} from './codex-app-server-home-probe' import type { AgentHookTarget } from '../../shared/agent-hook-types' import { createManagedHookLocalFilesystem } from './managed-hook-local-filesystem' import { withManagedHookInstallLock } from './managed-hook-install-lock' @@ -109,6 +112,7 @@ export async function resolveRelayRedirectedCodexHome( const reported = await probe({ loginShell: shell, loginShellFlag: flag, + env: buildCodexProbeEnvironment(home), ...(signal ? { signal } : {}) }) const codexHome = reported === null ? null : normalizePosixAgentHome(reported.trim())