From 0adef843c89a25170337abf72a30dbedfbeef1bc Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:46:49 -0700 Subject: [PATCH 1/5] fix(codex): write workspace trust into the Codex home the launch runs on Every local Codex launch wrote folder trust into two files: Orca's shared mirror home and the user's own ~/.codex/config.toml (under the launch env's HOME). It never wrote the home the launched Codex actually reads. A Codex started on the mirror or on a per-account home therefore also changed the user's real ~/.codex, which that Codex never reads, and a per-account home only picked trust up through the mirror on some later launch. Trust now follows the CODEX_HOME Orca gives the launch: Orca's mirror (kept, it is Orca-owned) plus the one config the launched Codex reads, written once when they are the same file. With no CODEX_HOME (the real-home lane) that is ~/.codex, the home the real-home hook lane uses. The terminal spawn builders pass the selected Codex home they already hold; a structured Codex chat now writes trust after its account home is chosen, so a resume trusts the home its conversation lives in. The SSH relay uses the same one rule, unchanged. --- src/main/agent-trust-presets.test.ts | 17 ++- .../agent-workspace-trust-codex-home.test.ts | 115 ++++++++++++++++++ ...gent-workspace-trust-guard-failure.test.ts | 3 +- src/main/agent-workspace-trust-spawn.test.ts | 4 +- src/main/agent-workspace-trust-spawn.ts | 11 +- src/main/agent-workspace-trust.test.ts | 22 +++- src/main/agent-workspace-trust.ts | 13 +- src/main/codex/codex-home-paths.test.ts | 16 +++ src/main/codex/codex-home-paths.ts | 17 ++- ...dex-trust-config-concurrent-launch.test.ts | 7 +- .../execution-host-workspace-trust.test.ts | 20 +++ src/main/execution-host-workspace-trust.ts | 12 +- src/main/ipc/pty/ipc/spawn-options.ts | 1 + src/main/ipc/pty/runtime/spawn-options.ts | 1 + ...pawn-options-agent-workspace-trust.test.ts | 17 ++- ...structured-agent-session-create-support.ts | 25 ++-- ...ctured-agent-session-create-intent.test.ts | 53 ++++++-- ...odex-launch-during-real-home-grant.test.ts | 20 ++- src/relay/agent-workspace-trust-spawn.ts | 6 +- 19 files changed, 324 insertions(+), 56 deletions(-) create mode 100644 src/main/agent-workspace-trust-codex-home.test.ts diff --git a/src/main/agent-trust-presets.test.ts b/src/main/agent-trust-presets.test.ts index e0052aa8b456..73b94ca12067 100644 --- a/src/main/agent-trust-presets.test.ts +++ b/src/main/agent-trust-presets.test.ts @@ -269,7 +269,7 @@ describe('markCodexProjectTrusted', () => { const held = runExclusivelyForCodexTrustConfig(configPath, () => grantHoldingTheFile) const marked = markCodexProjectTrusted( workspace, - getLocalCodexTrustConfigFiles(testState.fakeHomeDir) + getLocalCodexTrustConfigFiles(join(testState.fakeHomeDir, '.codex', 'config.toml')) ) await Promise.resolve() expect(existsSync(configPath)).toBe(false) @@ -316,7 +316,10 @@ describe('markCodexProjectTrusted', () => { } async function expectWorkspaceTrusted(workspace: string): Promise { - await markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir)) + await markCodexProjectTrusted( + workspace, + getLocalCodexTrustConfigFiles(join(testState.fakeHomeDir, '.codex', 'config.toml')) + ) const runtimeHome = join(testState.userDataDir, 'codex-runtime-home', 'home') const [system, runtime] = [ join(testState.fakeHomeDir, '.codex', 'config.toml'), @@ -410,7 +413,10 @@ describe('markCodexProjectTrusted', () => { const workspace = mkdtempSync(join(tmpdir(), 'orca-codex-ws-')) try { const realpath = realpathSync.native(workspace) - await markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir)) + await markCodexProjectTrusted( + workspace, + getLocalCodexTrustConfigFiles(join(testState.fakeHomeDir, '.codex', 'config.toml')) + ) const configPath = join(testState.fakeHomeDir, '.codex', 'config.toml') const runtimeConfigPath = join( testState.userDataDir, @@ -464,7 +470,10 @@ describe('markCodexProjectTrusted', () => { 'utf-8' ) - await markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir)) + await markCodexProjectTrusted( + workspace, + getLocalCodexTrustConfigFiles(join(testState.fakeHomeDir, '.codex', 'config.toml')) + ) const written = readFileSync(join(codexDir, 'config.toml'), 'utf-8') const runtimeWritten = readFileSync(join(runtimeCodexDir, 'config.toml'), 'utf-8') diff --git a/src/main/agent-workspace-trust-codex-home.test.ts b/src/main/agent-workspace-trust-codex-home.test.ts new file mode 100644 index 000000000000..d35fec8a7760 --- /dev/null +++ b/src/main/agent-workspace-trust-codex-home.test.ts @@ -0,0 +1,115 @@ +import { + existsSync, + mkdirSync, + mkdtempSync, + readFileSync, + realpathSync, + rmSync, + writeFileSync +} from 'node:fs' +import { tmpdir } from 'node:os' +import type * as Os from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +const state = vi.hoisted(() => ({ home: '' })) + +vi.mock('node:os', async (importOriginal) => { + const actual = await importOriginal() + return { ...actual, homedir: () => state.home } +}) + +import { applyAgentWorkspaceTrust, type AgentTrustLaunchContext } from './agent-workspace-trust' +import { resolveOrcaManagedCodexHomePath } from './codex/codex-home-paths' + +const USER_CONFIG = '# the user\'s own Codex config\nmodel = "o3"\n' + +let root: string +let workspace: string +let previousUserDataPath: string | undefined + +function userCodexConfig(): string { + return join(state.home, '.codex', 'config.toml') +} + +function mirrorConfig(): string { + return join(resolveOrcaManagedCodexHomePath(), 'config.toml') +} + +function trustedProjects(configFile: string): string[] { + return [...readFileSync(configFile, 'utf-8').matchAll(/^\[projects\."(.*)"\]$/gm)].map((match) => + match[1].replaceAll('\\\\', '\\') + ) +} + +function launchOn(codexHome: string | null): Promise { + const context: AgentTrustLaunchContext = { + // Why HOME too: the env's home must not pull Codex trust back into a ~/.codex. + env: { HOME: state.home, USERPROFILE: state.home }, + claudeAuth: null, + wslDistro: null, + connectionId: null, + codexHome + } + return applyAgentWorkspaceTrust('codex', workspace, context) +} + +beforeEach(() => { + root = realpathSync(mkdtempSync(join(tmpdir(), 'orca-codex-trust-home-'))) + state.home = join(root, 'home') + workspace = join(root, 'projects', 'app') + mkdirSync(workspace, { recursive: true }) + mkdirSync(join(state.home, '.codex'), { recursive: true }) + writeFileSync(userCodexConfig(), USER_CONFIG) + previousUserDataPath = process.env.ORCA_USER_DATA_PATH + process.env.ORCA_USER_DATA_PATH = join(root, 'user-data') +}) + +afterEach(() => { + if (previousUserDataPath === undefined) { + delete process.env.ORCA_USER_DATA_PATH + } else { + process.env.ORCA_USER_DATA_PATH = previousUserDataPath + } + rmSync(root, { recursive: true, force: true }) +}) + +describe('local Codex trust follows the CODEX_HOME the launch runs on', () => { + it("writes a per-account home and Orca's mirror, never the user's ~/.codex", async () => { + const accountHome = join(root, 'user-data', 'codex-accounts', 'a', 'home') + mkdirSync(accountHome, { recursive: true }) + + await launchOn(accountHome) + + expect(trustedProjects(join(accountHome, 'config.toml'))).toEqual([workspace]) + expect(trustedProjects(mirrorConfig())).toEqual([workspace]) + expect(readFileSync(userCodexConfig(), 'utf-8')).toBe(USER_CONFIG) + }) + + it("writes only Orca's mirror when the launch runs on it", async () => { + await launchOn(resolveOrcaManagedCodexHomePath()) + + expect(trustedProjects(mirrorConfig())).toEqual([workspace]) + expect(readFileSync(userCodexConfig(), 'utf-8')).toBe(USER_CONFIG) + }) + + it('writes ~/.codex when the launch sets no CODEX_HOME, since that is the home Codex reads', async () => { + rmSync(userCodexConfig()) + + await launchOn(null) + + expect(trustedProjects(userCodexConfig())).toEqual([workspace]) + expect(trustedProjects(mirrorConfig())).toEqual([workspace]) + }) + + it('creates no ~/.codex config for a launch on another home', async () => { + rmSync(join(state.home, '.codex'), { recursive: true }) + const accountHome = join(root, 'accounts', 'b') + mkdirSync(accountHome, { recursive: true }) + + await launchOn(accountHome) + + expect(existsSync(join(state.home, '.codex'))).toBe(false) + expect(trustedProjects(join(accountHome, 'config.toml'))).toEqual([workspace]) + }) +}) diff --git a/src/main/agent-workspace-trust-guard-failure.test.ts b/src/main/agent-workspace-trust-guard-failure.test.ts index 9f5e1ab7d9ba..862ef2353dca 100644 --- a/src/main/agent-workspace-trust-guard-failure.test.ts +++ b/src/main/agent-workspace-trust-guard-failure.test.ts @@ -25,7 +25,8 @@ describe('applyAgentWorkspaceTrust when the breadth guard fails', () => { env: {}, claudeAuth: null, wslDistro: null, - connectionId: null + connectionId: null, + codexHome: null }) ).resolves.toEqual({}) expect(mocks.copilot).not.toHaveBeenCalled() diff --git a/src/main/agent-workspace-trust-spawn.test.ts b/src/main/agent-workspace-trust-spawn.test.ts index b87d58ff855c..11593948ed93 100644 --- a/src/main/agent-workspace-trust-spawn.test.ts +++ b/src/main/agent-workspace-trust-spawn.test.ts @@ -24,6 +24,7 @@ function spawnArgs(overrides: Partial { env: { CLAUDE_CONFIG_DIR: '/cfg' }, claudeAuth: null, wslDistro: null, - connectionId: null + connectionId: null, + codexHome: '/orca/codex-home' }) }) diff --git a/src/main/agent-workspace-trust-spawn.ts b/src/main/agent-workspace-trust-spawn.ts index f893529b53b8..043ba5ae526a 100644 --- a/src/main/agent-workspace-trust-spawn.ts +++ b/src/main/agent-workspace-trust-spawn.ts @@ -60,7 +60,8 @@ export function applyAgentWorkspaceTrustToSpawn( env: args.env, claudeAuth: args.claudeAuth, wslDistro: args.wslDistro, - connectionId: args.connectionId + connectionId: args.connectionId, + codexHome: args.codexHome }).then((fields) => { if (fields.agentWorkspaceTrust) { args.spawnOptions.agentWorkspaceTrust = fields.agentWorkspaceTrust @@ -74,16 +75,18 @@ export function applyAgentWorkspaceTrustToSpawn( */ export async function applyStructuredCodexWorkspaceTrust(args: { workspacePath: string - launchEnv: Record + /** The account home the chat's Codex runs on as its CODEX_HOME. */ + accountHomePath: string settings: AgentWorkspaceTrustSetting }): Promise { if (!isAgentWorkspaceTrustOn(args.settings)) { return } await applyAgentWorkspaceTrust('codex', args.workspacePath, { - env: args.launchEnv, + env: undefined, claudeAuth: null, wslDistro: null, - connectionId: null + connectionId: null, + codexHome: args.accountHomePath }) } diff --git a/src/main/agent-workspace-trust.test.ts b/src/main/agent-workspace-trust.test.ts index 8aead85ff78e..36ce6f530048 100644 --- a/src/main/agent-workspace-trust.test.ts +++ b/src/main/agent-workspace-trust.test.ts @@ -20,7 +20,7 @@ const mocks = vi.hoisted(() => ({ copilot: vi.fn<(path: string, home: string) => void>(), antigravity: vi.fn<(path: string, home: string) => void>(), qoder: vi.fn<(path: string, home: string) => void>(), - codexConfigFiles: vi.fn<(agentHome: string) => string[]>(() => CODEX_CONFIG_FILES), + codexConfigFiles: vi.fn<(launchedConfigFile: string) => string[]>(() => CODEX_CONFIG_FILES), claudeGrant: vi.fn() })) @@ -52,7 +52,8 @@ const local: AgentTrustLaunchContext = { env: {}, claudeAuth: null, wslDistro: null, - connectionId: null + connectionId: null, + codexHome: null } function pending(): { promise: Promise; release: () => void } { @@ -89,15 +90,28 @@ describe('applyAgentWorkspaceTrust on this machine', () => { it('writes per-user trust under the home the launch env names, where the agent reads it', async () => { const context = { ...local, env: { HOME: '/home/agent', USERPROFILE: '/home/agent' } } - for (const preset of ['codex', 'cursor', 'copilot', 'qoder', 'antigravity'] as const) { + for (const preset of ['cursor', 'copilot', 'qoder', 'antigravity'] as const) { await applyAgentWorkspaceTrust(preset, WORKSPACE, context) } - expect(mocks.codexConfigFiles).toHaveBeenCalledWith('/home/agent') for (const writer of [mocks.cursor, mocks.copilot, mocks.qoder, mocks.antigravity]) { expect(writer).toHaveBeenCalledWith(WORKSPACE, '/home/agent') } }) + it.each([ + ['the CODEX_HOME Orca gives the launch', '/orca/accounts/a/home', '/orca/accounts/a/home'], + // Why: with no CODEX_HOME Orca runs Codex on ~/.codex, whatever HOME the launch env names. + ['~/.codex without one', null, join(homedir(), '.codex')] + ])('writes Codex trust into %s', async (_label, codexHome, expectedHome) => { + const context = { + ...local, + env: { HOME: '/home/agent', USERPROFILE: '/home/agent' }, + codexHome + } + await applyAgentWorkspaceTrust('codex', WORKSPACE, context) + expect(mocks.codexConfigFiles).toHaveBeenCalledWith(join(expectedHome, 'config.toml')) + }) + it('contains a rejected or throwing write so the launch proceeds', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) mocks.codex.mockRejectedValueOnce(new Error('write failed')) diff --git a/src/main/agent-workspace-trust.ts b/src/main/agent-workspace-trust.ts index 0fc52991e3c2..5e26eca055ad 100644 --- a/src/main/agent-workspace-trust.ts +++ b/src/main/agent-workspace-trust.ts @@ -8,7 +8,11 @@ import { resolveLocalClaudeTrustConfig } from './claude/claude-folder-trust-file import type { ClaudeRuntimeAuthPreparation } from './claude-accounts/runtime-auth/runtime-auth-types' import type { AgentWorkspaceTrustSpawnRequest } from '../shared/agent-workspace-trust-spawn-request' import { parseWslUncPath } from '../shared/wsl-paths' -import { applyWorkspaceTrustOnThisHost, launchedAgentHome } from './execution-host-workspace-trust' +import { + applyWorkspaceTrustOnThisHost, + launchedAgentHome, + launchedCodexConfigFile +} from './execution-host-workspace-trust' import { getLocalCodexTrustConfigFiles } from './codex/codex-home-paths' import { getCachedWslHome } from './wsl-home-cache' @@ -21,6 +25,8 @@ export type AgentTrustLaunchContext = { wslDistro: string | null /** SSH connection that runs the agent; null means this machine. */ connectionId: string | null + /** The CODEX_HOME Orca gives this launch's Codex; null means Codex's default home. */ + codexHome: string | null } /** Spawn fields the dispatcher asks the caller to forward to the process owner. */ @@ -74,7 +80,10 @@ export async function applyAgentWorkspaceTrust( claudeAuth: context.claudeAuth, wslDistro: context.wslDistro }), - codexConfigFiles: () => getLocalCodexTrustConfigFiles(agentHome), + // Why homedir(), not the launch env's HOME: with no CODEX_HOME Orca runs Codex on the + // ~/.codex its real-home hook lane uses (getSystemCodexHomePath). + codexConfigFiles: () => + getLocalCodexTrustConfigFiles(launchedCodexConfigFile(context.codexHome, homedir())), // Why: Codex queues behind a config lane it shares with Orca's hook installs. deadlineMs: preset === 'codex' ? AGENT_TRUST_WRITE_DEADLINE_MS : SHORT_AGENT_TRUST_WRITE_DEADLINE_MS diff --git a/src/main/codex/codex-home-paths.test.ts b/src/main/codex/codex-home-paths.test.ts index 26053b8786c9..cc9f9374e901 100644 --- a/src/main/codex/codex-home-paths.test.ts +++ b/src/main/codex/codex-home-paths.test.ts @@ -67,6 +67,7 @@ vi.mock('node:os', async () => { }) import { + getLocalCodexTrustConfigFiles, syncCodexGlobalInstructionsIntoManagedHome, syncSystemCodexResourcesIntoManagedHome } from './codex-home-paths' @@ -135,6 +136,21 @@ afterEach(() => { vi.clearAllMocks() }) +describe('getLocalCodexTrustConfigFiles', () => { + it('puts the mirror first, then the config the launched Codex reads', () => { + const accountConfig = join(userDataDir, 'codex-accounts', 'a', 'home', 'config.toml') + expect(getLocalCodexTrustConfigFiles(accountConfig)).toEqual([ + join(getRuntimeCodexHomePath(), 'config.toml'), + accountConfig + ]) + }) + + it('names the mirror once when the launch runs on it', () => { + const mirrorConfig = join(getRuntimeCodexHomePath(), 'config.toml') + expect(getLocalCodexTrustConfigFiles(mirrorConfig)).toEqual([mirrorConfig]) + }) +}) + describe('syncSystemCodexResourcesIntoManagedHome', () => { it('uses ORCA_USER_DATA_PATH when Electron cannot be required', async () => { vi.resetModules() diff --git a/src/main/codex/codex-home-paths.ts b/src/main/codex/codex-home-paths.ts index e838d11fe6b2..3e3844356660 100644 --- a/src/main/codex/codex-home-paths.ts +++ b/src/main/codex/codex-home-paths.ts @@ -18,6 +18,7 @@ import { targetIsOwnedFallbackCopy } from './codex-managed-home-resource-copy-marker' import { observe, observeResolvedPathEntry } from './codex-path-observation' +import { normalizeRuntimePathForComparison } from '../../shared/cross-platform-path' const CODEX_GLOBAL_INSTRUCTIONS_ENTRY = 'AGENTS.md' @@ -47,12 +48,16 @@ export function getOrcaManagedCodexHomePath(): string { return managedHomePath } -/** Config files an Orca-launched local Codex reads trust from, in the hook installer's lock order. */ -export function getLocalCodexTrustConfigFiles(agentHome: string): string[] { - return [ - join(getOrcaManagedCodexHomePath(), 'config.toml'), - join(agentHome, '.codex', 'config.toml') - ] +/** + * Where a local Codex launch's trust goes, in the hook installer's lock order: Orca's shared + * mirror, then `launchedConfigFile`, the one config the launched Codex reads. + */ +export function getLocalCodexTrustConfigFiles(launchedConfigFile: string): string[] { + const mirrorConfigFile = join(getOrcaManagedCodexHomePath(), 'config.toml') + return normalizeRuntimePathForComparison(launchedConfigFile) === + normalizeRuntimePathForComparison(mirrorConfigFile) + ? [mirrorConfigFile] + : [mirrorConfigFile, launchedConfigFile] } export function getCodexSessionBackfillStateDirPath(): string { diff --git a/src/main/codex/codex-trust-config-concurrent-launch.test.ts b/src/main/codex/codex-trust-config-concurrent-launch.test.ts index 1a3e230a091a..f891f286c7f4 100644 --- a/src/main/codex/codex-trust-config-concurrent-launch.test.ts +++ b/src/main/codex/codex-trust-config-concurrent-launch.test.ts @@ -197,7 +197,10 @@ describe('two Codex pane launches against one config.toml', () => { await tick() await tick() // Why: the grant holds no lane across its session, so a launch's write lands at once. - await markCodexProjectTrusted(workspace, getLocalCodexTrustConfigFiles(testState.fakeHomeDir)) + await markCodexProjectTrusted( + workspace, + getLocalCodexTrustConfigFiles(join(testState.fakeHomeDir, '.codex', 'config.toml')) + ) expect(readFileSync(tomlPath, 'utf-8')).toContain('trust_level = "trusted"') releaseSession() @@ -385,7 +388,7 @@ describe('reentrancy under concurrency', () => { runExclusivelyForCodexTrustConfig(systemToml, async () => { await markCodexProjectTrusted( workspace, - getLocalCodexTrustConfigFiles(testState.fakeHomeDir) + getLocalCodexTrustConfigFiles(join(testState.fakeHomeDir, '.codex', 'config.toml')) ) return grantManagedCodexHookTrust(buildPlan(entries)) }) diff --git a/src/main/execution-host-workspace-trust.test.ts b/src/main/execution-host-workspace-trust.test.ts index a9603190923b..3987a24ef15c 100644 --- a/src/main/execution-host-workspace-trust.test.ts +++ b/src/main/execution-host-workspace-trust.test.ts @@ -24,6 +24,7 @@ vi.mock('node:os', async (importOriginal) => { import { AGENT_TRUST_INHERITS_FROM_A_HOME, applyWorkspaceTrustOnThisHost, + launchedCodexConfigFile, type WorkspaceTrustHost } from './execution-host-workspace-trust' @@ -199,3 +200,22 @@ describe('applyWorkspaceTrustOnThisHost', () => { warn.mockRestore() }) }) + +describe('launchedCodexConfigFile', () => { + it("is the config.toml in the launch's CODEX_HOME", () => { + expect(launchedCodexConfigFile(join(root, 'account'), state.home)).toBe( + join(root, 'account', 'config.toml') + ) + }) + + it.each([ + ['unset', undefined], + ['null', null], + // Why: Codex treats an empty CODEX_HOME as unset. + ['empty', ''] + ])('falls back to .codex under the home when CODEX_HOME is %s', (_label, codexHome) => { + expect(launchedCodexConfigFile(codexHome, state.home)).toBe( + join(state.home, '.codex', 'config.toml') + ) + }) +}) diff --git a/src/main/execution-host-workspace-trust.ts b/src/main/execution-host-workspace-trust.ts index acb84ea7ef1d..2555f75bdbd3 100644 --- a/src/main/execution-host-workspace-trust.ts +++ b/src/main/execution-host-workspace-trust.ts @@ -1,6 +1,6 @@ import { realpathSync } from 'node:fs' import { homedir } from 'node:os' -import { resolve } from 'node:path' +import { join, resolve } from 'node:path' import { markQoderWorkspaceTrusted } from './qoder/workspace-trust' import { type AgentTrustPreset, @@ -27,7 +27,7 @@ export type WorkspaceTrustHost = { agentHome: string /** The config Claude reads on this host, or null when this host cannot tell. */ claudeConfig: () => ClaudeTrustConfigTarget | null - /** Every config.toml the launched Codex may read, in the hook installer's lock order. */ + /** Every config.toml Codex trust is written to, in the hook installer's lock order. */ codexConfigFiles: () => readonly string[] deadlineMs: number } @@ -39,6 +39,14 @@ export function launchedAgentHome( return (process.platform === 'win32' ? launchEnv?.USERPROFILE : launchEnv?.HOME) || homedir() } +/** The config.toml a Codex launch reads: its CODEX_HOME, else `.codex` under the home it resolves `~` to. */ +export function launchedCodexConfigFile( + codexHome: string | null | undefined, + home: string +): string { + return join(codexHome || join(home, '.codex'), 'config.toml') +} + /** * Whether trust on a home or a disk root would also trust the folders below it, per each agent's * own lookup; such trust is never pre-written. Claude walks up parent folders (to the repo root, diff --git a/src/main/ipc/pty/ipc/spawn-options.ts b/src/main/ipc/pty/ipc/spawn-options.ts index 8adec688a678..4e3a612b3391 100644 --- a/src/main/ipc/pty/ipc/spawn-options.ts +++ b/src/main/ipc/pty/ipc/spawn-options.ts @@ -132,6 +132,7 @@ export async function buildPtyIpcSpawnOptions( claudeAuth: ctx.claudeAuth, wslDistro: ctx.expectedWslDistro, connectionId: args.connectionId ?? null, + codexHome: ctx.selectedCodexHomePath, spawnOptions: ctx.spawnOptions }) if (trustWrite) { diff --git a/src/main/ipc/pty/runtime/spawn-options.ts b/src/main/ipc/pty/runtime/spawn-options.ts index 26c3856b45ee..3f6fccfcb809 100644 --- a/src/main/ipc/pty/runtime/spawn-options.ts +++ b/src/main/ipc/pty/runtime/spawn-options.ts @@ -155,6 +155,7 @@ export async function buildRuntimePtySpawnOptions( claudeAuth: ctx.claudeAuth, wslDistro: ctx.expectedWslDistro, connectionId: args.connectionId ?? null, + codexHome: ctx.selectedCodexHomePath, spawnOptions: ctx.spawnOptions }) if (trustWrite) { diff --git a/src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts b/src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts index 3a8096c53096..7ffd53ad2420 100644 --- a/src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts +++ b/src/main/ipc/pty/spawn-options-agent-workspace-trust.test.ts @@ -24,6 +24,7 @@ type BuildInput = { restored?: boolean claudeAuth?: ClaudeRuntimeAuthPreparation | null wslDistro?: string | null + codexHome?: string | null } const RESTORED_PANE: AdoptStablePaneResult = { @@ -68,7 +69,13 @@ function makeDeps(): PtySpawnIpcDeps & PtyRuntimeControllerDeps { function seed( ctx: Pick< PtyIpcSpawnState, - 'env' | 'cwd' | 'launchCommand' | 'claudeAuth' | 'expectedWslDistro' | 'preAdoptedStablePane' + | 'env' + | 'cwd' + | 'launchCommand' + | 'claudeAuth' + | 'expectedWslDistro' + | 'preAdoptedStablePane' + | 'selectedCodexHomePath' >, input: BuildInput ): void { @@ -78,6 +85,7 @@ function seed( ctx.claudeAuth = input.claudeAuth ?? null ctx.expectedWslDistro = input.wslDistro ?? null ctx.preAdoptedStablePane = input.restored ? RESTORED_PANE : null + ctx.selectedCodexHomePath = input.codexHome ?? null } async function build(route: 'renderer' | 'runtime', input: BuildInput) { @@ -113,13 +121,16 @@ describe.each(['renderer', 'runtime'] as const)('%s spawn builder agent trust', await build(route, { launchAgent: 'codex', command: 'codex', - wslDistro: 'Ubuntu' + wslDistro: 'Ubuntu', + codexHome: '/orca/accounts/a/home' }) expect(applyAgentWorkspaceTrust).toHaveBeenCalledWith('codex', '/repo/wt', { env: expect.objectContaining({ CLAUDE_CONFIG_DIR: '/cfg' }), claudeAuth: null, wslDistro: 'Ubuntu', - connectionId: null + connectionId: null, + // Why: the provider sets this as CODEX_HOME only after the builder, so trust must take it here. + codexHome: '/orca/accounts/a/home' }) }) diff --git a/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts b/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts index cb360070191e..dc9a06cf2263 100644 --- a/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts +++ b/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts @@ -111,17 +111,12 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR }) ) } - return this.resolveStructuredAgentSessionIntent(input, async ({ launchEnv }) => { - await applyStructuredCodexWorkspaceTrust({ - workspacePath: (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path, - launchEnv, - settings: this.requireStore().getSettings() - }) - return resolveStructuredCodexAccountHomePath({ + return this.resolveStructuredAgentSessionIntent(input, async ({ launchEnv }) => + resolveStructuredCodexAccountHomePath({ launchEnv, resolveLaunchHome: this.prepareCodexStructuredLaunchFn }) - }) + ) } /** @@ -210,6 +205,15 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR selectedAccountHomePath }) : null + const accountHomePath = adoption ? adoption.accountHomePath : selectedAccountHomePath + if (input.agent === 'codex') { + // Why after adoption: trust goes into the home this chat's Codex runs on, which a resume repins. + await applyStructuredCodexWorkspaceTrust({ + workspacePath: (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path, + accountHomePath, + settings + }) + } return { envelope: { sessionId: input.envelope.sessionId, @@ -220,10 +224,7 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR location, provider: input.agent, agent: input.agent, - accountHome: agentSessionAccountHome( - input.agent, - adoption ? adoption.accountHomePath : selectedAccountHomePath - ), + accountHome: agentSessionAccountHome(input.agent, accountHomePath), ...(options ? { options } : {}), ...(input.resumeFrom && adoption ? { diff --git a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts index edff20ee99cb..574e9fae0c7f 100644 --- a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts +++ b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts @@ -2,11 +2,21 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' const applyAgentWorkspaceTrust = vi.hoisted(() => vi.fn(async () => ({}))) vi.mock('../agent-workspace-trust', () => ({ applyAgentWorkspaceTrust })) +const adoption = vi.hoisted(() => ({ + committedReplay: vi.fn<() => unknown>(() => null), + forCreate: vi.fn<() => Promise>(async () => null) +})) +vi.mock('./structured-agent-session-create-adoption', () => ({ + resolveCommittedStructuredAgentSessionAdoptionIntent: adoption.committedReplay, + resolveStructuredAgentSessionAdoptionForCreate: adoption.forCreate +})) import { OrcaRuntimeService } from './orca-runtime' beforeEach(() => { applyAgentWorkspaceTrust.mockClear() + adoption.committedReplay.mockReset().mockReturnValue(null) + adoption.forCreate.mockReset().mockResolvedValue(null) }) function createCodexIntentRuntime(settings: Record) { @@ -29,32 +39,61 @@ function createCodexIntentRuntime(settings: Record) { })), resolveRuntimeFileTarget: vi.fn(async () => ({ worktree: { path: '/repos/workspace-1' } })) }) - const createIntent = () => + const createIntent = (resumeFrom?: { providerSessionId: string }) => runtime.resolveStructuredAgentSessionCreateIntent({ envelope: { sessionId: 'session-1', clientOperationId: 'operation-1' }, worktree: 'id:workspace-1', - agent: 'codex' + agent: 'codex', + ...(resumeFrom ? { resumeFrom, callerKey: 'caller-1' } : {}) }) return { prepareCodexStructuredLaunch, createIntent } } describe('structured Codex folder trust', () => { - it('pre-trusts the chat folder before launch preparation, as a Codex terminal launch does', async () => { + it('pre-trusts the chat folder in the account home launch preparation picks', async () => { const { prepareCodexStructuredLaunch, createIntent } = createCodexIntentRuntime({}) await createIntent() expect(applyAgentWorkspaceTrust).toHaveBeenCalledWith('codex', '/repos/workspace-1', { - env: expect.any(Object), + env: undefined, claudeAuth: null, wslDistro: null, - connectionId: null + connectionId: null, + codexHome: '/accounts/selected/home' }) - expect(applyAgentWorkspaceTrust.mock.invocationCallOrder[0]).toBeLessThan( - prepareCodexStructuredLaunch.mock.invocationCallOrder[0] + expect(prepareCodexStructuredLaunch.mock.invocationCallOrder[0]).toBeLessThan( + applyAgentWorkspaceTrust.mock.invocationCallOrder[0] ) }) + it('pre-trusts a resumed chat in the home its conversation lives in', async () => { + adoption.forCreate.mockResolvedValue({ + accountHomePath: '/accounts/original/home', + transcriptPath: null + }) + const { createIntent } = createCodexIntentRuntime({}) + + const intent = await createIntent({ providerSessionId: 'thread-1' }) + + expect(intent.accountHome).toEqual({ variable: 'CODEX_HOME', path: '/accounts/original/home' }) + expect(applyAgentWorkspaceTrust).toHaveBeenCalledTimes(1) + expect(applyAgentWorkspaceTrust).toHaveBeenCalledWith( + 'codex', + '/repos/workspace-1', + expect.objectContaining({ codexHome: '/accounts/original/home' }) + ) + }) + + it('writes nothing for a replay of a create that already committed', async () => { + const replay = { replayed: true } + adoption.committedReplay.mockReturnValue(replay) + const { createIntent } = createCodexIntentRuntime({}) + + await expect(createIntent({ providerSessionId: 'thread-1' })).resolves.toBe(replay) + expect(applyAgentWorkspaceTrust).not.toHaveBeenCalled() + }) + it('writes nothing with the setting off, and still prepares the launch', async () => { const { prepareCodexStructuredLaunch, createIntent } = createCodexIntentRuntime({ agentWorkspaceTrustEnabled: false diff --git a/src/main/startup/codex-launch-during-real-home-grant.test.ts b/src/main/startup/codex-launch-during-real-home-grant.test.ts index ea10e8bd1d1b..f6b587d2411f 100644 --- a/src/main/startup/codex-launch-during-real-home-grant.test.ts +++ b/src/main/startup/codex-launch-during-real-home-grant.test.ts @@ -1,4 +1,4 @@ -import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import type * as Os from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -192,7 +192,8 @@ async function launch(workspacePath: string): Promise { env: undefined, claudeAuth: null, wslDistro: null, - connectionId: null + connectionId: null, + codexHome: home }) return home } @@ -285,11 +286,20 @@ describe('a Codex launch while the real-home approval hangs', () => { const managedHooks = readFileSync(join(getOrcaManagedCodexHomePath(), 'hooks.json'), 'utf-8') expect(managedHooks).toContain('codex-hook') - const systemConfig = readFileSync(join(homes.tmpHome, '.codex', 'config.toml'), 'utf-8') + // Why: trust goes where the launched Codex reads it, not into the ~/.codex it does not run on. + const managedConfig = readFileSync( + join(getOrcaManagedCodexHomePath(), 'config.toml'), + 'utf-8' + ) + const systemConfigPath = join(homes.tmpHome, '.codex', 'config.toml') + const systemConfig = existsSync(systemConfigPath) + ? readFileSync(systemConfigPath, 'utf-8') + : '' for (const workspace of workspaces) { - expect(systemConfig).toContain(workspace) + expect(managedConfig).toContain(workspace) + expect(systemConfig).not.toContain(workspace) } - expect(systemConfig.match(/trust_level = "trusted"/g)).toHaveLength(2) + expect(managedConfig.match(/trust_level = "trusted"/g)).toHaveLength(2) } finally { release() await realHomeInternals.settledVerdictForTesting() diff --git a/src/relay/agent-workspace-trust-spawn.ts b/src/relay/agent-workspace-trust-spawn.ts index b8af0dc7fe75..57a72de11b43 100644 --- a/src/relay/agent-workspace-trust-spawn.ts +++ b/src/relay/agent-workspace-trust-spawn.ts @@ -1,6 +1,5 @@ import { existsSync } from 'node:fs' import { homedir } from 'node:os' -import { join } from 'node:path' import { parseAgentWorkspaceTrustSpawnRequest } from '../shared/agent-workspace-trust-spawn-request' import { TUI_AGENT_CONFIG } from '../shared/tui-agent-config' import type { TuiAgent } from '../shared/tui-agent' @@ -8,7 +7,8 @@ import { resolveClaudeGlobalConfigFile } from '../main/claude/claude-folder-trus import { SHORT_AGENT_TRUST_WRITE_DEADLINE_MS } from '../main/agent-trust-write-deadline' import { applyWorkspaceTrustOnThisHost, - launchedAgentHome + launchedAgentHome, + launchedCodexConfigFile } from '../main/execution-host-workspace-trust' /** @@ -43,7 +43,7 @@ export async function applyRelayAgentWorkspaceTrust( }), keyStyle }), - codexConfigFiles: () => [join(spawnEnv.CODEX_HOME || join(homeDir, '.codex'), 'config.toml')], + codexConfigFiles: () => [launchedCodexConfigFile(spawnEnv.CODEX_HOME, homeDir)], // Why: every write here is on the relay's own disk, so each preset gets the local budget. deadlineMs: SHORT_AGENT_TRUST_WRITE_DEADLINE_MS } From 75f1cab73e2d4f762b4660ecb029b0c1b4d2f0e9 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 5 Oct 2026 23:53:55 -0700 Subject: [PATCH 2/5] fix(codex): default Codex trust home follows the launch env's home; structured trust runs from a pinned-home callback With no CODEX_HOME, Codex reads .codex under the home the launch env names, so the local writer now uses the same home the relay does instead of homedir(). The shared structured create resolver no longer names Codex: the Codex branch passes a callback that writes trust once the chat's account home is final. --- src/main/agent-workspace-trust.test.ts | 9 ++++-- src/main/agent-workspace-trust.ts | 5 ++- ...structured-agent-session-create-support.ts | 32 +++++++++++-------- 3 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/main/agent-workspace-trust.test.ts b/src/main/agent-workspace-trust.test.ts index 36ce6f530048..100e64cc7d57 100644 --- a/src/main/agent-workspace-trust.test.ts +++ b/src/main/agent-workspace-trust.test.ts @@ -99,9 +99,9 @@ describe('applyAgentWorkspaceTrust on this machine', () => { }) it.each([ + // Why: an explicit CODEX_HOME wins over whatever HOME the launch env names. ['the CODEX_HOME Orca gives the launch', '/orca/accounts/a/home', '/orca/accounts/a/home'], - // Why: with no CODEX_HOME Orca runs Codex on ~/.codex, whatever HOME the launch env names. - ['~/.codex without one', null, join(homedir(), '.codex')] + ['.codex under the launch env home without one', null, join('/home/agent', '.codex')] ])('writes Codex trust into %s', async (_label, codexHome, expectedHome) => { const context = { ...local, @@ -112,6 +112,11 @@ describe('applyAgentWorkspaceTrust on this machine', () => { expect(mocks.codexConfigFiles).toHaveBeenCalledWith(join(expectedHome, 'config.toml')) }) + it('falls back to the home folder when the launch env names no home', async () => { + await applyAgentWorkspaceTrust('codex', WORKSPACE, local) + expect(mocks.codexConfigFiles).toHaveBeenCalledWith(join(homedir(), '.codex', 'config.toml')) + }) + it('contains a rejected or throwing write so the launch proceeds', async () => { const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}) mocks.codex.mockRejectedValueOnce(new Error('write failed')) diff --git a/src/main/agent-workspace-trust.ts b/src/main/agent-workspace-trust.ts index 5e26eca055ad..9ea727adde3f 100644 --- a/src/main/agent-workspace-trust.ts +++ b/src/main/agent-workspace-trust.ts @@ -80,10 +80,9 @@ export async function applyAgentWorkspaceTrust( claudeAuth: context.claudeAuth, wslDistro: context.wslDistro }), - // Why homedir(), not the launch env's HOME: with no CODEX_HOME Orca runs Codex on the - // ~/.codex its real-home hook lane uses (getSystemCodexHomePath). + // Why agentHome: with no CODEX_HOME, Codex reads `.codex` under the home it resolves `~` to. codexConfigFiles: () => - getLocalCodexTrustConfigFiles(launchedCodexConfigFile(context.codexHome, homedir())), + getLocalCodexTrustConfigFiles(launchedCodexConfigFile(context.codexHome, agentHome)), // Why: Codex queues behind a config lane it shares with Orca's hook installs. deadlineMs: preset === 'codex' ? AGENT_TRUST_WRITE_DEADLINE_MS : SHORT_AGENT_TRUST_WRITE_DEADLINE_MS diff --git a/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts b/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts index dc9a06cf2263..de6adc3d1119 100644 --- a/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts +++ b/src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts @@ -111,11 +111,20 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR }) ) } - return this.resolveStructuredAgentSessionIntent(input, async ({ launchEnv }) => - resolveStructuredCodexAccountHomePath({ - launchEnv, - resolveLaunchHome: this.prepareCodexStructuredLaunchFn - }) + return this.resolveStructuredAgentSessionIntent( + input, + async ({ launchEnv }) => + resolveStructuredCodexAccountHomePath({ + launchEnv, + resolveLaunchHome: this.prepareCodexStructuredLaunchFn + }), + // Why after pinning: trust goes into the home this chat's Codex runs on, which a resume repins. + async (accountHomePath) => + applyStructuredCodexWorkspaceTrust({ + workspacePath: (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path, + accountHomePath, + settings: this.requireStore().getSettings() + }) ) } @@ -168,7 +177,9 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR workspaceId: string workspaceKind: 'folder' | 'git-worktree' } - }) => string | Promise + }) => string | Promise, + /** Runs once the account home this chat will run on is final, before the intent returns. */ + afterAccountHomePinned?: (accountHomePath: string) => Promise ): Promise { const support = await this.getStructuredAgentSessionCreateSupport(input.worktree, input.agent) if (!support.supported) { @@ -206,14 +217,7 @@ export class OrcaRuntimeWithGetStructuredAgentSessionCreateSupport extends OrcaR }) : null const accountHomePath = adoption ? adoption.accountHomePath : selectedAccountHomePath - if (input.agent === 'codex') { - // Why after adoption: trust goes into the home this chat's Codex runs on, which a resume repins. - await applyStructuredCodexWorkspaceTrust({ - workspacePath: (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path, - accountHomePath, - settings - }) - } + await afterAccountHomePinned?.(accountHomePath) return { envelope: { sessionId: input.envelope.sessionId, From 951950bf23ccd7504b29b9ecfbdf30021d23770d Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 00:19:47 -0700 Subject: [PATCH 3/5] docs(codex): describe the Codex trust targets as they are now --- src/main/agent-trust-presets.ts | 4 ++-- src/main/agent-workspace-trust-codex-home.test.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/agent-trust-presets.ts b/src/main/agent-trust-presets.ts index eac0591728bb..a6972f55c5fb 100644 --- a/src/main/agent-trust-presets.ts +++ b/src/main/agent-trust-presets.ts @@ -162,8 +162,8 @@ export function markAntigravityWorkspaceTrusted(workspacePath: string, home: str * Verified against codex-rs/tui/src/onboarding/trust_directory.rs and * codex-rs/core/src/config/config_tests.rs in the Codex CLI source. * - * `configFiles` names every config.toml the launched Codex may read, in the - * hook installer's lock order (an Orca-owned CODEX_HOME before the system one). + * `configFiles` is Orca's shared home config plus the one config.toml the launch + * reads, in the hook installer's lock order. */ export function markCodexProjectTrusted( workspacePath: string, diff --git a/src/main/agent-workspace-trust-codex-home.test.ts b/src/main/agent-workspace-trust-codex-home.test.ts index d35fec8a7760..c65755f2a226 100644 --- a/src/main/agent-workspace-trust-codex-home.test.ts +++ b/src/main/agent-workspace-trust-codex-home.test.ts @@ -44,7 +44,7 @@ function trustedProjects(configFile: string): string[] { function launchOn(codexHome: string | null): Promise { const context: AgentTrustLaunchContext = { - // Why HOME too: the env's home must not pull Codex trust back into a ~/.codex. + // Why HOME too: an explicit CODEX_HOME must leave this home's .codex alone; without one, trust goes there. env: { HOME: state.home, USERPROFILE: state.home }, claudeAuth: null, wslDistro: null, From 332081ae23910d26ac59e8c73da9ae3af26a81b0 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:28:28 -0700 Subject: [PATCH 4/5] test(native-chat): pin Codex trust ordering on the registration path The Codex registration's account-home resolver writes no trust for a launch or a read; its post-pin step writes into the home it is given, once, and Claude has none. --- ...ctured-agent-session-create-intent.test.ts | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts index 1886b6ac17aa..4dd5e473a513 100644 --- a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts +++ b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts @@ -12,6 +12,7 @@ vi.mock('./structured-agent-session-create-adoption', () => ({ })) import { OrcaRuntimeService } from './orca-runtime' +import { structuredAgentRuntimeRegistration } from './structured-agent-runtime-registrations' beforeEach(() => { applyAgentWorkspaceTrust.mockClear() @@ -62,6 +63,7 @@ describe('structured Codex folder trust', () => { connectionId: null, codexHome: '/accounts/selected/home' }) + expect(applyAgentWorkspaceTrust).toHaveBeenCalledTimes(1) expect(prepareCodexStructuredLaunch.mock.invocationCallOrder[0]).toBeLessThan( applyAgentWorkspaceTrust.mock.invocationCallOrder[0] ) @@ -106,6 +108,44 @@ describe('structured Codex folder trust', () => { }) }) +describe('the Codex runtime registration', () => { + const services = { + getClaudeConfigDirectory: vi.fn(() => null), + prepareCodexLaunchHome: vi.fn(() => '/accounts/selected/home'), + readCodexLaunchHome: vi.fn(() => '/accounts/selected/home'), + workspaceTrustSettings: () => ({ agentWorkspaceTrustEnabled: true }) + } + + it.each(['launch', 'read'] as const)( + 'resolves a %s account home without writing trust, which waits for the pinned home', + async (purpose) => { + const codex = structuredAgentRuntimeRegistration('codex')! + await expect( + codex.resolveAccountHomePath({ launchEnv: {}, location: null, purpose }, services) + ).resolves.toBe('/accounts/selected/home') + expect(applyAgentWorkspaceTrust).not.toHaveBeenCalled() + } + ) + + it('writes trust into the home its post-pin step is given', async () => { + const codex = structuredAgentRuntimeRegistration('codex')! + await codex.afterAccountHomePinned!( + { accountHomePath: '/accounts/pinned/home', workspacePath: async () => '/repos/workspace-1' }, + services + ) + expect(applyAgentWorkspaceTrust).toHaveBeenCalledTimes(1) + expect(applyAgentWorkspaceTrust).toHaveBeenCalledWith( + 'codex', + '/repos/workspace-1', + expect.objectContaining({ codexHome: '/accounts/pinned/home' }) + ) + }) + + it('gives Claude no post-pin step', () => { + expect(structuredAgentRuntimeRegistration('claude')!.afterAccountHomePinned).toBeUndefined() + }) +}) + describe('structured agent-session create intent', () => { it('pins the selected Codex launch home after normal launch preparation', async () => { const prepareCodexStructuredLaunch = vi.fn(() => '/accounts/selected/home') From 961bba223e55798ca6717c29399a388136daee09 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:59:59 -0700 Subject: [PATCH 5/5] test(native-chat): pin floating-chat Codex trust to the host launch directory --- ...ctured-agent-session-create-intent.test.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts index c2724ace695d..333ff764bb60 100644 --- a/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts +++ b/src/main/runtime/orca-runtime-structured-agent-session-create-intent.test.ts @@ -31,6 +31,7 @@ function createCodexIntentRuntime( workspacePath = '/repos/workspace-1' ) { const prepareCodexStructuredLaunch = vi.fn(() => '/accounts/selected/home') + const resolveRuntimeFileTarget = vi.fn(async () => ({ worktree: { path: workspacePath } })) const runtime = new OrcaRuntimeService( // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: create intent only reads getSettings from the store. { getSettings: () => ({ agentDefaultEnv: { codex: {} }, ...settings }) } as never, @@ -47,7 +48,7 @@ function createCodexIntentRuntime( workspaceId, workspaceKind: 'git-worktree' as const })), - resolveRuntimeFileTarget: vi.fn(async () => ({ worktree: { path: workspacePath } })) + resolveRuntimeFileTarget }) const createIntent = (resumeFrom?: { providerSessionId: string }) => runtime.resolveStructuredAgentSessionCreateIntent({ @@ -56,7 +57,7 @@ function createCodexIntentRuntime( agent: 'codex', ...(resumeFrom ? { resumeFrom, callerKey: 'caller-1' } : {}) }) - return { prepareCodexStructuredLaunch, createIntent } + return { prepareCodexStructuredLaunch, resolveRuntimeFileTarget, createIntent } } describe('structured Codex folder trust', () => { @@ -78,6 +79,31 @@ describe('structured Codex folder trust', () => { expect(attachFingerprintFields(intent)).not.toHaveProperty('hostLaunchDirectory') }) + it('trusts the floating host launch directory, and a worktree chat its resolved path', async () => { + const floating = createCodexIntentRuntime({}, FLOATING_TERMINAL_WORKTREE_ID, '/repos/later') + // Why distinct: a hook that re-resolved the selector instead of using the host directory would pass otherwise. + floating.resolveRuntimeFileTarget.mockResolvedValueOnce({ + worktree: { path: '/host/floating-folder' } + }) + + expect((await floating.createIntent()).hostLaunchDirectory).toBe('/host/floating-folder') + expect(applyAgentWorkspaceTrust).toHaveBeenCalledTimes(1) + expect(applyAgentWorkspaceTrust).toHaveBeenLastCalledWith( + 'codex', + '/host/floating-folder', + expect.objectContaining({ codexHome: '/accounts/selected/home' }) + ) + + const worktree = createCodexIntentRuntime({}, 'workspace-2', '/repos/workspace-2') + expect((await worktree.createIntent()).hostLaunchDirectory).toBeUndefined() + expect(applyAgentWorkspaceTrust).toHaveBeenCalledTimes(2) + expect(applyAgentWorkspaceTrust).toHaveBeenLastCalledWith( + 'codex', + '/repos/workspace-2', + expect.objectContaining({ codexHome: '/accounts/selected/home' }) + ) + }) + it('pre-trusts the chat folder in the account home launch preparation picks', async () => { const { prepareCodexStructuredLaunch, createIntent } = createCodexIntentRuntime({})