diff --git a/mobile/src/session/ai-vault-resume-preparation.test.ts b/mobile/src/session/ai-vault-resume-preparation.test.ts index a0eed5515c6..038133ebb62 100644 --- a/mobile/src/session/ai-vault-resume-preparation.test.ts +++ b/mobile/src/session/ai-vault-resume-preparation.test.ts @@ -70,7 +70,7 @@ describe('prepareMobileAiVaultSessionResume', () => { 'terminal.send', { terminal: 'pty-1', - text: `cd '/Users/ada/repo' && CODEX_HOME='${LEGACY_CODEX_HOME}' codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'legacy-1'`, + text: `cd '/Users/ada/repo' && CODEX_HOME='${LEGACY_CODEX_HOME}' codex 'resume' 'legacy-1'`, enter: true }, { timeoutMs: RESUME_RPC_TIMEOUT_MS } @@ -98,9 +98,7 @@ describe('prepareMobileAiVaultSessionResume', () => { { timeoutMs: RESUME_RPC_TIMEOUT_MS } ) expect(prepared).toEqual({ ...legacy, codexHome: null }) - expect(launch.command).toBe( - "cd '/Users/ada/repo' && codex '--dangerously-bypass-approvals-and-sandbox' 'resume' 'legacy-1'" - ) + expect(launch.command).toBe("cd '/Users/ada/repo' && codex 'resume' 'legacy-1'") expect(launch.envToDelete).toEqual(['CODEX_HOME', 'ORCA_CODEX_HOME']) }) diff --git a/src/renderer/src/lib/sleeping-agent-session-launch.ts b/src/renderer/src/lib/sleeping-agent-session-launch.ts index 67b9a2bbf8e..4456bb03685 100644 --- a/src/renderer/src/lib/sleeping-agent-session-launch.ts +++ b/src/renderer/src/lib/sleeping-agent-session-launch.ts @@ -93,7 +93,7 @@ export function launchSleepingAgentSession( : {}) }, launchAgent: record.agent, - ...(launchConfig ? { agentArgsOverride: launchConfig.agentArgs } : {}), + ...(launchConfig ? { agentArgsOverride: startupPlan.launchConfig.agentArgs } : {}), ...(startupPlan.startupCommandDelivery ? { startupCommandDelivery: startupPlan.startupCommandDelivery } : {}), diff --git a/src/shared/tui-agent-auto-permissions.test.ts b/src/shared/tui-agent-auto-permissions.test.ts index 92764ce796e..bae2c4259f1 100644 --- a/src/shared/tui-agent-auto-permissions.test.ts +++ b/src/shared/tui-agent-auto-permissions.test.ts @@ -50,14 +50,56 @@ describe('automatic terminal permissions', () => { shell, isRemote: true }) + if (agent === 'codex') { + // Codex restores the thread's own saved permissions on resume. + expect(resumed?.launchConfig.agentArgs).toBe('') + expect(tokenizeStartupCommand(resumed?.launchCommand ?? '', shell)).toMatchObject({ + ok: true, + tokens: ['codex', 'resume', 'saved-session'] + }) + continue + } expect(resumed?.launchConfig.agentArgs).toBe(agentArgs) expect(tokenizeStartupCommand(resumed?.launchCommand ?? '', shell)).toMatchObject({ ok: true, - tokens: expect.arrayContaining( - agent === 'codex' ? ['--approve-for-me'] : ['--permission-mode', 'auto'] - ) + tokens: expect.arrayContaining(['--permission-mode', 'auto']) }) } } ) }) + +describe('codex resume permissions', () => { + it.each(['posix', 'powershell', 'cmd'] as const)( + "drops Orca's full-access flag from a recorded %s launch but keeps the user's args", + (shell) => { + const platform = shell === 'posix' ? 'linux' : 'win32' + const startup = buildAgentStartupPlan({ + agent: 'codex', + agentArgs: "--dangerously-bypass-approvals-and-sandbox -m gpt-5.5 --add-dir 'my dir'", + prompt: '', + cmdOverrides: { codex: 'my-codex' }, + platform, + shell, + allowEmptyPromptLaunch: true + }) + const resume = (fork: boolean) => + buildAgentResumeStartupPlan({ + agent: 'codex', + ...startup?.launchConfig, + providerSession: { key: 'session_id', id: 'saved-session' }, + cmdOverrides: {}, + platform, + shell, + fork + }) + expect(tokenizeStartupCommand(resume(false)?.launchCommand ?? '', shell)).toMatchObject({ + ok: true, + tokens: ['my-codex', '-m', 'gpt-5.5', '--add-dir', 'my dir', 'resume', 'saved-session'] + }) + expect(resume(false)?.launchConfig.agentArgs).toBe("-m gpt-5.5 --add-dir 'my dir'") + // A fork starts a new thread, so it keeps the recorded launch's permission. + expect(resume(true)?.launchCommand).toContain('--dangerously-bypass-approvals-and-sandbox') + } + ) +}) diff --git a/src/shared/tui-agent-permission-lift.ts b/src/shared/tui-agent-permission-lift.ts index c54b5574ccd..e5419867bb7 100644 --- a/src/shared/tui-agent-permission-lift.ts +++ b/src/shared/tui-agent-permission-lift.ts @@ -12,6 +12,7 @@ import { type AgentPermissionChoice } from './tui-agent-permission-choice' import { permissionRecipeCut } from './agent-permission-args' +import { planAgentCliArgsSuffix, type AgentStartupShell } from './tui-agent-startup-shell' export { agentArgsSetPermission } from './agent-permission-args' import type { TuiAgent } from './tui-agent' @@ -33,6 +34,29 @@ export function liftAgentPermissionArgs( return { choice: null, args: original } } +/** A recorded launch without Orca's permission flag; recorded text it cannot rebuild stays whole. */ +export function dropRecordedAgentPermissionArgs( + agent: TuiAgent, + recorded: { agentArgs?: string | null; agentCommand?: string | null }, + shell: AgentStartupShell +): { agentArgs?: string | null; agentCommand?: string | null } { + const lifted = liftAgentPermissionArgs(agent, recorded.agentArgs) + if (!lifted.choice) { + return recorded + } + const command = recorded.agentCommand?.trim() + if (!command) { + return { ...recorded, agentArgs: lifted.args } + } + const before = planAgentCliArgsSuffix(recorded.agentArgs, shell) + const after = planAgentCliArgsSuffix(lifted.args, shell) + if (!before.ok || !after.ok || !command.endsWith(` ${before.suffix}`)) { + return recorded + } + const head = command.slice(0, -before.suffix.length).trimEnd() + return { agentArgs: lifted.args, agentCommand: after.suffix ? `${head} ${after.suffix}` : head } +} + /** Lift env bypass with execution-host name casing; unknown browser caches retain ambiguous spellings. */ export function liftAgentPermissionEnv( agent: TuiAgent, diff --git a/src/shared/tui-agent-resume-startup.ts b/src/shared/tui-agent-resume-startup.ts index 8d278010467..a260274998b 100644 --- a/src/shared/tui-agent-resume-startup.ts +++ b/src/shared/tui-agent-resume-startup.ts @@ -15,6 +15,7 @@ import { buildAgentResumeLaunchCommand } from './agent-resume-launch-command' import { buildAiVaultResumeShellCommand } from './ai-vault-resume-command' import { normalizeRuntimePathForComparison } from './cross-platform-path' import { parseWslUncPath } from './wsl-paths' +import { dropRecordedAgentPermissionArgs } from './tui-agent-permission-lift' export function buildAgentResumeStartupPlan(args: { agent: ResumableTuiAgent @@ -48,7 +49,13 @@ export function buildAgentResumeStartupPlan(args: { ? [resumeArgv[0], '-c', 'tui.resume_cwd=current', ...resumeArgv.slice(1)] : resumeArgv const shell = resolveStartupShell(args.platform, args.shell) - const resolvedAgentCommand = args.agentCommand?.trim() + // Why: Codex restores a resumed thread's saved permissions unless the launch passes one, so + // replaying Orca's launch-time flag would undo a change made inside the session (#10886). + const recorded = + args.agent === 'codex' && !args.fork + ? dropRecordedAgentPermissionArgs(args.agent, args, shell) + : args + const resolvedAgentCommand = recorded.agentCommand?.trim() const baseCommand = resolvedAgentCommand ? ({ ok: true, @@ -61,7 +68,7 @@ export function buildAgentResumeStartupPlan(args: { cmdOverrides: args.cmdOverrides, platform: args.platform, shell, - agentArgs: args.agentArgs, + agentArgs: recorded.agentArgs, sessionOptions: args.sessionOptions, sessionOptionsOverrideAgentArgs: args.sessionOptionsOverrideAgentArgs, isRemote: args.isRemote @@ -71,6 +78,7 @@ export function buildAgentResumeStartupPlan(args: { } const launchConfig = buildSleepingAgentLaunchConfig({ ...args, + agentArgs: recorded.agentArgs, agentCommand: baseCommand.commandWithoutSessionOptions }) const resumeCommand = buildAgentResumeLaunchCommand(args.agent, baseCommand.command, argv, shell)