fix(codex): resume a thread with the permissions it was last given, not Orca's launch flag (#33460)

* fix(codex): resume a thread with the permissions it was last given, not Orca's launch flag

Codex restores a resumed thread's saved approval/sandbox settings unless the
launch passes a permission flag. Orca replayed its launch-time recipe
(--dangerously-bypass-approvals-and-sandbox, --approve-for-me, or the Ask
flags) on every resume, so a session switched to "Approve for me" came back
as Full Access after a restart, and that override was saved to the thread.

Fixes #10886

Co-authored-by: mys <4546173+wowpotato@users.noreply.github.com>

* test(mobile): Codex AI Vault resume no longer replays Orca's permission flag

The mobile AI Vault resume builds its command through the shared resume plan,
so it now omits --dangerously-bypass-approvals-and-sandbox like desktop does.

Co-authored-by: mys <4546173+wowpotato@users.noreply.github.com>

---------

Co-authored-by: mys <4546173+wowpotato@users.noreply.github.com>
This commit is contained in:
Neil
2026-10-25 22:39:00 -07:00
committed by GitHub
co-authored by mys
parent 64bba690ad
commit de16d987fb
5 changed files with 82 additions and 10 deletions
@@ -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'])
})
@@ -93,7 +93,7 @@ export function launchSleepingAgentSession(
: {})
},
launchAgent: record.agent,
...(launchConfig ? { agentArgsOverride: launchConfig.agentArgs } : {}),
...(launchConfig ? { agentArgsOverride: startupPlan.launchConfig.agentArgs } : {}),
...(startupPlan.startupCommandDelivery
? { startupCommandDelivery: startupPlan.startupCommandDelivery }
: {}),
+45 -3
View File
@@ -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')
}
)
})
+24
View File
@@ -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,
+10 -2
View File
@@ -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)