From 97711f005d0f4d678330c7905bce81d67ed18e18 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 20:40:16 -0700 Subject: [PATCH 1/6] fix(native-chat): apply or refuse saved Codex Arguments instead of silently dropping them Settings -> Agents -> Arguments is shared by terminal tabs and Codex native chat, and is written for the interactive `codex`. The chat launch put the Arguments before `app-server`, where Codex parses and then ignores -m, --search, --oss and --local-provider, and Orca itself silently dropped -s, -a, --profile, --add-dir, -C, -i, --strict-config and permission -c keys. A chat could run a different model or provider than the terminal, or a looser sandbox than `-s read-only` asked for, with no sign of it. Every saved Argument now either reaches Codex the way a terminal applies it, or the chat refuses to start and names the argument: - Pass after `app-server`: -c/--config (every key), --enable/--disable, --strict-config. - Translate to the config Codex itself derives: -m/--model -> model, --search -> web_search="live", --approve-for-me -> its three overrides. - Under Manual, -s/-a and -c sandbox_mode/approval_policy become the thread policy instead of Manual's defaults (validated); Yolo unchanged. - Refuse by name: --profile, --oss, --local-provider, --remote*, --add-dir, -C, -i, --worktree, --dangerously-bypass-hook-trust, prompts (also after --), invalid sandbox/approval values. - Drop: the bypass flag (Agent Permissions reads it), --yolo, display and help/version flags. The model-list probe launches with the same translated Arguments, its own config last. An unclosed quote in Arguments is now a named refusal for both agents, and the Agent Permissions tooltip says what the toggle does. --- .../codex-read-only-app-server-args.ts | 8 +- .../codex/codex-model-catalog-probe.test.ts | 58 ++++ src/main/codex/codex-model-catalog-probe.ts | 16 +- ...codex-short-lived-app-server-spawn.test.ts | 8 +- .../codex-structured-launch-args.test.ts | 155 ++++++++--- .../codex/codex-structured-launch-args.ts | 255 ++++++++++++------ ...codex-structured-launch-resolution.test.ts | 53 ++-- .../codex-structured-launch-resolution.ts | 16 +- ...codex-structured-permission-policy.test.ts | 28 +- .../codex-structured-permission-policy.ts | 24 +- .../structured-agent-arguments-error.ts | 10 +- .../structured-agent-configured-args.test.ts | 31 ++- .../structured-agent-configured-args.ts | 8 +- .../runtime/orca-runtime-get-worktree-ps.ts | 4 +- ...-structured-launch-settings-wiring.test.ts | 10 + .../structured-agent-model-catalog-wiring.ts | 2 + .../structured-agent-session-runtime.ts | 6 +- ...nt-session-arguments-failure-words-text.ts | 55 ++++ .../agent-session-failure-words-text.ts | 27 +- .../src/components/settings/AgentsPane.tsx | 2 +- .../src/i18n/en-runtime-required.json | 2 + src/renderer/src/i18n/locales/en.json | 4 +- src/renderer/src/i18n/locales/es.json | 4 +- src/renderer/src/i18n/locales/fr.json | 4 +- src/renderer/src/i18n/locales/ja.json | 4 +- src/renderer/src/i18n/locales/ko.json | 4 +- src/renderer/src/i18n/locales/zh.json | 4 +- src/shared/agent-session-argument-problem.ts | 21 +- src/shared/agent-session-failure-copy.ts | 2 + src/shared/agent-session-failure-words.ts | 4 +- .../agent-session-refusal-notice.test.ts | 24 +- 31 files changed, 627 insertions(+), 226 deletions(-) create mode 100644 src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts diff --git a/src/main/codex-cli/codex-read-only-app-server-args.ts b/src/main/codex-cli/codex-read-only-app-server-args.ts index 049905c98c01..524157ef23a5 100644 --- a/src/main/codex-cli/codex-read-only-app-server-args.ts +++ b/src/main/codex-cli/codex-read-only-app-server-args.ts @@ -11,9 +11,13 @@ export const CODEX_READ_ONLY_APP_SERVER_ARGS = [ // Short-lived probes (rate limits, model catalog); plugin startup can launch marketplace // clones that outlive the probe teardown. Other read-only app-servers retain // normal plugin behavior. -export const CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS = [ +export const CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS = [ '-c', 'approval_policy=never', - ...CODEX_DISABLE_PLUGINS_ARGS, + ...CODEX_DISABLE_PLUGINS_ARGS +] as const + +export const CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS = [ + ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS, ...CODEX_READ_ONLY_APP_SERVER_TAIL ] as const diff --git a/src/main/codex/codex-model-catalog-probe.test.ts b/src/main/codex/codex-model-catalog-probe.test.ts index a3aeeebc4af0..1d86fdb5c5f2 100644 --- a/src/main/codex/codex-model-catalog-probe.test.ts +++ b/src/main/codex/codex-model-catalog-probe.test.ts @@ -38,6 +38,7 @@ describe('codex model catalog probe', () => { const probe = createCodexModelCatalogProbe({ resolveEnvironment, resolveCommand, + resolveLaunchArgs: () => [], runSession: async (invocation, body) => { invocations.push(invocation) return body({ @@ -67,6 +68,60 @@ describe('codex model catalog probe', () => { } ) + // The picker lists what a chat would run: the same saved Arguments, with the probe's own config + // after them, since Codex applies `-c` in order. + it('lists under the saved Arguments a chat launches with', async () => { + const invocations: CodexAppServerInvocation[] = [] + const probe = createCodexModelCatalogProbe({ + resolveEnvironment: async () => ({ PATH: '/bin' }), + resolveCommand: () => '/bin/codex', + resolveLaunchArgs: () => [ + '--dangerously-bypass-approvals-and-sandbox', + '-c', + 'model_provider=azure', + '-c', + 'features.plugins=true', + '-m', + 'gpt-azure' + ], + runSession: async (invocation, body) => { + invocations.push(invocation) + return body({ + request: async () => ({ data: [MODEL_ROW], nextCursor: null }), + notify: () => {} + }) + } + }) + await probe('/homes/a') + expect(invocations[0]!.args).toEqual([ + 'app-server', + '-c', + 'model_provider=azure', + '-c', + 'features.plugins=true', + '-c', + 'model="gpt-azure"', + '-c', + 'approval_policy=never', + '-c', + 'features.plugins=false' + ]) + }) + + it('lists nothing when the saved Arguments refuse a chat', async () => { + const runSession = vi.fn() + const probe = createCodexModelCatalogProbe({ + resolveEnvironment: async () => ({ PATH: '/bin' }), + resolveCommand: () => '/bin/codex', + resolveLaunchArgs: () => ['--profile', 'work'], + runSession + }) + await expect(probe('/homes/a')).rejects.toMatchObject({ + argumentProblem: { agent: 'Codex', option: '--profile', problem: 'unsupportedOption' } + }) + expect(runSession).not.toHaveBeenCalled() + }) + it('keeps the listing when config/read never answers', async () => { const server = String.raw` const readline = require('node:readline') @@ -82,6 +137,7 @@ describe('codex model catalog probe', () => { const probe = createCodexModelCatalogProbe({ resolveEnvironment: async () => ({ PATH: '/bin' }), resolveCommand: () => '/bin/codex', + resolveLaunchArgs: () => [], // Real transport against a fake server; a session deadline shorter than the // production one keeps the test fast while still outliving config/read's bound. runSession: (invocation, body) => @@ -106,6 +162,7 @@ describe('codex model catalog probe', () => { const probe = createCodexModelCatalogProbe({ resolveEnvironment: async () => ({ PATH: '/bin' }), resolveCommand: () => '/bin/codex', + resolveLaunchArgs: () => [], runSession: async (_invocation, body) => body({ request: async () => ({ data: [], nextCursor: null }), notify: () => {} }) }) @@ -124,6 +181,7 @@ describe('codex model catalog probe', () => { { agentCmdOverrides: { codex: command } }, options ), + resolveLaunchArgs: () => [], runSession }) await expect(probe('/homes/a')).rejects.toMatchObject({ reason: 'agentCommandNotRunnable' }) diff --git a/src/main/codex/codex-model-catalog-probe.ts b/src/main/codex/codex-model-catalog-probe.ts index 66def74598c2..6291d8f3fc23 100644 --- a/src/main/codex/codex-model-catalog-probe.ts +++ b/src/main/codex/codex-model-catalog-probe.ts @@ -1,4 +1,5 @@ -import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args' +import { CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS } from '../codex-cli/codex-read-only-app-server-args' +import { codexStructuredLaunchArgs } from './codex-structured-launch-args' import { runCodexAppServerSession } from './codex-app-server-session' import { fetchCodexModelCatalogListing } from './codex-structured-model-catalog' import { @@ -16,7 +17,7 @@ const CODEX_MODEL_CATALOG_PROBE_TIMEOUT_MS = 15_000 export type CodexModelCatalogProbeDeps = Pick< CodexStructuredLaunchResolverDeps, - 'resolveCommand' | 'resolveEnvironment' + 'resolveCommand' | 'resolveEnvironment' | 'resolveLaunchArgs' > & { /** Test seam; production runs the shared short-lived app-server session. */ runSession?: typeof runCodexAppServerSession @@ -33,21 +34,24 @@ function definedEnv(env: NodeJS.ProcessEnv | undefined): Record } /** - * Lists models without a live session: one short-lived read-only app-server + * Lists models without a live session: one short-lived app-server * under the given account home, spawned through the SAME invocation resolver - * a structured session launch uses — a probe that resolved a different binary - * or env could list models the user's sessions cannot see, under their key. + * and saved Arguments a structured session launch uses — a probe that resolved + * a different binary, env or config could list models the user's sessions + * cannot see, under their key. */ export function createCodexModelCatalogProbe( deps: CodexModelCatalogProbeDeps ): AgentModelCatalogProbe { return async (accountHomePath: string): Promise => { const { command, environment } = await resolveCodexStructuredInvocation(deps) + const { args } = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) const run = deps.runSession ?? runCodexAppServerSession const listing = await run( { command, - args: [...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS], + // Codex applies `-c` in order, so the probe's own config comes last and wins. + args: ['app-server', ...args, ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS], cliPath: command, env: { ...definedEnv(environment), CODEX_HOME: accountHomePath }, timeoutMs: CODEX_MODEL_CATALOG_PROBE_TIMEOUT_MS diff --git a/src/main/codex/codex-short-lived-app-server-spawn.test.ts b/src/main/codex/codex-short-lived-app-server-spawn.test.ts index fdd3733225d4..d3dfe7149744 100644 --- a/src/main/codex/codex-short-lived-app-server-spawn.test.ts +++ b/src/main/codex/codex-short-lived-app-server-spawn.test.ts @@ -1,5 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import { CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS } from '../codex-cli/codex-read-only-app-server-args' +import { + CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS, + CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS +} from '../codex-cli/codex-read-only-app-server-args' import type { CodexAppServerInvocation } from './codex-app-server-session' const NPM_CODEX_SHIM = 'C:\\Users\\alice\\AppData\\Roaming\\npm\\codex.cmd' @@ -37,6 +40,7 @@ describe('short-lived Codex app-server invocations on Windows', () => { const probe = createCodexModelCatalogProbe({ resolveEnvironment: async () => ({ PATH: 'C:\\bin' }), resolveCommand: () => NPM_CODEX_SHIM, + resolveLaunchArgs: () => [], runSession: async (invocation, body) => { invocations.push(invocation) return body({ @@ -58,6 +62,6 @@ describe('short-lived Codex app-server invocations on Windows', () => { }) await probe('C:\\homes\\a') expect(invocations[0]?.command).toBe(NPM_CODEX_SHIM) - expect(invocations[0]?.args).toEqual([...CODEX_SHORT_LIVED_PROBE_APP_SERVER_ARGS]) + expect(invocations[0]?.args).toEqual(['app-server', ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS]) }) }) diff --git a/src/main/codex/codex-structured-launch-args.test.ts b/src/main/codex/codex-structured-launch-args.test.ts index 0b156acdd703..5bbb3a74bb74 100644 --- a/src/main/codex/codex-structured-launch-args.test.ts +++ b/src/main/codex/codex-structured-launch-args.test.ts @@ -3,77 +3,138 @@ import { codexStructuredLaunchArgs } from './codex-structured-launch-args' import { StructuredAgentArgumentsError } from '../native-chat/structured-agent-arguments-error' describe('codexStructuredLaunchArgs', () => { - it('preserves root config and feature option order', () => { + it('passes config, feature and strict-config options in order', () => { expect( codexStructuredLaunchArgs([ '-c', 'model_reasoning_effort=high', '--enable', 'unified_exec', - '--config=web_search="live"', + '--config=web_search="cached"', '--disable=some_feature', - '-m', - 'gpt-5.6-sol' + '-c=model_provider=azure', + '--strict-config' ]) - ).toEqual([ + ).toEqual({ + args: [ + '-c', + 'model_reasoning_effort=high', + '--enable', + 'unified_exec', + '--config', + 'web_search="cached"', + '--disable', + 'some_feature', + '-c', + 'model_provider=azure', + '--strict-config' + ], + permissions: {} + }) + }) + + // app-server ignores `-m` and `--search`; Codex's own equivalents are config it applies after `-c`. + it('turns -m and --search into the config Codex derives from them, after the user config', () => { + expect( + codexStructuredLaunchArgs(['-m', 'gpt-5.6 "sol"', '--search', '-c', 'model="other"']).args + ).toEqual(['-c', 'model="other"', '-c', 'model="gpt-5.6 \\"sol\\""', '-c', 'web_search="live"']) + expect(codexStructuredLaunchArgs(['--model=gpt-5.6-sol']).args).toEqual([ '-c', - 'model_reasoning_effort=high', - '--enable', - 'unified_exec', - '--config=web_search="live"', - '--disable=some_feature', - '-m', - 'gpt-5.6-sol' + 'model="gpt-5.6-sol"' ]) }) - it('drops permission options and config overrides owned by Orca', () => { + it('passes sandbox details and the reviewer, which Orca does not own', () => { expect( codexStructuredLaunchArgs([ - '--dangerously-bypass-approvals-and-sandbox', - '-a', - 'never', - '--sandbox=danger-full-access', - '--approve-for-me', - '-c', - 'approval_policy="never"', - '--config=sandbox_mode="danger-full-access"', '-c', - 'sandbox_workspace_write.writable_roots=["/tmp"]', + 'sandbox_workspace_write.network_access=true', '-c', - '"sandbox_workspace_write".writable_roots=["/tmp"]', + 'approvals_reviewer="auto_review"' + ]) + ).toEqual({ + args: [ '-c', - '\'approval_policy\'="never"', + 'sandbox_workspace_write.network_access=true', '-c', - 'model_reasoning_effort=high' - ]) - ).toEqual(['-c', 'model_reasoning_effort=high']) + 'approvals_reviewer="auto_review"' + ], + permissions: {} + }) }) - it('drops a profile and TUI-only path options without leaving their values as prompts', () => { + it('reads the sandbox and approval the Arguments state, flags over config', () => { + expect(codexStructuredLaunchArgs(['-s', 'read-only', '-a', 'untrusted'])).toEqual({ + args: [], + permissions: { sandbox: 'read-only', approvalPolicy: 'untrusted' } + }) expect( codexStructuredLaunchArgs([ - '--profile', - 'review', - '-pother', - '--cd', - '/another/repo', - '--worktree', '-c', - 'model_reasoning_effort=high' - ]) - ).toEqual(['-c', 'model_reasoning_effort=high']) + 'sandbox_mode="danger-full-access"', + '--config', + "approval_policy='never'", + '-c', + 'sandbox_mode=read-only' + ]).permissions + ).toEqual({ sandbox: 'read-only', approvalPolicy: 'never' }) + expect( + codexStructuredLaunchArgs(['--sandbox=workspace-write', '-c', 'sandbox_mode=read-only']) + .permissions + ).toEqual({ sandbox: 'workspace-write' }) + }) + + it('applies --approve-for-me as the config Codex folds it into', () => { + expect(codexStructuredLaunchArgs(['--approve-for-me', '-c', 'approval_policy=never'])).toEqual({ + args: [ + '-c', + 'approval_policy=never', + '-c', + 'approvals_reviewer="auto_review"', + '-c', + 'approval_policy="on-request"', + '-c', + 'sandbox_mode="workspace-write"' + ], + permissions: { approvalPolicy: 'on-request', sandbox: 'workspace-write' } + }) }) - it('discards operands after the option terminator', () => { + it('drops the bypass flag and terminal-only display options', () => { expect( - codexStructuredLaunchArgs(['-c', 'model_reasoning_effort=high', '--', 'a prompt']) - ).toEqual(['-c', 'model_reasoning_effort=high']) + codexStructuredLaunchArgs([ + '--dangerously-bypass-approvals-and-sandbox', + '--yolo', + '--no-alt-screen', + '--no-daemon', + '-h', + '--version', + '--' + ]) + ).toEqual({ args: [], permissions: {} }) }) it.each([ { tokens: ['a private prompt'], option: 'prompt', problem: 'positionalPrompt' }, { tokens: ['app-server'], option: 'prompt', problem: 'positionalPrompt' }, + { tokens: ['--', 'a private prompt'], option: 'prompt', problem: 'positionalPrompt' }, + { tokens: ['--profile', 'private'], option: '--profile', problem: 'unsupportedOption' }, + { tokens: ['-pprivate'], option: '-p', problem: 'unsupportedOption' }, + { tokens: ['--oss'], option: '--oss', problem: 'unsupportedOption' }, + { + tokens: ['--local-provider', 'private'], + option: '--local-provider', + problem: 'unsupportedOption' + }, + { tokens: ['--add-dir', '/private'], option: '--add-dir', problem: 'unsupportedOption' }, + { tokens: ['-C', '/private'], option: '-C', problem: 'unsupportedOption' }, + { tokens: ['--image=private.png'], option: '--image', problem: 'unsupportedOption' }, + { tokens: ['--worktree'], option: '--worktree', problem: 'unsupportedOption' }, + { + tokens: ['--dangerously-bypass-hook-trust'], + option: '--dangerously-bypass-hook-trust', + problem: 'unsupportedOption' + }, { tokens: ['--remote', 'wss://private-host'], option: '--remote', @@ -86,8 +147,18 @@ describe('codexStructuredLaunchArgs', () => { problem: 'unsupportedOption' }, { tokens: ['--unknown-flag=secret'], option: '--unknown-flag', problem: 'unsupportedOption' }, - { tokens: ['--enable'], option: '--enable', problem: 'missingValue' } - ] as const)('rejects unsafe or incomplete arguments: %j', ({ tokens, option, problem }) => { + { tokens: ['--search=secret'], option: '--search', problem: 'unsupportedOption' }, + { tokens: ['--enable'], option: '--enable', problem: 'missingValue' }, + { tokens: ['-m', '--secret'], option: '-m', problem: 'missingValue' }, + { tokens: ['-s', 'private-mode'], option: '-s', problem: 'invalidValue' }, + { + tokens: ['--ask-for-approval=on-failure'], + option: '--ask-for-approval', + problem: 'invalidValue' + }, + { tokens: ['-c', 'approval_policy={ secret = true }'], option: '-c', problem: 'invalidValue' }, + { tokens: ['--config=sandbox_mode=secret'], option: '--config', problem: 'invalidValue' } + ] as const)('refuses what a chat cannot honor: %j', ({ tokens, option, problem }) => { const thrown = () => codexStructuredLaunchArgs(tokens) expect(thrown).toThrow(StructuredAgentArgumentsError) try { diff --git a/src/main/codex/codex-structured-launch-args.ts b/src/main/codex/codex-structured-launch-args.ts index 90b39fffec81..52d326347b25 100644 --- a/src/main/codex/codex-structured-launch-args.ts +++ b/src/main/codex/codex-structured-launch-args.ts @@ -1,115 +1,196 @@ -// Only root CLI options can precede `app-server`. A prompt or another subcommand -// would change the process Orca starts, and permission options belong to Orca. -import { parseTomlKeyPath } from './config-toml-key-path' +// Saved Arguments are written for the interactive `codex` a terminal runs; a chat runs +// `codex app-server`, which reads only config and its own few options. So each interactive option +// either passes to app-server, becomes the config Codex itself derives from it, is dropped because +// it only shapes a terminal, or refuses the start by name because a chat can't honor it. import { StructuredAgentArgumentsError } from '../native-chat/structured-agent-arguments-error' +import { + CODEX_APPROVAL_POLICIES, + CODEX_SANDBOX_MODES, + type CodexStructuredPermissionPolicy +} from './codex-structured-permission-policy' -const VALUE_OPTIONS = new Set([ - '-c', - '--config', - '--enable', - '--disable', - '-m', - '--model', - '--local-provider' -]) - -const DROPPED_VALUE_OPTIONS = new Set([ - '-a', - '--ask-for-approval', - '-s', - '--sandbox', - '-p', - '--profile', - '-C', - '--cd', - '--add-dir', - '-i', - '--image' -]) - -const DROPPED_SWITCHES = new Set([ - '--dangerously-bypass-approvals-and-sandbox', - '--yolo', - '--approve-for-me', - '--not-so-yolo', - '--dangerously-bypass-hook-trust', - '--worktree', - '--strict-config', - '--help', - '-h', - '--version', - '-V' -]) - -const PASSTHROUGH_SWITCHES = new Set(['--search', '--oss', '--no-alt-screen', '--no-daemon']) +export type CodexStructuredLaunchArgs = { + /** Options for after `app-server`. */ + args: string[] + /** The sandbox and approval policy the Arguments state explicitly. */ + permissions: Partial +} + +type LaunchPlan = { + args: string[] + /** Config the interactive CLI derives from its own options; it applies them after every `-c`. */ + derived: string[] + /** From `-c`, last one wins. */ + configured: Partial + /** From `-s` / `-a`, which outrank config. */ + flags: Partial +} + +type OptionRule = + | { takesValue: boolean; apply?: (plan: LaunchPlan, option: string, value: string) => void } + | 'refuse' + +const pass = (plan: LaunchPlan, option: string, value: string): void => { + plan.args.push(option, ...(value ? [value] : [])) +} + +const passConfig = (plan: LaunchPlan, option: string, value: string): void => { + readConfiguredPermission(plan.configured, option, value) + pass(plan, option, value) +} + +const derive = + (...overrides: string[]) => + (plan: LaunchPlan): void => { + plan.derived.push(...overrides) + } + +const deriveModel = (plan: LaunchPlan, _option: string, value: string): void => { + // A JSON string is a valid TOML basic string. + plan.derived.push(`model=${JSON.stringify(value)}`) +} + +const flagSandbox = (plan: LaunchPlan, option: string, value: string): void => { + plan.flags.sandbox = oneOf(CODEX_SANDBOX_MODES, value, option) +} + +const flagApproval = (plan: LaunchPlan, option: string, value: string): void => { + plan.flags.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, option) +} + +const VALUE = { takesValue: true } +const DROPPED_SWITCH = { takesValue: false } +const APPROVE_FOR_ME = { + takesValue: false, + apply: derive( + 'approvals_reviewer="auto_review"', + 'approval_policy="on-request"', + 'sandbox_mode="workspace-write"' + ) +} + +// Equivalents are the ones Codex applies itself: `--search` and `--approve-for-me` in +// codex-rs cli/src/main.rs and utils/cli/src/shared_options.rs, `-m` as `-c model=`. +const RULES: Record = { + '-c': { ...VALUE, apply: passConfig }, + '--config': { ...VALUE, apply: passConfig }, + '--enable': { ...VALUE, apply: pass }, + '--disable': { ...VALUE, apply: pass }, + '--strict-config': { takesValue: false, apply: pass }, + '-m': { ...VALUE, apply: deriveModel }, + '--model': { ...VALUE, apply: deriveModel }, + '--search': { takesValue: false, apply: derive('web_search="live"') }, + '-s': { ...VALUE, apply: flagSandbox }, + '--sandbox': { ...VALUE, apply: flagSandbox }, + '-a': { ...VALUE, apply: flagApproval }, + '--ask-for-approval': { ...VALUE, apply: flagApproval }, + '--approve-for-me': APPROVE_FOR_ME, + '--not-so-yolo': APPROVE_FOR_ME, + // Agent Permissions reads the bypass flag; the rest only shape a terminal. + '--dangerously-bypass-approvals-and-sandbox': DROPPED_SWITCH, + '--yolo': DROPPED_SWITCH, + '--no-alt-screen': DROPPED_SWITCH, + '--no-daemon': DROPPED_SWITCH, + '--help': DROPPED_SWITCH, + '-h': DROPPED_SWITCH, + '--version': DROPPED_SWITCH, + '-V': DROPPED_SWITCH, + // No app-server equivalent: a different config, provider, server, folder or first prompt. + '-p': 'refuse', + '--profile': 'refuse', + '--oss': 'refuse', + '--local-provider': 'refuse', + '--remote': 'refuse', + '--remote-auth-token-env': 'refuse', + '--add-dir': 'refuse', + '-C': 'refuse', + '--cd': 'refuse', + '-i': 'refuse', + '--image': 'refuse', + '--worktree': 'refuse', + '--dangerously-bypass-hook-trust': 'refuse' +} + +function oneOf(values: readonly T[], value: string, option: string): T { + const match = values.find((candidate) => candidate === value) + if (match === undefined) { + throw new StructuredAgentArgumentsError('Codex', option, 'invalidValue') + } + return match +} + +/** The policy a `-c approval_policy=` / `sandbox_mode=` sets, read the way Codex reads it. */ +function readConfiguredPermission( + configured: Partial, + option: string, + override: string +): void { + const separator = override.indexOf('=') + if (separator === -1) { + return + } + const key = override.slice(0, separator).trim() + // Codex takes a value that isn't TOML as its text with the quotes trimmed. + const value = override + .slice(separator + 1) + .trim() + .replace(/^["']+|["']+$/g, '') + if (key === 'approval_policy') { + configured.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, option) + } else if (key === 'sandbox_mode') { + configured.sandbox = oneOf(CODEX_SANDBOX_MODES, value, option) + } +} function optionName(token: string): string { if (token.startsWith('--')) { return token.split('=', 1)[0] } - if (/^-[acmspiC].+/.test(token)) { - return token.slice(0, 2) - } - return token + return /^-[acmspiC].+/.test(token) ? token.slice(0, 2) : token } +// Codex's parser accepts `-c=k=v` as well as `-ck=v`. function inlineValue(token: string, option: string): string | undefined { if (token === option) { return undefined } - return token.startsWith('--') ? token.slice(option.length + 1) : token.slice(2) + return token.startsWith('--') ? token.slice(option.length + 1) : token.slice(2).replace(/^=/, '') } -function isPermissionConfig(value: string): boolean { - const key = parseTomlKeyPath(value)?.segments[0] - return ( - key === 'approval_policy' || - key === 'sandbox_mode' || - key === 'approvals_reviewer' || - key === 'sandbox_workspace_write' - ) -} - -/** Turns saved terminal Arguments into root options for a structured app-server launch. */ -export function codexStructuredLaunchArgs(tokens: readonly string[]): string[] { - const args: string[] = [] +/** Turns saved terminal Arguments into a structured app-server launch. */ +export function codexStructuredLaunchArgs(tokens: readonly string[]): CodexStructuredLaunchArgs { + const plan: LaunchPlan = { args: [], derived: [], configured: {}, flags: {} } for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index] if (token === '--') { + // What follows is the terminal's first prompt. + if (index + 1 < tokens.length) { + throw new StructuredAgentArgumentsError('Codex', token, 'positionalPrompt') + } break } + if (!token.startsWith('-')) { + throw new StructuredAgentArgumentsError('Codex', token, 'positionalPrompt') + } const option = optionName(token) - if (option === '--remote' || option === '--remote-auth-token-env') { + const rule = RULES[option] + if (!rule || rule === 'refuse') { throw new StructuredAgentArgumentsError('Codex', option, 'unsupportedOption') } - if (DROPPED_SWITCHES.has(option)) { - continue - } - if (PASSTHROUGH_SWITCHES.has(option) && token === option) { - args.push(token) - continue - } - if (VALUE_OPTIONS.has(option) || DROPPED_VALUE_OPTIONS.has(option)) { - const value = inlineValue(token, option) ?? tokens[++index] - if (!value || value === '--' || (value.startsWith('-') && !token.includes('='))) { + let value = '' + if (rule.takesValue) { + const inline = inlineValue(token, option) + value = inline ?? tokens[++index] ?? '' + if (!value || value === '--' || (value.startsWith('-') && inline === undefined)) { throw new StructuredAgentArgumentsError('Codex', option, 'missingValue') } - if ( - DROPPED_VALUE_OPTIONS.has(option) || - (['-c', '--config'].includes(option) && isPermissionConfig(value)) - ) { - continue - } - args.push(token) - if (token === option) { - args.push(value) - } - continue - } - if (!token.startsWith('-')) { - throw new StructuredAgentArgumentsError('Codex', token, 'positionalPrompt') + } else if (token !== option) { + throw new StructuredAgentArgumentsError('Codex', option, 'unsupportedOption') } - throw new StructuredAgentArgumentsError('Codex', token, 'unsupportedOption') + rule.apply?.(plan, option, value) + } + for (const override of plan.derived) { + passConfig(plan, '-c', override) } - return args + return { args: plan.args, permissions: { ...plan.configured, ...plan.flags } } } diff --git a/src/main/codex/codex-structured-launch-resolution.test.ts b/src/main/codex/codex-structured-launch-resolution.test.ts index b47599113281..58a4b7ca0326 100644 --- a/src/main/codex/codex-structured-launch-resolution.test.ts +++ b/src/main/codex/codex-structured-launch-resolution.test.ts @@ -63,7 +63,8 @@ function resolverFor( resolveCommand: () => '/usr/local/bin/codex', resolveRollout, resolveLaunchArgs: resolveLaunchArgs ?? (() => value?.launchArgs ?? []), - resolvePermissionPolicy: () => codexStructuredPermissionPolicyForSettings({ agentDefaultArgs }) + resolvePermissionPolicy: (requested) => + codexStructuredPermissionPolicyForSettings({ agentDefaultArgs }, requested) }) } @@ -96,9 +97,9 @@ describe('codex structured launch resolution', () => { await expect(resolve({ identity: IDENTITY })).rejects.toThrow(/Arguments/) args = ['--enable', 'unified_exec'] expect((await resolve({ identity: IDENTITY })).args).toEqual([ + 'app-server', '--enable', - 'unified_exec', - 'app-server' + 'unified_exec' ]) args = [] expect((await resolve({ identity: IDENTITY })).args).toEqual(['app-server']) @@ -277,29 +278,47 @@ describe('codex structured launch resolution', () => { expect(launch.model).toBe('gpt-chosen') }) - it('uses saved arguments before app-server on a fresh launch', async () => { + it('uses saved arguments after app-server on a fresh launch', async () => { const launch = await resolverFor( record({ - launchArgs: [ - '--profile', - 'review', - '-c', - 'model_reasoning_effort=high', - '--model', - 'gpt-5.6-sol' - ] + launchArgs: ['-c', 'model_reasoning_effort=high', '--model', 'gpt-5.6-sol'] }) )({ identity: IDENTITY }) expect(launch.args).toEqual([ + 'app-server', '-c', 'model_reasoning_effort=high', - '--model', - 'gpt-5.6-sol', - 'app-server' + '-c', + 'model="gpt-5.6-sol"' ]) }) + // A terminal honors these, so a chat that sent Manual's workspace-write would run looser. + it('opens the thread under the sandbox and approval the Arguments state under Manual', async () => { + const launch = await resolverFor( + record({ launchArgs: ['-s', 'read-only', '-c', 'approval_policy=untrusted'] }) + )({ identity: IDENTITY }) + + expect(launch.args).toEqual(['app-server', '-c', 'approval_policy=untrusted']) + expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'untrusted', sandbox: 'read-only' }) + }) + + it('keeps Yolo when the Arguments also state a sandbox', async () => { + const launch = await resolverFor( + record(), + undefined, + undefined, + { codex: '--dangerously-bypass-approvals-and-sandbox' }, + () => ['--dangerously-bypass-approvals-and-sandbox', '-s', 'read-only'] + )({ identity: IDENTITY }) + + expect(launch.permissionPolicy).toEqual({ + approvalPolicy: 'never', + sandbox: 'danger-full-access' + }) + }) + it('pins resume to the rollout file that proved the durable thread', async () => { const resolveRollout = vi.fn(async () => '/home/work/.codex/sessions/rollout.jsonl') const launch = await resolverFor( @@ -317,11 +336,11 @@ describe('codex structured launch resolution', () => { expect(resolveRollout).toHaveBeenCalledWith('/home/work/.codex', 'thread-current') expect(launch.resumePath).toBe('/home/work/.codex/sessions/rollout.jsonl') expect(launch.args).toEqual([ + 'app-server', '--enable', 'unified_exec', '-c', - 'model_reasoning_effort=high', - 'app-server' + 'model_reasoning_effort=high' ]) }) diff --git a/src/main/codex/codex-structured-launch-resolution.ts b/src/main/codex/codex-structured-launch-resolution.ts index 183f765f5c86..1acb613d90b3 100644 --- a/src/main/codex/codex-structured-launch-resolution.ts +++ b/src/main/codex/codex-structured-launch-resolution.ts @@ -29,9 +29,12 @@ export type CodexStructuredLaunchResolverDeps = { /** Fresh shell/configured environment for this spawn; never written to the session record. */ resolveEnvironment?: () => Promise resolveRollout?: typeof resolvePinnedCodexRolloutProof - /** The user's Agent Permissions setting as thread policy, re-read per acquisition. - * States both postures outright — a resume inherits the last one for any field left absent. */ - resolvePermissionPolicy?: () => CodexStructuredPermissionPolicy + /** The user's Agent Permissions setting as thread policy, re-read per acquisition, given the + * policy the Arguments state. States both postures outright — a resume inherits the last one + * for any field left absent. */ + resolvePermissionPolicy?: ( + requested: Partial + ) => CodexStructuredPermissionPolicy } export type CodexStructuredInvocation = { @@ -84,8 +87,8 @@ export function createCodexStructuredLaunchResolver( throw new Error(`codex sessions pin ${pinned}, not ${accountHome.variable}`) } const { command, environment } = await resolveCodexStructuredInvocation(deps) - const args = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) - const permissionPolicy = deps.resolvePermissionPolicy?.() + const { args, permissions } = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) + const permissionPolicy = deps.resolvePermissionPolicy?.(permissions) const head = agentSessionProviderHandleChainHead(record.providerHandleChain) // A Codex record's chain holds only Codex handles; the attach admission refuses anything else. const resumeThreadId = head?.handle.nativeId ?? null @@ -93,7 +96,8 @@ export function createCodexStructuredLaunchResolver( const model = record.options?.model return { command, - args: [...args, 'app-server'], + // After the subcommand, so an option app-server doesn't take fails loudly, not silently. + args: ['app-server', ...args], cwd: await resolveAgentSessionLaunchDirectory(deps, record), codexHome: accountHome.path, ...(environment ? { env: { ...environment } as Record } : {}), diff --git a/src/main/codex/codex-structured-permission-policy.test.ts b/src/main/codex/codex-structured-permission-policy.test.ts index 67d08f3f109c..78389b8a3e4d 100644 --- a/src/main/codex/codex-structured-permission-policy.test.ts +++ b/src/main/codex/codex-structured-permission-policy.test.ts @@ -61,8 +61,32 @@ describe('codexStructuredPermissionPolicyForSettings', () => { } }) - // The passthrough that used to carry these to app-server is gone on purpose; only the - // permission posture is derived, and nothing else from the field reaches argv. + // A terminal running `codex -s read-only` is read-only; Manual's default would loosen it. + it('takes the sandbox and approval the Arguments state under Manual', () => { + expect( + codexStructuredPermissionPolicyForSettings( + { agentDefaultArgs: { codex: '-s read-only' } }, + { sandbox: 'read-only' } + ) + ).toEqual({ approvalPolicy: 'on-request', sandbox: 'read-only' }) + expect( + codexStructuredPermissionPolicyForSettings( + { agentDefaultArgs: { codex: '-a untrusted -s danger-full-access' } }, + { approvalPolicy: 'untrusted', sandbox: 'danger-full-access' } + ) + ).toEqual({ approvalPolicy: 'untrusted', sandbox: 'danger-full-access' }) + }) + + it('keeps Yolo whatever else the Arguments state', () => { + expect( + codexStructuredPermissionPolicyForSettings( + { agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox -s read-only' } }, + { sandbox: 'read-only' } + ) + ).toEqual(BYPASS) + }) + + // The posture alone comes out of the field here; the translator reads the stated policy. it('carries nothing but the permission posture out of the arguments field', () => { expect( codexStructuredPermissionPolicyForSettings({ diff --git a/src/main/codex/codex-structured-permission-policy.ts b/src/main/codex/codex-structured-permission-policy.ts index 3dbf5a07085c..3e13fa9016bf 100644 --- a/src/main/codex/codex-structured-permission-policy.ts +++ b/src/main/codex/codex-structured-permission-policy.ts @@ -1,9 +1,14 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import { resolvedTuiAgentArgsBypassPermissions } from '../../shared/tui-agent-launch-defaults' -export type CodexStructuredPermissionPolicy = - | { approvalPolicy: 'never'; sandbox: 'danger-full-access' } - | { approvalPolicy: 'on-request'; sandbox: 'workspace-write' } +// The values app-server's v2 thread params accept (`AskForApproval` / `SandboxMode`, kebab-case). +export const CODEX_APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'] as const +export const CODEX_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'] as const + +export type CodexStructuredPermissionPolicy = { + approvalPolicy: (typeof CODEX_APPROVAL_POLICIES)[number] + sandbox: (typeof CODEX_SANDBOX_MODES)[number] +} /** Yolo: no approval prompts, no sandbox. */ const BYPASS_POLICY = { approvalPolicy: 'never', sandbox: 'danger-full-access' } as const @@ -29,10 +34,10 @@ const MANUAL_POLICY = { approvalPolicy: 'on-request', sandbox: 'workspace-write' /** * The Agent Permissions setting as app-server thread policy. * - * Derived per acquisition from the resolved launch arguments, never from the free-text Arguments - * field: app-server takes a narrower option set than the interactive CLI and the two are versioned - * apart, so the only thing read out of that field is the posture the toggle stores in it. An - * untouched profile resolves to the default Orca ships, which is the bypass flag. + * The posture is the one the toggle stores in the Arguments field; an untouched profile resolves + * to the default Orca ships, which is the bypass flag. Under Manual, a sandbox or approval policy + * the Arguments state explicitly (`requested`) replaces that field's default, as it does in a + * terminal; Yolo ignores it. * * Always a policy, never `undefined`: both postures have to be said out loud, because the one * that goes unsaid is the one a resume silently inherits from the other. @@ -41,9 +46,10 @@ export function codexStructuredPermissionPolicyForSettings( settings: | Partial> | null - | undefined + | undefined, + requested: Partial = {} ): CodexStructuredPermissionPolicy { return resolvedTuiAgentArgsBypassPermissions('codex', settings, process.platform) ? BYPASS_POLICY - : MANUAL_POLICY + : { ...MANUAL_POLICY, ...requested } } diff --git a/src/main/native-chat/structured-agent-arguments-error.ts b/src/main/native-chat/structured-agent-arguments-error.ts index f6fca945307d..05a2911d5083 100644 --- a/src/main/native-chat/structured-agent-arguments-error.ts +++ b/src/main/native-chat/structured-agent-arguments-error.ts @@ -1,4 +1,5 @@ import type { AgentSessionArgumentProblem } from '../../shared/agent-session-failure' +import { AGENT_SESSION_ARGUMENT_PROBLEM_WORD } from '../../shared/agent-session-argument-problem' /** A saved Arguments refusal with only the option name, never its value. */ export class StructuredAgentArgumentsError extends Error { @@ -10,11 +11,10 @@ export class StructuredAgentArgumentsError extends Error { problem: AgentSessionArgumentProblem['problem'] ) { const normalizedOption = - problem === 'positionalPrompt' - ? 'prompt' - : (option.match(/^--[a-zA-Z][a-zA-Z0-9-]{0,63}/)?.[0] ?? - option.match(/^-[a-zA-Z]/)?.[0] ?? - '--?') + AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] ?? + option.match(/^--[a-zA-Z][a-zA-Z0-9-]{0,63}/)?.[0] ?? + option.match(/^-[a-zA-Z]/)?.[0] ?? + '--?' super(`${agent} structured chat cannot use ${normalizedOption} in saved Arguments`) this.name = 'StructuredAgentArgumentsError' this.argumentProblem = { agent, option: normalizedOption, problem } diff --git a/src/main/native-chat/structured-agent-configured-args.test.ts b/src/main/native-chat/structured-agent-configured-args.test.ts index 58823a406088..3db4cd3a9d35 100644 --- a/src/main/native-chat/structured-agent-configured-args.test.ts +++ b/src/main/native-chat/structured-agent-configured-args.test.ts @@ -1,5 +1,18 @@ import { describe, expect, it } from 'vitest' import { structuredAgentConfiguredArgs } from './structured-agent-configured-args' +import { + StructuredAgentArgumentsError, + argumentProblemOf +} from './structured-agent-arguments-error' + +function captured(run: () => unknown): unknown { + try { + run() + } catch (error) { + return error + } + return undefined +} describe('structured chat configured Arguments', () => { it.each(['claude', 'codex'] as const)( @@ -37,11 +50,19 @@ describe('structured chat configured Arguments', () => { ).toEqual(['--plugin-dir', String.raw`C:\My Plugins`]) }) - it('refuses unclosed quotes without spawning', () => { - expect(() => - structuredAgentConfiguredArgs('claude', { - agentDefaultArgs: { claude: '--model "unfinished' } + it.each([ + ['claude', 'Claude'], + ['codex', 'Codex'] + ] as const)('refuses unclosed %s quotes as a saved Arguments problem', (agent, agentName) => { + const read = () => + structuredAgentConfiguredArgs(agent, { + agentDefaultArgs: { [agent]: '--model "unfinished secret' } }) - ).toThrow('Arguments are invalid') + expect(read).toThrow(StructuredAgentArgumentsError) + expect(argumentProblemOf(captured(read))).toEqual({ + agent: agentName, + option: 'quote', + problem: 'unclosedQuote' + }) }) }) diff --git a/src/main/native-chat/structured-agent-configured-args.ts b/src/main/native-chat/structured-agent-configured-args.ts index 3b60c7642680..39b564188a4b 100644 --- a/src/main/native-chat/structured-agent-configured-args.ts +++ b/src/main/native-chat/structured-agent-configured-args.ts @@ -3,6 +3,7 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import { resolveTuiAgentLaunchArgs } from '../../shared/tui-agent-launch-defaults' import { tokenizeStartupCommand } from '../../shared/tui-agent-startup-shell' import { resolveLocalWindowsAgentStartupShell } from '../../shared/windows-terminal-shell' +import { StructuredAgentArgumentsError } from './structured-agent-arguments-error' /** Use the same grouping rules as terminal launches before the provider filters its owned flags. */ export function structuredAgentConfiguredArgs( @@ -20,8 +21,13 @@ export function structuredAgentConfiguredArgs( resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs), shell ) + // An unclosed quote is the tokenizer's only failure. if (!parsed.ok) { - throw new Error(`${agent} Arguments are invalid: ${parsed.error}`) + throw new StructuredAgentArgumentsError( + agent === 'codex' ? 'Codex' : 'Claude', + 'quote', + 'unclosedQuote' + ) } return parsed.tokens } diff --git a/src/main/runtime/orca-runtime-get-worktree-ps.ts b/src/main/runtime/orca-runtime-get-worktree-ps.ts index bd812e4324ba..36dfe1de0135 100644 --- a/src/main/runtime/orca-runtime-get-worktree-ps.ts +++ b/src/main/runtime/orca-runtime-get-worktree-ps.ts @@ -214,8 +214,8 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVis // the one copy of this fact, even when Arguments contain permission flags. resolveClaudePermissionMode: () => claudeStructuredPermissionModeForSettings(this.requireStore().getSettings()), - resolveCodexPermissionPolicy: () => - codexStructuredPermissionPolicyForSettings(this.requireStore().getSettings()), + resolveCodexPermissionPolicy: (requested) => + codexStructuredPermissionPolicyForSettings(this.requireStore().getSettings(), requested), // Same gate and same settings as agentSession.createSupport, re-read on every acquisition. getClaudeManagedAccountGateSettings: () => this.requireStore().getSettings(), resolveAgentAccountHome: (agent) => this.resolveStructuredAgentAccountHome(agent), diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index dd81e303b41d..4930174c7a3e 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -37,6 +37,16 @@ describe('execution host structured launch settings wiring', () => { settings.agentDefaultArgs = { claude: '--model second', codex: '' } expect(await deps?.resolveLaunchArgs?.('claude')).toEqual(['--model', 'second']) expect(await deps?.resolveLaunchArgs?.('codex')).toEqual([]) + // Manual takes what the Arguments state; Yolo keeps bypassing. + expect(deps?.resolveCodexPermissionPolicy?.({ sandbox: 'read-only' })).toEqual({ + approvalPolicy: 'on-request', + sandbox: 'read-only' + }) + settings.agentDefaultArgs = { codex: '--dangerously-bypass-approvals-and-sandbox' } + expect(deps?.resolveCodexPermissionPolicy?.({ sandbox: 'read-only' })).toEqual({ + approvalPolicy: 'never', + sandbox: 'danger-full-access' + }) const notRunnable = expect.objectContaining({ reason: 'agentCommandNotRunnable' }) settings.agentCmdOverrides = { claude: 'wrapper --arg' } expect(() => deps?.resolveClaudeCommand?.()).toThrow(notRunnable) diff --git a/src/main/runtime/structured-agent-model-catalog-wiring.ts b/src/main/runtime/structured-agent-model-catalog-wiring.ts index 0b537dd04893..80a6f70a7fc1 100644 --- a/src/main/runtime/structured-agent-model-catalog-wiring.ts +++ b/src/main/runtime/structured-agent-model-catalog-wiring.ts @@ -55,6 +55,7 @@ export async function modelCatalogHostDeps(input: { | 'resolveClaudeCommand' | 'resolveClaudeLaunchEnv' | 'resolveClaudeAuthPolicy' + | 'resolveLaunchArgs' > envResolvers: { resolveCodexEnvironment: NonNullable @@ -77,6 +78,7 @@ export async function modelCatalogHostDeps(input: { probes: { codex: createCodexModelCatalogProbe({ resolveEnvironment: input.envResolvers.resolveCodexEnvironment, + resolveLaunchArgs: () => deps.resolveLaunchArgs('codex'), ...(deps.resolveCodexCommand ? { resolveCommand: deps.resolveCodexCommand } : {}) }), claude: createClaudeModelCatalogProbe({ diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index eb12f16935c1..c6a4db1102e3 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -113,8 +113,10 @@ export type StructuredAgentSessionRuntimeDeps = { resolveClaudeAuthPolicy: () => Promise | ClaudeStructuredAuthPolicy /** The user's Agent Permissions setting for Claude; absent means prompting. */ resolveClaudePermissionMode?: () => Promise | PermissionMode - /** The same setting for Codex, as app-server thread policy. */ - resolveCodexPermissionPolicy?: () => CodexStructuredPermissionPolicy + /** The same setting for Codex, as app-server thread policy, given what the Arguments state. */ + resolveCodexPermissionPolicy?: ( + requested: Partial + ) => CodexStructuredPermissionPolicy /** Raw settings getter; the reader that fails closed around it is built here, in checked code. */ getClaudeManagedAccountGateSettings?: () => ClaudeManagedAccountGateSettings resolveEnvironment?: () => Promise diff --git a/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts b/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts new file mode 100644 index 000000000000..f9727b334a67 --- /dev/null +++ b/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts @@ -0,0 +1,55 @@ +// Desktop words for a saved Arguments refusal, one piece each of the shared sentence. + +import { translate } from '@/i18n/i18n' +import { + AGENT_SESSION_FAILURE_COPY as COPY, + type AgentSessionFailureCopyId, + type AgentSessionFailureCopyValues +} from '../../../../shared/agent-session-failure-copy' + +type SavedArgumentsCopyId = Extract< + AgentSessionFailureCopyId, + `arguments${string}` | 'editSavedArguments' +> + +export const SAVED_ARGUMENTS_PIECES: Record< + SavedArgumentsCopyId, + (values: AgentSessionFailureCopyValues) => string +> = { + argumentsUnsupportedOption: (values) => + translate( + 'components.native-chat.failureWords.argumentsUnsupportedOption', + COPY.argumentsUnsupportedOption, + values + ), + argumentsMissingValue: (values) => + translate( + 'components.native-chat.failureWords.argumentsMissingValue', + COPY.argumentsMissingValue, + values + ), + argumentsMultipleValues: (values) => + translate( + 'components.native-chat.failureWords.argumentsMultipleValues', + COPY.argumentsMultipleValues, + values + ), + argumentsInvalidValue: (values) => + translate( + 'components.native-chat.failureWords.argumentsInvalidValue', + COPY.argumentsInvalidValue, + values + ), + argumentsPositionalPrompt: () => + translate( + 'components.native-chat.failureWords.argumentsPositionalPrompt', + COPY.argumentsPositionalPrompt + ), + argumentsUnclosedQuote: () => + translate( + 'components.native-chat.failureWords.argumentsUnclosedQuote', + COPY.argumentsUnclosedQuote + ), + editSavedArguments: () => + translate('components.native-chat.failureWords.editSavedArguments', COPY.editSavedArguments) +} diff --git a/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts b/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts index 0fa34b19aaab..98853ee556ee 100644 --- a/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts +++ b/src/renderer/src/components/native-chat/agent-session-failure-words-text.ts @@ -8,6 +8,7 @@ import { type AgentSessionFailureCopyValues, type AgentSessionFailureSay } from '../../../../shared/agent-session-failure-copy' +import { SAVED_ARGUMENTS_PIECES } from './agent-session-arguments-failure-words-text' // The pieces a refusal notice says too keep the notice's keys, so each has one translation. const PIECES: Record string> = @@ -40,31 +41,7 @@ const PIECES: Record - translate( - 'components.native-chat.failureWords.argumentsUnsupportedOption', - COPY.argumentsUnsupportedOption, - values - ), - argumentsMissingValue: (values) => - translate( - 'components.native-chat.failureWords.argumentsMissingValue', - COPY.argumentsMissingValue, - values - ), - argumentsMultipleValues: (values) => - translate( - 'components.native-chat.failureWords.argumentsMultipleValues', - COPY.argumentsMultipleValues, - values - ), - argumentsPositionalPrompt: () => - translate( - 'components.native-chat.failureWords.argumentsPositionalPrompt', - COPY.argumentsPositionalPrompt - ), - editSavedArguments: () => - translate('components.native-chat.failureWords.editSavedArguments', COPY.editSavedArguments), + ...SAVED_ARGUMENTS_PIECES, terminalAgentHoldsChat: () => translate( 'components.native-chat.writeNotice.terminalAgentHoldsChat', diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index b4d9be1ceea0..790e0098584c 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -100,7 +100,7 @@ export function AgentPermissionsSetting({ {translate( 'auto.components.settings.AgentsPane.agentPermissionsTooltip', - "Doesn't apply to agents where you've overridden launch arguments." + "Switching modes doesn't change Arguments you've edited for an agent." )} diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 2933c7538f5f..aec1a1cdc8a2 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2759,6 +2759,8 @@ "answerUnconfirmed": "Your answer was recorded but the agent did not confirm it.", "argumentsMissingValue": "Saved Arguments need a value for {{option}}.", "argumentsMultipleValues": "Saved Arguments give {{option}} more than one value.", + "argumentsInvalidValue": "Saved Arguments give {{option}} a value that isn't supported.", + "argumentsUnclosedQuote": "Saved Arguments have a quote that isn't closed.", "argumentsPositionalPrompt": "Saved Arguments include a prompt.", "argumentsUnsupportedOption": "Saved Arguments contain an unsupported option ({{option}}).", "attachmentAtMost": "{{agent}} accepts at most {{limit}} images in one message, so this message was not sent.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index a8320eb8c969..4a75d3c48895 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -6959,7 +6959,7 @@ "2d133152fa": "No default environment", "agentPermissions": "Agent Permissions", "agentPermissionsInfo": "Agent permissions info", - "agentPermissionsTooltip": "Doesn't apply to agents where you've overridden launch arguments.", + "agentPermissionsTooltip": "Switching modes doesn't change Arguments you've edited for an agent.", "agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manual", @@ -17849,6 +17849,8 @@ "argumentsUnsupportedOption": "Saved Arguments contain an unsupported option ({{option}}).", "argumentsMissingValue": "Saved Arguments need a value for {{option}}.", "argumentsMultipleValues": "Saved Arguments give {{option}} more than one value.", + "argumentsInvalidValue": "Saved Arguments give {{option}} a value that isn't supported.", + "argumentsUnclosedQuote": "Saved Arguments have a quote that isn't closed.", "argumentsPositionalPrompt": "Saved Arguments include a prompt.", "editSavedArguments": "Edit them in Settings > Agents > Arguments." }, diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 5af0be4c76b5..f12e84aa1785 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -6736,7 +6736,7 @@ "2d133152fa": "Sin entorno predeterminado", "agentPermissions": "Permisos de Agents", "agentPermissionsInfo": "Información de permisos de Agents", - "agentPermissionsTooltip": "No se aplica a agents con argumentos de lanzamiento sobrescritos.", + "agentPermissionsTooltip": "Cambiar de modo no modifica los argumentos que hayas editado para un agente.", "agentPermissionsDescription": "Elige si Orca lanza agents con menos prompts de permisos o con checks manuales.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manual", @@ -17613,6 +17613,8 @@ "answerUnconfirmed": "Su respuesta se registró, pero el agente no la confirmó.", "argumentsMissingValue": "Los argumentos guardados necesitan un valor para {{option}}.", "argumentsMultipleValues": "Los argumentos guardados asignan más de un valor a {{option}}.", + "argumentsInvalidValue": "Los argumentos guardados asignan a {{option}} un valor no compatible.", + "argumentsUnclosedQuote": "Los argumentos guardados tienen una comilla sin cerrar.", "argumentsPositionalPrompt": "Los argumentos guardados incluyen un mensaje.", "argumentsUnsupportedOption": "Los argumentos guardados contienen una opción no compatible ({{option}}).", "hostFault": "Orca tuvo un problema, así que esto no se completó.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 93954938d4c3..9b02e6e58d5e 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -6845,7 +6845,7 @@ "2d133152fa": "Aucun environnement par défaut", "agentPermissions": "Permissions des agents", "agentPermissionsInfo": "Infos permissions des agents", - "agentPermissionsTooltip": "Ne s'applique pas aux agents dont vous avez remplacé les arguments de lancement.", + "agentPermissionsTooltip": "Changer de mode ne modifie pas les arguments que vous avez modifiés pour un agent.", "agentPermissionsDescription": "Choisissez si Orca lance les agents avec moins de demandes de permission ou avec des vérifications manuelles.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manuel", @@ -17714,6 +17714,8 @@ "answerUnconfirmed": "Votre réponse a été enregistrée, mais l'agent ne l'a pas confirmée.", "argumentsMissingValue": "Les arguments enregistrés nécessitent une valeur pour {{option}}.", "argumentsMultipleValues": "Les arguments enregistrés donnent plusieurs valeurs à {{option}}.", + "argumentsInvalidValue": "Les arguments enregistrés donnent à {{option}} une valeur non prise en charge.", + "argumentsUnclosedQuote": "Les arguments enregistrés contiennent un guillemet non fermé.", "argumentsPositionalPrompt": "Les arguments enregistrés contiennent un message.", "argumentsUnsupportedOption": "Les arguments enregistrés contiennent une option non prise en charge ({{option}}).", "hostFault": "Orca a rencontré un problème, l'opération n'a donc pas abouti.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 676b1d728d91..6ec5f6b20c31 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -6721,7 +6721,7 @@ "2d133152fa": "デフォルト環境なし", "agentPermissions": "Agent の権限", "agentPermissionsInfo": "Agent 権限情報", - "agentPermissionsTooltip": "起動引数を上書きしている Agent には適用されません。", + "agentPermissionsTooltip": "モードを切り替えても、Agent ごとに編集した引数は変更されません。", "agentPermissionsDescription": "Orca が権限プロンプトの少ない Agent を起動するか、手動チェックを含む Agent を起動するかを選択します。", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "手動", @@ -17648,6 +17648,8 @@ "answerUnconfirmed": "回答は記録されましたが、Agent は確認しませんでした。", "argumentsMissingValue": "保存された引数の {{option}} に値が必要です。", "argumentsMultipleValues": "保存された引数の {{option}} に複数の値が指定されています。", + "argumentsInvalidValue": "保存された引数の {{option}} に未対応の値が指定されています。", + "argumentsUnclosedQuote": "保存された引数に閉じられていない引用符があります。", "argumentsPositionalPrompt": "保存された引数にプロンプトが含まれています。", "argumentsUnsupportedOption": "保存された引数に未対応のオプション({{option}})が含まれています。", "hostFault": "Orca で問題が発生したため、この操作は完了しませんでした。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 829fa949ea67..731da0f008b2 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -6721,7 +6721,7 @@ "2d133152fa": "기본 환경 없음", "agentPermissions": "Agent 권한", "agentPermissionsInfo": "Agent 권한 정보", - "agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.", + "agentPermissionsTooltip": "모드를 전환해도 에이전트별로 수정한 인수는 변경되지 않습니다.", "agentPermissionsDescription": "Orca가 agents를 실행할 때 권한 프롬프트를 줄일지, 수동 확인을 유지할지 선택합니다.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "수동", @@ -17648,6 +17648,8 @@ "answerUnconfirmed": "응답이 기록되었지만 에이전트가 확인하지 않았습니다.", "argumentsMissingValue": "저장된 인수의 {{option}}에 값이 필요합니다.", "argumentsMultipleValues": "저장된 인수에 {{option}}의 값이 여러 개 지정되어 있습니다.", + "argumentsInvalidValue": "저장된 인수에 {{option}}의 지원하지 않는 값이 지정되어 있습니다.", + "argumentsUnclosedQuote": "저장된 인수에 닫히지 않은 따옴표가 있습니다.", "argumentsPositionalPrompt": "저장된 인수에 프롬프트가 포함되어 있습니다.", "argumentsUnsupportedOption": "저장된 인수에 지원하지 않는 옵션({{option}})이 포함되어 있습니다.", "hostFault": "Orca에 문제가 발생하여 이 작업이 완료되지 않았습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 48e88654708f..c8911095166d 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -6721,7 +6721,7 @@ "2d133152fa": "无默认环境", "agentPermissions": "智能体权限", "agentPermissionsInfo": "智能体权限信息", - "agentPermissionsTooltip": "切换模式时,自定义智能体参数保持不变。", + "agentPermissionsTooltip": "切换模式不会更改你为智能体编辑过的参数。", "agentPermissionsDescription": "选择 Orca 是以更少的权限提示启动智能体,还是手动检查。", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "手动", @@ -17613,6 +17613,8 @@ "answerUnconfirmed": "您的回答已记录,但智能体未确认。", "argumentsMissingValue": "已保存的参数需要为 {{option}} 指定一个值。", "argumentsMultipleValues": "已保存的参数为 {{option}} 指定了多个值。", + "argumentsInvalidValue": "已保存的参数为 {{option}} 指定了不支持的值。", + "argumentsUnclosedQuote": "已保存的参数中有未闭合的引号。", "argumentsPositionalPrompt": "已保存的参数中包含提示词。", "argumentsUnsupportedOption": "已保存的参数包含不支持的选项({{option}})。", "hostFault": "Orca 遇到问题,因此此操作未完成。", diff --git a/src/shared/agent-session-argument-problem.ts b/src/shared/agent-session-argument-problem.ts index b6fcc98d3362..790682aa87ad 100644 --- a/src/shared/agent-session-argument-problem.ts +++ b/src/shared/agent-session-argument-problem.ts @@ -2,9 +2,20 @@ export type AgentSessionArgumentProblem = { agent: 'Codex' | 'Claude' option: string - problem: 'unsupportedOption' | 'missingValue' | 'multipleValues' | 'positionalPrompt' + problem: + | 'unsupportedOption' + | 'missingValue' + | 'multipleValues' + | 'invalidValue' + | 'positionalPrompt' + | 'unclosedQuote' } +/** Problems that are about no single option carry this fixed word as their option. */ +export const AGENT_SESSION_ARGUMENT_PROBLEM_WORD: Partial< + Record +> = { positionalPrompt: 'prompt', unclosedQuote: 'quote' } + export function readAgentSessionArgumentProblem( value: unknown ): AgentSessionArgumentProblem | undefined { @@ -26,10 +37,12 @@ export function readAgentSessionArgumentProblem( (problem !== 'unsupportedOption' && problem !== 'missingValue' && problem !== 'multipleValues' && - problem !== 'positionalPrompt') || + problem !== 'invalidValue' && + problem !== 'positionalPrompt' && + problem !== 'unclosedQuote') || typeof option !== 'string' || - (problem === 'positionalPrompt' - ? option !== 'prompt' + (AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] !== undefined + ? option !== AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] : !/^(?:--[a-zA-Z][a-zA-Z0-9-]{0,63}|-[a-zA-Z]|--\?)$/.test(option)) ) { return undefined diff --git a/src/shared/agent-session-failure-copy.ts b/src/shared/agent-session-failure-copy.ts index 4575485b456b..8bdbccd7f088 100644 --- a/src/shared/agent-session-failure-copy.ts +++ b/src/shared/agent-session-failure-copy.ts @@ -20,7 +20,9 @@ export const AGENT_SESSION_FAILURE_COPY = { argumentsUnsupportedOption: 'Saved Arguments contain an unsupported option ({{option}}).', argumentsMissingValue: 'Saved Arguments need a value for {{option}}.', argumentsMultipleValues: 'Saved Arguments give {{option}} more than one value.', + argumentsInvalidValue: "Saved Arguments give {{option}} a value that isn't supported.", argumentsPositionalPrompt: 'Saved Arguments include a prompt.', + argumentsUnclosedQuote: "Saved Arguments have a quote that isn't closed.", editSavedArguments: 'Edit them in Settings > Agents > Arguments.', terminalAgentHoldsChat: TERMINAL_AGENT_HOLDS_CHAT, quitTerminalAgent: QUIT_TERMINAL_AGENT, diff --git a/src/shared/agent-session-failure-words.ts b/src/shared/agent-session-failure-words.ts index b8dacdfd8136..a6b307c526e2 100644 --- a/src/shared/agent-session-failure-words.ts +++ b/src/shared/agent-session-failure-words.ts @@ -143,7 +143,9 @@ function couldNot(verb: 'couldNotStart' | 'couldNotRestart'): Sentence { unsupportedOption: 'argumentsUnsupportedOption', missingValue: 'argumentsMissingValue', multipleValues: 'argumentsMultipleValues', - positionalPrompt: 'argumentsPositionalPrompt' + invalidValue: 'argumentsInvalidValue', + positionalPrompt: 'argumentsPositionalPrompt', + unclosedQuote: 'argumentsUnclosedQuote' } as const return joinSentences([ failed, diff --git a/src/shared/agent-session-refusal-notice.test.ts b/src/shared/agent-session-refusal-notice.test.ts index 52aa497f83c2..8c8f8510716f 100644 --- a/src/shared/agent-session-refusal-notice.test.ts +++ b/src/shared/agent-session-refusal-notice.test.ts @@ -741,9 +741,31 @@ describe('saved Arguments refusals', () => { ) }) + it.each([ + [ + { agent: 'Codex', option: '-s', problem: 'invalidValue' }, + "Codex couldn't start. Saved Arguments give -s a value that isn't supported. Edit them in Settings > Agents > Arguments." + ], + [ + { agent: 'Claude', option: 'quote', problem: 'unclosedQuote' }, + "Claude couldn't start. Saved Arguments have a quote that isn't closed. Edit them in Settings > Agents > Arguments." + ] + ] as const)('names the %j problem and where to fix it', (argumentProblem, sentence) => { + const parsed = parseAgentSessionWriteFailure({ + kind: 'refused', + code: 'agent_session_operation_invalid', + details: { reason: 'attachFailed', argumentProblem } + }) + if (!parsed) { + throw new Error('saved refusal was not read') + } + expect(agentSessionWriteNoticeEnglish(agentSessionRefusalCauseParts(parsed))).toBe(sentence) + }) + it.each([ { agent: 'Codex', option: '--remote', problem: 'futureProblem' }, - { agent: 'Codex', option: '--remote=private', problem: 'unsupportedOption' } + { agent: 'Codex', option: '--remote=private', problem: 'unsupportedOption' }, + { agent: 'Codex', option: '--private', problem: 'unclosedQuote' } ])('ignores an unrecognized or unsafe argument detail', (argumentProblem) => { const refused = parseAgentSessionWriteFailure({ kind: 'refused', From 56e545df801258cfe52a17ac038780b53ff7e1f1 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:17:06 -0700 Subject: [PATCH 2/6] fix(native-chat): state the Codex reviewer on every thread and say how to fix refused Arguments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A resumed Codex chat kept the approvals reviewer it last ran with, because the reviewer from --approve-for-me or -c approvals_reviewer= reached Codex only as process config, which a resume's saved setting outranks. Removing --approve-for-me left the automatic reviewer approving requests. Every thread start and resume now states the reviewer with the rest of the policy: the one the Arguments name under Manual, otherwise Codex's default, user. approval_policy on-failure (from -a or -c) now reads as on-request, which is Codex's own alias, instead of refusing the chat. Refusals now say what to do. A refused option reads "Codex chats can't use --profile from saved Arguments. Remove it from Codex's Arguments in Settings → Agents." plus, where one exists, what a chat honors instead (config.toml or -c for a profile, -c model_provider= for --oss, the chat's workspace folder for -C/--add-dir/--worktree, attaching an image for -i). An invalid sandbox, approval policy or reviewer names the setting and its accepted values. A lone "-" is a prompt, not an unnamed option. --- .../codex-structured-launch-args.test.ts | 47 +++++++++--- .../codex/codex-structured-launch-args.ts | 44 +++++++---- ...codex-structured-launch-resolution.test.ts | 37 ++++++++-- ...codex-structured-permission-policy.test.ts | 29 ++++++-- .../codex-structured-permission-policy.ts | 26 +++++-- .../codex-structured-thread-open.test.ts | 21 +++++- ...-agent-session-accept-then-deliver.test.ts | 2 +- ...uctured-agent-session-failure-text.test.ts | 4 +- ...ent-session-pre-spawn-first-answer.test.ts | 2 +- .../structured-agent-arguments-error.ts | 6 +- ...-structured-launch-settings-wiring.test.ts | 6 +- ...tructuredSession.launch-lifecycle.test.tsx | 2 +- ...nt-session-arguments-failure-words-text.ts | 54 +++++++++++++- .../src/i18n/en-runtime-required.json | 13 +++- src/renderer/src/i18n/locales/en.json | 13 +++- src/renderer/src/i18n/locales/es.json | 13 +++- src/renderer/src/i18n/locales/fr.json | 13 +++- src/renderer/src/i18n/locales/ja.json | 13 +++- src/renderer/src/i18n/locales/ko.json | 13 +++- src/renderer/src/i18n/locales/zh.json | 13 +++- .../agent-session-argument-problem-words.ts | 74 +++++++++++++++++++ src/shared/agent-session-argument-problem.ts | 28 +++++-- src/shared/agent-session-failure-copy.ts | 16 +++- src/shared/agent-session-failure-words.ts | 12 +-- .../agent-session-refusal-notice.test.ts | 44 +++++++++-- 25 files changed, 450 insertions(+), 95 deletions(-) create mode 100644 src/shared/agent-session-argument-problem-words.ts diff --git a/src/main/codex/codex-structured-launch-args.test.ts b/src/main/codex/codex-structured-launch-args.test.ts index 5bbb3a74bb74..4e8e04419010 100644 --- a/src/main/codex/codex-structured-launch-args.test.ts +++ b/src/main/codex/codex-structured-launch-args.test.ts @@ -44,7 +44,7 @@ describe('codexStructuredLaunchArgs', () => { ]) }) - it('passes sandbox details and the reviewer, which Orca does not own', () => { + it('passes sandbox details and the reviewer, and reads the reviewer for the thread', () => { expect( codexStructuredLaunchArgs([ '-c', @@ -59,7 +59,17 @@ describe('codexStructuredLaunchArgs', () => { '-c', 'approvals_reviewer="auto_review"' ], - permissions: {} + permissions: { approvalsReviewer: 'auto_review' } + }) + }) + + it("reads Codex's own aliases as the values they stand for", () => { + expect( + codexStructuredLaunchArgs(['-c', 'approvals_reviewer=guardian_subagent', '-a', 'on-failure']) + .permissions + ).toEqual({ approvalsReviewer: 'auto_review', approvalPolicy: 'on-request' }) + expect(codexStructuredLaunchArgs(['-c', 'approval_policy="on-failure"']).permissions).toEqual({ + approvalPolicy: 'on-request' }) }) @@ -96,7 +106,11 @@ describe('codexStructuredLaunchArgs', () => { '-c', 'sandbox_mode="workspace-write"' ], - permissions: { approvalPolicy: 'on-request', sandbox: 'workspace-write' } + permissions: { + approvalsReviewer: 'auto_review', + approvalPolicy: 'on-request', + sandbox: 'workspace-write' + } }) }) @@ -117,6 +131,7 @@ describe('codexStructuredLaunchArgs', () => { it.each([ { tokens: ['a private prompt'], option: 'prompt', problem: 'positionalPrompt' }, { tokens: ['app-server'], option: 'prompt', problem: 'positionalPrompt' }, + { tokens: ['-'], option: 'prompt', problem: 'positionalPrompt' }, { tokens: ['--', 'a private prompt'], option: 'prompt', problem: 'positionalPrompt' }, { tokens: ['--profile', 'private'], option: '--profile', problem: 'unsupportedOption' }, { tokens: ['-pprivate'], option: '-p', problem: 'unsupportedOption' }, @@ -150,14 +165,24 @@ describe('codexStructuredLaunchArgs', () => { { tokens: ['--search=secret'], option: '--search', problem: 'unsupportedOption' }, { tokens: ['--enable'], option: '--enable', problem: 'missingValue' }, { tokens: ['-m', '--secret'], option: '-m', problem: 'missingValue' }, - { tokens: ['-s', 'private-mode'], option: '-s', problem: 'invalidValue' }, + // An invalid value names the setting it is for, however the Arguments spelled it. + { tokens: ['-s', 'private-mode'], option: 'sandbox_mode', problem: 'invalidValue' }, { - tokens: ['--ask-for-approval=on-failure'], - option: '--ask-for-approval', + tokens: ['--ask-for-approval=private'], + option: 'approval_policy', problem: 'invalidValue' }, - { tokens: ['-c', 'approval_policy={ secret = true }'], option: '-c', problem: 'invalidValue' }, - { tokens: ['--config=sandbox_mode=secret'], option: '--config', problem: 'invalidValue' } + { + tokens: ['-c', 'approval_policy={ secret = true }'], + option: 'approval_policy', + problem: 'invalidValue' + }, + { tokens: ['--config=sandbox_mode=secret'], option: 'sandbox_mode', problem: 'invalidValue' }, + { + tokens: ['-c', 'approvals_reviewer=secret'], + option: 'approvals_reviewer', + problem: 'invalidValue' + } ] as const)('refuses what a chat cannot honor: %j', ({ tokens, option, problem }) => { const thrown = () => codexStructuredLaunchArgs(tokens) expect(thrown).toThrow(StructuredAgentArgumentsError) @@ -167,8 +192,10 @@ describe('codexStructuredLaunchArgs', () => { expect(error).toMatchObject({ argumentProblem: { agent: 'Codex', option, problem } }) - expect(JSON.stringify(error)).not.toContain('secret') - expect(JSON.stringify(error)).not.toContain('private') + for (const text of [JSON.stringify(error), String(error)]) { + expect(text).not.toContain('secret') + expect(text).not.toContain('private') + } } }) }) diff --git a/src/main/codex/codex-structured-launch-args.ts b/src/main/codex/codex-structured-launch-args.ts index 52d326347b25..c6e5e0fd06fc 100644 --- a/src/main/codex/codex-structured-launch-args.ts +++ b/src/main/codex/codex-structured-launch-args.ts @@ -3,8 +3,10 @@ // either passes to app-server, becomes the config Codex itself derives from it, is dropped because // it only shapes a terminal, or refuses the start by name because a chat can't honor it. import { StructuredAgentArgumentsError } from '../native-chat/structured-agent-arguments-error' +import type { CodexArgumentSetting } from '../../shared/agent-session-argument-problem' import { CODEX_APPROVAL_POLICIES, + CODEX_APPROVALS_REVIEWERS, CODEX_SANDBOX_MODES, type CodexStructuredPermissionPolicy } from './codex-structured-permission-policy' @@ -12,7 +14,7 @@ import { export type CodexStructuredLaunchArgs = { /** Options for after `app-server`. */ args: string[] - /** The sandbox and approval policy the Arguments state explicitly. */ + /** The sandbox, approval policy and reviewer the Arguments state explicitly. */ permissions: Partial } @@ -35,7 +37,7 @@ const pass = (plan: LaunchPlan, option: string, value: string): void => { } const passConfig = (plan: LaunchPlan, option: string, value: string): void => { - readConfiguredPermission(plan.configured, option, value) + readConfiguredPermission(plan.configured, value) pass(plan, option, value) } @@ -50,12 +52,12 @@ const deriveModel = (plan: LaunchPlan, _option: string, value: string): void => plan.derived.push(`model=${JSON.stringify(value)}`) } -const flagSandbox = (plan: LaunchPlan, option: string, value: string): void => { - plan.flags.sandbox = oneOf(CODEX_SANDBOX_MODES, value, option) +const flagSandbox = (plan: LaunchPlan, _option: string, value: string): void => { + plan.flags.sandbox = oneOf(CODEX_SANDBOX_MODES, value, 'sandbox_mode') } -const flagApproval = (plan: LaunchPlan, option: string, value: string): void => { - plan.flags.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, option) +const flagApproval = (plan: LaunchPlan, _option: string, value: string): void => { + plan.flags.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, 'approval_policy') } const VALUE = { takesValue: true } @@ -111,18 +113,30 @@ const RULES: Record = { '--dangerously-bypass-hook-trust': 'refuse' } -function oneOf(values: readonly T[], value: string, option: string): T { - const match = values.find((candidate) => candidate === value) +// Codex's own aliases: `on-failure` reads as `on-request` from 0.143; `guardian_subagent` is the +// reviewer's old name. +const VALUE_ALIASES = new Map([ + ['on-failure', 'on-request'], + ['guardian_subagent', 'auto_review'] +]) + +function oneOf( + values: readonly T[], + value: string, + setting: CodexArgumentSetting +): T { + const canonical = VALUE_ALIASES.get(value) ?? value + const match = values.find((candidate) => candidate === canonical) if (match === undefined) { - throw new StructuredAgentArgumentsError('Codex', option, 'invalidValue') + throw new StructuredAgentArgumentsError('Codex', setting, 'invalidValue') } return match } -/** The policy a `-c approval_policy=` / `sandbox_mode=` sets, read the way Codex reads it. */ +/** The policy a `-c approval_policy=` / `sandbox_mode=` / `approvals_reviewer=` sets, read the + * way Codex reads it. */ function readConfiguredPermission( configured: Partial, - option: string, override: string ): void { const separator = override.indexOf('=') @@ -136,9 +150,11 @@ function readConfiguredPermission( .trim() .replace(/^["']+|["']+$/g, '') if (key === 'approval_policy') { - configured.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, option) + configured.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, key) } else if (key === 'sandbox_mode') { - configured.sandbox = oneOf(CODEX_SANDBOX_MODES, value, option) + configured.sandbox = oneOf(CODEX_SANDBOX_MODES, value, key) + } else if (key === 'approvals_reviewer') { + configured.approvalsReviewer = oneOf(CODEX_APPROVALS_REVIEWERS, value, key) } } @@ -169,7 +185,7 @@ export function codexStructuredLaunchArgs(tokens: readonly string[]): CodexStruc } break } - if (!token.startsWith('-')) { + if (!token.startsWith('-') || token === '-') { throw new StructuredAgentArgumentsError('Codex', token, 'positionalPrompt') } const option = optionName(token) diff --git a/src/main/codex/codex-structured-launch-resolution.test.ts b/src/main/codex/codex-structured-launch-resolution.test.ts index 58a4b7ca0326..44d37cbd6dbb 100644 --- a/src/main/codex/codex-structured-launch-resolution.test.ts +++ b/src/main/codex/codex-structured-launch-resolution.test.ts @@ -151,7 +151,11 @@ describe('codex structured launch resolution', () => { codexHome: '/home/work/.codex', resumeThreadId: null, // Every launch now carries a posture; neither one is left for config.toml to decide. - permissionPolicy: { approvalPolicy: 'on-request', sandbox: 'workspace-write' } + permissionPolicy: { + approvalPolicy: 'on-request', + sandbox: 'workspace-write', + approvalsReviewer: 'user' + } }) }) @@ -242,7 +246,8 @@ describe('codex structured launch resolution', () => { expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'never', - sandbox: 'danger-full-access' + sandbox: 'danger-full-access', + approvalsReviewer: 'user' }) }) @@ -252,7 +257,8 @@ describe('codex structured launch resolution', () => { expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'never', - sandbox: 'danger-full-access' + sandbox: 'danger-full-access', + approvalsReviewer: 'user' }) }) @@ -264,7 +270,8 @@ describe('codex structured launch resolution', () => { expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'on-request', - sandbox: 'workspace-write' + sandbox: 'workspace-write', + approvalsReviewer: 'user' }) }) @@ -301,7 +308,24 @@ describe('codex structured launch resolution', () => { )({ identity: IDENTITY }) expect(launch.args).toEqual(['app-server', '-c', 'approval_policy=untrusted']) - expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'untrusted', sandbox: 'read-only' }) + expect(launch.permissionPolicy).toEqual({ + approvalPolicy: 'untrusted', + sandbox: 'read-only', + approvalsReviewer: 'user' + }) + }) + + // A resume keeps the thread's saved reviewer unless the request names one. + it('opens the thread with the reviewer --approve-for-me asks for under Manual', async () => { + const launch = await resolverFor(record({ launchArgs: ['--approve-for-me'] }))({ + identity: IDENTITY + }) + + expect(launch.permissionPolicy).toEqual({ + approvalPolicy: 'on-request', + sandbox: 'workspace-write', + approvalsReviewer: 'auto_review' + }) }) it('keeps Yolo when the Arguments also state a sandbox', async () => { @@ -315,7 +339,8 @@ describe('codex structured launch resolution', () => { expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'never', - sandbox: 'danger-full-access' + sandbox: 'danger-full-access', + approvalsReviewer: 'user' }) }) diff --git a/src/main/codex/codex-structured-permission-policy.test.ts b/src/main/codex/codex-structured-permission-policy.test.ts index 78389b8a3e4d..bb8824fe9a40 100644 --- a/src/main/codex/codex-structured-permission-policy.test.ts +++ b/src/main/codex/codex-structured-permission-policy.test.ts @@ -1,11 +1,15 @@ import { describe, expect, it } from 'vitest' import { codexStructuredPermissionPolicyForSettings } from './codex-structured-permission-policy' -const BYPASS = { approvalPolicy: 'never', sandbox: 'danger-full-access' } +const BYPASS = { approvalPolicy: 'never', sandbox: 'danger-full-access', approvalsReviewer: 'user' } // Approvals on, writes confined to the workspace. Verified against codex 0.153.4: both values are // accepted on thread/start and thread/resume, and the reply echoes them back as the effective // policy even when the home's config.toml asks for `never` / `danger-full-access`. -const MANUAL = { approvalPolicy: 'on-request', sandbox: 'workspace-write' } +const MANUAL = { + approvalPolicy: 'on-request', + sandbox: 'workspace-write', + approvalsReviewer: 'user' +} describe('codexStructuredPermissionPolicyForSettings', () => { it('bypasses when the user has never opened Agent settings', () => { @@ -68,20 +72,35 @@ describe('codexStructuredPermissionPolicyForSettings', () => { { agentDefaultArgs: { codex: '-s read-only' } }, { sandbox: 'read-only' } ) - ).toEqual({ approvalPolicy: 'on-request', sandbox: 'read-only' }) + ).toEqual({ ...MANUAL, sandbox: 'read-only' }) expect( codexStructuredPermissionPolicyForSettings( { agentDefaultArgs: { codex: '-a untrusted -s danger-full-access' } }, { approvalPolicy: 'untrusted', sandbox: 'danger-full-access' } ) - ).toEqual({ approvalPolicy: 'untrusted', sandbox: 'danger-full-access' }) + ).toEqual({ ...MANUAL, approvalPolicy: 'untrusted', sandbox: 'danger-full-access' }) + }) + + // A resume keeps the reviewer the thread last ran with unless the request names one, so + // dropping --approve-for-me has to say `user` out loud. + it('states the reviewer under Manual: the one the Arguments name, else Codex default', () => { + expect( + codexStructuredPermissionPolicyForSettings( + { agentDefaultArgs: { codex: '--approve-for-me' } }, + { approvalsReviewer: 'auto_review' } + ) + ).toEqual({ ...MANUAL, approvalsReviewer: 'auto_review' }) + expect( + codexStructuredPermissionPolicyForSettings({ agentDefaultArgs: { codex: '' } }) + .approvalsReviewer + ).toBe('user') }) it('keeps Yolo whatever else the Arguments state', () => { expect( codexStructuredPermissionPolicyForSettings( { agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox -s read-only' } }, - { sandbox: 'read-only' } + { sandbox: 'read-only', approvalsReviewer: 'auto_review' } ) ).toEqual(BYPASS) }) diff --git a/src/main/codex/codex-structured-permission-policy.ts b/src/main/codex/codex-structured-permission-policy.ts index 3e13fa9016bf..0f9758f769c1 100644 --- a/src/main/codex/codex-structured-permission-policy.ts +++ b/src/main/codex/codex-structured-permission-policy.ts @@ -1,17 +1,24 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import { resolvedTuiAgentArgsBypassPermissions } from '../../shared/tui-agent-launch-defaults' -// The values app-server's v2 thread params accept (`AskForApproval` / `SandboxMode`, kebab-case). +// The values app-server's v2 thread params accept (`AskForApproval` / `SandboxMode` / +// `ApprovalsReviewer`). export const CODEX_APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'] as const export const CODEX_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'] as const +export const CODEX_APPROVALS_REVIEWERS = ['user', 'auto_review'] as const export type CodexStructuredPermissionPolicy = { approvalPolicy: (typeof CODEX_APPROVAL_POLICIES)[number] sandbox: (typeof CODEX_SANDBOX_MODES)[number] + approvalsReviewer: (typeof CODEX_APPROVALS_REVIEWERS)[number] } /** Yolo: no approval prompts, no sandbox. */ -const BYPASS_POLICY = { approvalPolicy: 'never', sandbox: 'danger-full-access' } as const +const BYPASS_POLICY = { + approvalPolicy: 'never', + sandbox: 'danger-full-access', + approvalsReviewer: 'user' +} as const /** * Manual: approvals on, writes confined to the workspace. @@ -28,16 +35,23 @@ const BYPASS_POLICY = { approvalPolicy: 'never', sandbox: 'danger-full-access' } * Why `workspace-write` and not codex's built-in `read-only`: read-only would override a * deliberate `sandbox_mode = "workspace-write"` and make every file write in a Manual session * need an approval it did not need before. This still resets Yolo's `danger-full-access`. + * + * The reviewer is stated too, as Codex's default `user`: a resume otherwise keeps the reviewer + * the thread last ran with, e.g. `auto_review` after `--approve-for-me` was removed. */ -const MANUAL_POLICY = { approvalPolicy: 'on-request', sandbox: 'workspace-write' } as const +const MANUAL_POLICY = { + approvalPolicy: 'on-request', + sandbox: 'workspace-write', + approvalsReviewer: 'user' +} as const /** * The Agent Permissions setting as app-server thread policy. * * The posture is the one the toggle stores in the Arguments field; an untouched profile resolves - * to the default Orca ships, which is the bypass flag. Under Manual, a sandbox or approval policy - * the Arguments state explicitly (`requested`) replaces that field's default, as it does in a - * terminal; Yolo ignores it. + * to the default Orca ships, which is the bypass flag. Under Manual, a sandbox, approval policy or + * reviewer the Arguments state explicitly (`requested`) replaces that field's default, as it does + * in a terminal; Yolo ignores it. * * Always a policy, never `undefined`: both postures have to be said out loud, because the one * that goes unsaid is the one a resume silently inherits from the other. diff --git a/src/main/codex/codex-structured-thread-open.test.ts b/src/main/codex/codex-structured-thread-open.test.ts index efd32b666cd5..140285360706 100644 --- a/src/main/codex/codex-structured-thread-open.test.ts +++ b/src/main/codex/codex-structured-thread-open.test.ts @@ -22,7 +22,8 @@ describe('openCodexThread', () => { const connection = connectionFor(request) const permissionPolicy = { approvalPolicy: 'never' as const, - sandbox: 'danger-full-access' as const + sandbox: 'danger-full-access' as const, + approvalsReviewer: 'auto_review' as const } await openCodexThread( @@ -39,7 +40,12 @@ describe('openCodexThread', () => { expect(request).toHaveBeenNthCalledWith( 1, 'thread/start', - { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access' }, + { + cwd: '/workspace', + approvalPolicy: 'never', + sandbox: 'danger-full-access', + approvalsReviewer: 'auto_review' + }, { timeoutMs: 2_000 } ) expect(request).toHaveBeenNthCalledWith( @@ -50,6 +56,7 @@ describe('openCodexThread', () => { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access', + approvalsReviewer: 'auto_review', excludeTurns: true }, { timeoutMs: 2_000 } @@ -75,9 +82,15 @@ describe('openCodexThread', () => { ) const params = request.mock.calls[0]?.[1] ?? {} - expect(params).toMatchObject({ approvalPolicy: 'on-request', sandbox: 'workspace-write' }) + expect(params).toMatchObject({ + approvalPolicy: 'on-request', + sandbox: 'workspace-write', + approvalsReviewer: 'user' + }) // Absence is the bug, so assert the keys are carried, not merely that they are not Yolo. - expect(Object.keys(params)).toEqual(expect.arrayContaining(['approvalPolicy', 'sandbox'])) + expect(Object.keys(params)).toEqual( + expect.arrayContaining(['approvalPolicy', 'sandbox', 'approvalsReviewer']) + ) }) it('preserves an explicitly reported service tier, including Standard', async () => { diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts index 28a675ee83dd..b42e69778353 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-accept-then-deliver.test.ts @@ -336,7 +336,7 @@ describe('a start the chat needed and did not get', () => { const id = await accept('first') await eventually(async () => expect((await submission(id))?.dispatchState).toBe('rejected')) const sentence = - "Codex couldn't restart. Saved Arguments contain an unsupported option (--unsafe). Edit them in Settings > Agents > Arguments. Send your message to try again." + "Codex couldn't restart. Codex chats can't use --unsafe from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Send your message to try again." expect(await errorRows()).toEqual([sentence]) expect(await submission(id)).toMatchObject({ reason: sentence, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts index c49b79b06e64..a074e40e7e47 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-failure-text.test.ts @@ -39,7 +39,7 @@ describe('structuredAgentSessionStartFailure', () => { } as const expect(structuredAgentSessionStartFailure({ error: wrapped }, { agentName: 'Codex' })).toEqual({ reason: - "Codex couldn't start. Saved Arguments contain an unsupported option (--unknown). Edit them in Settings > Agents > Arguments. Send your message to try again.", + "Codex couldn't start. Codex chats can't use --unknown from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Send your message to try again.", rejection: { kind: 'startFailed', argumentProblem } }) const refusal = refuseUnclassified('agent_session_operation_invalid', 'generic start failure') @@ -47,7 +47,7 @@ describe('structuredAgentSessionStartFailure', () => { structuredAgentSessionStartFailure({ refusal, argumentProblem }, { agentName: 'Codex' }) ).toEqual({ reason: - "Codex couldn't restart. Saved Arguments contain an unsupported option (--unknown). Edit them in Settings > Agents > Arguments. Send your message to try again.", + "Codex couldn't restart. Codex chats can't use --unknown from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Send your message to try again.", rejection: { kind: 'restartFailed', argumentProblem, diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts index 7dfd624b496d..7c023335163c 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-pre-spawn-first-answer.test.ts @@ -122,7 +122,7 @@ describe('a create that fails before any process spawns', () => { ) ) const sentence = - "Claude couldn't start. Saved Arguments give --model more than one value. Edit them in Settings > Agents > Arguments. Send your message to try again." + "Claude couldn't start. Saved Arguments give --model more than one value. Fix it in Claude's Arguments in Settings → Agents. Send your message to try again." for (const result of [first, replay]) { expect(result).toMatchObject({ ok: false, diff --git a/src/main/native-chat/structured-agent-arguments-error.ts b/src/main/native-chat/structured-agent-arguments-error.ts index 05a2911d5083..0e86952dc4a3 100644 --- a/src/main/native-chat/structured-agent-arguments-error.ts +++ b/src/main/native-chat/structured-agent-arguments-error.ts @@ -1,5 +1,5 @@ import type { AgentSessionArgumentProblem } from '../../shared/agent-session-failure' -import { AGENT_SESSION_ARGUMENT_PROBLEM_WORD } from '../../shared/agent-session-argument-problem' +import { AGENT_SESSION_ARGUMENT_PROBLEM_WORDS } from '../../shared/agent-session-argument-problem' /** A saved Arguments refusal with only the option name, never its value. */ export class StructuredAgentArgumentsError extends Error { @@ -10,8 +10,10 @@ export class StructuredAgentArgumentsError extends Error { option: string, problem: AgentSessionArgumentProblem['problem'] ) { + const words = AGENT_SESSION_ARGUMENT_PROBLEM_WORDS[problem] + // A prompt or quote arrives as the user's own text, which is never carried. const normalizedOption = - AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] ?? + (words && (words.find((word) => word === option) ?? words[0])) ?? option.match(/^--[a-zA-Z][a-zA-Z0-9-]{0,63}/)?.[0] ?? option.match(/^-[a-zA-Z]/)?.[0] ?? '--?' diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index 4930174c7a3e..76f55148b182 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -40,12 +40,14 @@ describe('execution host structured launch settings wiring', () => { // Manual takes what the Arguments state; Yolo keeps bypassing. expect(deps?.resolveCodexPermissionPolicy?.({ sandbox: 'read-only' })).toEqual({ approvalPolicy: 'on-request', - sandbox: 'read-only' + sandbox: 'read-only', + approvalsReviewer: 'user' }) settings.agentDefaultArgs = { codex: '--dangerously-bypass-approvals-and-sandbox' } expect(deps?.resolveCodexPermissionPolicy?.({ sandbox: 'read-only' })).toEqual({ approvalPolicy: 'never', - sandbox: 'danger-full-access' + sandbox: 'danger-full-access', + approvalsReviewer: 'user' }) const notRunnable = expect.objectContaining({ reason: 'agentCommandNotRunnable' }) settings.agentCmdOverrides = { claude: 'wrapper --arg' } diff --git a/src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx b/src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx index 4be08aa86945..59b4f33b4953 100644 --- a/src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatStructuredSession.launch-lifecycle.test.tsx @@ -179,7 +179,7 @@ describe('NativeChatStructuredSession launch lifecycle', () => { expect( screen.getByText( - "Codex couldn't start. Saved Arguments contain an unsupported option (--remote). Edit them in Settings > Agents > Arguments." + "Codex couldn't start. Codex chats can't use --remote from saved Arguments. Remove it from Codex's Arguments in Settings → Agents." ) ).toBeTruthy() expect(screen.queryByText('Chat could not be started.')).toBeNull() diff --git a/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts b/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts index f9727b334a67..52f2b29654c6 100644 --- a/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts +++ b/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts @@ -9,7 +9,7 @@ import { type SavedArgumentsCopyId = Extract< AgentSessionFailureCopyId, - `arguments${string}` | 'editSavedArguments' + `arguments${string}` | 'removeFromSavedArguments' | 'editSavedArguments' > export const SAVED_ARGUMENTS_PIECES: Record< @@ -22,6 +22,12 @@ export const SAVED_ARGUMENTS_PIECES: Record< COPY.argumentsUnsupportedOption, values ), + argumentsUnnamedOption: (values) => + translate( + 'components.native-chat.failureWords.argumentsUnnamedOption', + COPY.argumentsUnnamedOption, + values + ), argumentsMissingValue: (values) => translate( 'components.native-chat.failureWords.argumentsMissingValue', @@ -40,6 +46,21 @@ export const SAVED_ARGUMENTS_PIECES: Record< COPY.argumentsInvalidValue, values ), + argumentsSandboxValues: () => + translate( + 'components.native-chat.failureWords.argumentsSandboxValues', + COPY.argumentsSandboxValues + ), + argumentsApprovalValues: () => + translate( + 'components.native-chat.failureWords.argumentsApprovalValues', + COPY.argumentsApprovalValues + ), + argumentsReviewerValues: () => + translate( + 'components.native-chat.failureWords.argumentsReviewerValues', + COPY.argumentsReviewerValues + ), argumentsPositionalPrompt: () => translate( 'components.native-chat.failureWords.argumentsPositionalPrompt', @@ -50,6 +71,33 @@ export const SAVED_ARGUMENTS_PIECES: Record< 'components.native-chat.failureWords.argumentsUnclosedQuote', COPY.argumentsUnclosedQuote ), - editSavedArguments: () => - translate('components.native-chat.failureWords.editSavedArguments', COPY.editSavedArguments) + argumentsProfileHint: () => + translate( + 'components.native-chat.failureWords.argumentsProfileHint', + COPY.argumentsProfileHint + ), + argumentsProviderHint: () => + translate( + 'components.native-chat.failureWords.argumentsProviderHint', + COPY.argumentsProviderHint + ), + argumentsWorkspaceHint: () => + translate( + 'components.native-chat.failureWords.argumentsWorkspaceHint', + COPY.argumentsWorkspaceHint + ), + argumentsImageHint: () => + translate('components.native-chat.failureWords.argumentsImageHint', COPY.argumentsImageHint), + removeFromSavedArguments: (values) => + translate( + 'components.native-chat.failureWords.removeFromSavedArguments', + COPY.removeFromSavedArguments, + values + ), + editSavedArguments: (values) => + translate( + 'components.native-chat.failureWords.editSavedArguments', + COPY.editSavedArguments, + values + ) } diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index aec1a1cdc8a2..47552771f922 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2762,7 +2762,16 @@ "argumentsInvalidValue": "Saved Arguments give {{option}} a value that isn't supported.", "argumentsUnclosedQuote": "Saved Arguments have a quote that isn't closed.", "argumentsPositionalPrompt": "Saved Arguments include a prompt.", - "argumentsUnsupportedOption": "Saved Arguments contain an unsupported option ({{option}}).", + "argumentsUnsupportedOption": "{{agent}} chats can't use {{option}} from saved Arguments.", + "argumentsUnnamedOption": "{{agent}} chats can't use one of the options in saved Arguments.", + "argumentsSandboxValues": "The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access.", + "argumentsApprovalValues": "The approval policy in saved Arguments must be untrusted, on-request or never.", + "argumentsReviewerValues": "The approvals reviewer in saved Arguments must be user or auto_review.", + "argumentsProfileHint": "To keep the profile's settings, put them in config.toml or pass them with -c.", + "argumentsProviderHint": "Choose the provider with -c model_provider=… instead.", + "argumentsWorkspaceHint": "A chat already works in its workspace folder.", + "argumentsImageHint": "Attach the image in the chat instead.", + "removeFromSavedArguments": "Remove it from {{agent}}'s Arguments in Settings → Agents.", "attachmentAtMost": "{{agent}} accepts at most {{limit}} images in one message, so this message was not sent.", "attachmentEmpty": "An image on this message is empty, so the message was not sent.", "attachmentInvalid": "An attachment on this message can't be sent to the agent.", @@ -2788,7 +2797,7 @@ "compactionUnconfirmed": "Compaction completion is unconfirmed.", "couldNotRestart": "{{agent}} couldn't restart.", "couldNotStart": "{{agent}} couldn't start.", - "editSavedArguments": "Edit them in Settings > Agents > Arguments.", + "editSavedArguments": "Fix it in {{agent}}'s Arguments in Settings → Agents.", "emptyMessage": "This message is empty, so it was not sent.", "historyTooLarge": "This conversation's history is too large to restore here.", "hostFault": "Orca ran into a problem, so this didn't go through.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 4a75d3c48895..491030259a76 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17846,13 +17846,22 @@ "providerRetryNumberOf": "Retry {{attempt}} of {{maxRetries}}.", "providerRetryLastError": "Last error: {{detail}}.", "previousExitUnverifiable": "Couldn't stop {{agent}} from before.", - "argumentsUnsupportedOption": "Saved Arguments contain an unsupported option ({{option}}).", + "argumentsUnsupportedOption": "{{agent}} chats can't use {{option}} from saved Arguments.", + "argumentsUnnamedOption": "{{agent}} chats can't use one of the options in saved Arguments.", + "argumentsSandboxValues": "The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access.", + "argumentsApprovalValues": "The approval policy in saved Arguments must be untrusted, on-request or never.", + "argumentsReviewerValues": "The approvals reviewer in saved Arguments must be user or auto_review.", + "argumentsProfileHint": "To keep the profile's settings, put them in config.toml or pass them with -c.", + "argumentsProviderHint": "Choose the provider with -c model_provider=… instead.", + "argumentsWorkspaceHint": "A chat already works in its workspace folder.", + "argumentsImageHint": "Attach the image in the chat instead.", + "removeFromSavedArguments": "Remove it from {{agent}}'s Arguments in Settings → Agents.", "argumentsMissingValue": "Saved Arguments need a value for {{option}}.", "argumentsMultipleValues": "Saved Arguments give {{option}} more than one value.", "argumentsInvalidValue": "Saved Arguments give {{option}} a value that isn't supported.", "argumentsUnclosedQuote": "Saved Arguments have a quote that isn't closed.", "argumentsPositionalPrompt": "Saved Arguments include a prompt.", - "editSavedArguments": "Edit them in Settings > Agents > Arguments." + "editSavedArguments": "Fix it in {{agent}}'s Arguments in Settings → Agents." }, "writeNotice": { "notDoneReadHistory": "This chat's history couldn't be loaded.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index f12e84aa1785..f11a53647480 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -17562,7 +17562,7 @@ "sendToTryAgain": "Envíe su mensaje para volver a intentarlo.", "sendAgainToTryOnceMore": "Vuelva a enviar su mensaje para intentarlo de nuevo.", "couldNotStart": "{{agent}} no pudo iniciarse.", - "editSavedArguments": "Edítalos en Ajustes > Agentes > Argumentos.", + "editSavedArguments": "Corríjalo en los argumentos de {{agent}} en Ajustes → Agentes.", "couldNotRestart": "{{agent}} no pudo reiniciarse.", "notSignedIn": "{{agent}} no tiene la sesión iniciada con la cuenta seleccionada.", "signInFirst": "Inicie sesión primero.", @@ -17616,7 +17616,16 @@ "argumentsInvalidValue": "Los argumentos guardados asignan a {{option}} un valor no compatible.", "argumentsUnclosedQuote": "Los argumentos guardados tienen una comilla sin cerrar.", "argumentsPositionalPrompt": "Los argumentos guardados incluyen un mensaje.", - "argumentsUnsupportedOption": "Los argumentos guardados contienen una opción no compatible ({{option}}).", + "argumentsUnsupportedOption": "Los chats de {{agent}} no pueden usar {{option}} de los argumentos guardados.", + "argumentsUnnamedOption": "Los chats de {{agent}} no pueden usar una de las opciones de los argumentos guardados.", + "argumentsSandboxValues": "El sandbox de los argumentos guardados debe ser read-only, workspace-write o danger-full-access.", + "argumentsApprovalValues": "La política de aprobación de los argumentos guardados debe ser untrusted, on-request o never.", + "argumentsReviewerValues": "El revisor de aprobaciones de los argumentos guardados debe ser user o auto_review.", + "argumentsProfileHint": "Para conservar los ajustes del perfil, póngalos en config.toml o páselos con -c.", + "argumentsProviderHint": "Elija el proveedor con -c model_provider=… en su lugar.", + "argumentsWorkspaceHint": "Un chat ya trabaja en la carpeta de su espacio de trabajo.", + "argumentsImageHint": "En su lugar, adjunte la imagen en el chat.", + "removeFromSavedArguments": "Quítelo de los argumentos de {{agent}} en Ajustes → Agentes.", "hostFault": "Orca tuvo un problema, así que esto no se completó.", "hostFaultTryAgain": "Orca tuvo un problema, así que esto no se completó. Vuelva a intentarlo.", "hostStopped": "{{agent}} no terminó de iniciarse, así que Orca lo detuvo.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 9b02e6e58d5e..85c6b1f418e8 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -17663,7 +17663,7 @@ "sendToTryAgain": "Envoyez votre message pour réessayer.", "sendAgainToTryOnceMore": "Renvoyez votre message pour réessayer.", "couldNotStart": "{{agent}} n'a pas pu démarrer.", - "editSavedArguments": "Modifiez-les dans Paramètres > Agents > Arguments.", + "editSavedArguments": "Corrigez-le dans les arguments de {{agent}} dans Paramètres → Agents.", "couldNotRestart": "{{agent}} n'a pas pu redémarrer.", "notSignedIn": "{{agent}} n'est pas connecté avec le compte sélectionné.", "signInFirst": "Connectez-vous d'abord.", @@ -17717,7 +17717,16 @@ "argumentsInvalidValue": "Les arguments enregistrés donnent à {{option}} une valeur non prise en charge.", "argumentsUnclosedQuote": "Les arguments enregistrés contiennent un guillemet non fermé.", "argumentsPositionalPrompt": "Les arguments enregistrés contiennent un message.", - "argumentsUnsupportedOption": "Les arguments enregistrés contiennent une option non prise en charge ({{option}}).", + "argumentsUnsupportedOption": "Les chats {{agent}} ne peuvent pas utiliser {{option}} depuis les arguments enregistrés.", + "argumentsUnnamedOption": "Les chats {{agent}} ne peuvent pas utiliser l'une des options des arguments enregistrés.", + "argumentsSandboxValues": "Le sandbox des arguments enregistrés doit être read-only, workspace-write ou danger-full-access.", + "argumentsApprovalValues": "La politique d'approbation des arguments enregistrés doit être untrusted, on-request ou never.", + "argumentsReviewerValues": "Le réviseur des approbations des arguments enregistrés doit être user ou auto_review.", + "argumentsProfileHint": "Pour conserver les réglages du profil, placez-les dans config.toml ou passez-les avec -c.", + "argumentsProviderHint": "Choisissez le fournisseur avec -c model_provider=… à la place.", + "argumentsWorkspaceHint": "Un chat travaille déjà dans le dossier de son espace de travail.", + "argumentsImageHint": "Joignez plutôt l'image dans le chat.", + "removeFromSavedArguments": "Retirez-le des arguments de {{agent}} dans Paramètres → Agents.", "hostFault": "Orca a rencontré un problème, l'opération n'a donc pas abouti.", "hostFaultTryAgain": "Orca a rencontré un problème, l'opération n'a donc pas abouti. Réessayez.", "hostStopped": "{{agent}} n'a jamais fini de démarrer, Orca l'a donc arrêté.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 6ec5f6b20c31..a662bb858df9 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -17597,7 +17597,7 @@ "sendToTryAgain": "もう一度試すには、メッセージを送信してください。", "sendAgainToTryOnceMore": "メッセージをもう一度送信して、再度お試しください。", "couldNotStart": "{{agent}} を起動できませんでした。", - "editSavedArguments": "設定 > Agent > 引数で編集してください。", + "editSavedArguments": "[設定] → [Agent] の {{agent}} の引数で修正してください。", "couldNotRestart": "{{agent}} を再起動できませんでした。", "notSignedIn": "{{agent}} は選択したアカウントでサインインしていません。", "signInFirst": "先にサインインしてください。", @@ -17651,7 +17651,16 @@ "argumentsInvalidValue": "保存された引数の {{option}} に未対応の値が指定されています。", "argumentsUnclosedQuote": "保存された引数に閉じられていない引用符があります。", "argumentsPositionalPrompt": "保存された引数にプロンプトが含まれています。", - "argumentsUnsupportedOption": "保存された引数に未対応のオプション({{option}})が含まれています。", + "argumentsUnsupportedOption": "{{agent}} のチャットでは、保存された引数の {{option}} を使用できません。", + "argumentsUnnamedOption": "{{agent}} のチャットでは、保存された引数のオプションの 1 つを使用できません。", + "argumentsSandboxValues": "保存された引数のサンドボックスは read-only、workspace-write、danger-full-access のいずれかである必要があります。", + "argumentsApprovalValues": "保存された引数の承認ポリシーは untrusted、on-request、never のいずれかである必要があります。", + "argumentsReviewerValues": "保存された引数の承認レビュアーは user または auto_review である必要があります。", + "argumentsProfileHint": "プロファイルの設定を使い続けるには、config.toml に記述するか -c で指定してください。", + "argumentsProviderHint": "代わりに -c model_provider=… でプロバイダーを選択してください。", + "argumentsWorkspaceHint": "チャットはすでにワークスペースフォルダーで動作しています。", + "argumentsImageHint": "代わりにチャットで画像を添付してください。", + "removeFromSavedArguments": "[設定] → [Agent] の {{agent}} の引数から削除してください。", "hostFault": "Orca で問題が発生したため、この操作は完了しませんでした。", "hostFaultTryAgain": "Orca で問題が発生したため、この操作は完了しませんでした。もう一度お試しください。", "hostStopped": "{{agent}} の起動が完了しなかったため、Orca が停止しました。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 731da0f008b2..2e3d3c4b93af 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -17597,7 +17597,7 @@ "sendToTryAgain": "다시 시도하려면 메시지를 보내세요.", "sendAgainToTryOnceMore": "메시지를 다시 보내 한 번 더 시도하세요.", "couldNotStart": "{{agent}}을(를) 시작하지 못했습니다.", - "editSavedArguments": "설정 > 에이전트 > 인수에서 수정하세요.", + "editSavedArguments": "설정 → 에이전트의 {{agent}} 인수에서 수정하세요.", "couldNotRestart": "{{agent}}을(를) 다시 시작하지 못했습니다.", "notSignedIn": "{{agent}}이(가) 선택한 계정으로 로그인되어 있지 않습니다.", "signInFirst": "먼저 로그인하세요.", @@ -17651,7 +17651,16 @@ "argumentsInvalidValue": "저장된 인수에 {{option}}의 지원하지 않는 값이 지정되어 있습니다.", "argumentsUnclosedQuote": "저장된 인수에 닫히지 않은 따옴표가 있습니다.", "argumentsPositionalPrompt": "저장된 인수에 프롬프트가 포함되어 있습니다.", - "argumentsUnsupportedOption": "저장된 인수에 지원하지 않는 옵션({{option}})이 포함되어 있습니다.", + "argumentsUnsupportedOption": "{{agent}} 채팅에서는 저장된 인수의 {{option}}을(를) 사용할 수 없습니다.", + "argumentsUnnamedOption": "{{agent}} 채팅에서는 저장된 인수의 옵션 중 하나를 사용할 수 없습니다.", + "argumentsSandboxValues": "저장된 인수의 샌드박스는 read-only, workspace-write, danger-full-access 중 하나여야 합니다.", + "argumentsApprovalValues": "저장된 인수의 승인 정책은 untrusted, on-request, never 중 하나여야 합니다.", + "argumentsReviewerValues": "저장된 인수의 승인 검토자는 user 또는 auto_review여야 합니다.", + "argumentsProfileHint": "프로필 설정을 유지하려면 config.toml에 넣거나 -c로 전달하세요.", + "argumentsProviderHint": "대신 -c model_provider=…로 제공자를 선택하세요.", + "argumentsWorkspaceHint": "채팅은 이미 워크스페이스 폴더에서 작동합니다.", + "argumentsImageHint": "대신 채팅에서 이미지를 첨부하세요.", + "removeFromSavedArguments": "설정 → 에이전트의 {{agent}} 인수에서 제거하세요.", "hostFault": "Orca에 문제가 발생하여 이 작업이 완료되지 않았습니다.", "hostFaultTryAgain": "Orca에 문제가 발생하여 이 작업이 완료되지 않았습니다. 다시 시도하세요.", "hostStopped": "{{agent}}이(가) 시작을 마치지 못해 Orca가 중지했습니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index c8911095166d..8d0cf980e703 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -17562,7 +17562,7 @@ "sendToTryAgain": "请发送您的消息以重试。", "sendAgainToTryOnceMore": "请重新发送您的消息,再试一次。", "couldNotStart": "{{agent}} 无法启动。", - "editSavedArguments": "请在设置 > 智能体 > 参数中编辑。", + "editSavedArguments": "请在设置 → 智能体中修正 {{agent}} 的参数。", "couldNotRestart": "{{agent}} 无法重新启动。", "notSignedIn": "{{agent}} 未使用所选账户登录。", "signInFirst": "请先登录。", @@ -17616,7 +17616,16 @@ "argumentsInvalidValue": "已保存的参数为 {{option}} 指定了不支持的值。", "argumentsUnclosedQuote": "已保存的参数中有未闭合的引号。", "argumentsPositionalPrompt": "已保存的参数中包含提示词。", - "argumentsUnsupportedOption": "已保存的参数包含不支持的选项({{option}})。", + "argumentsUnsupportedOption": "{{agent}} 聊天无法使用已保存参数中的 {{option}}。", + "argumentsUnnamedOption": "{{agent}} 聊天无法使用已保存参数中的某个选项。", + "argumentsSandboxValues": "已保存参数中的沙盒必须是 read-only、workspace-write 或 danger-full-access。", + "argumentsApprovalValues": "已保存参数中的审批策略必须是 untrusted、on-request 或 never。", + "argumentsReviewerValues": "已保存参数中的审批审核者必须是 user 或 auto_review。", + "argumentsProfileHint": "如需保留该配置档的设置,请将其写入 config.toml 或通过 -c 传入。", + "argumentsProviderHint": "请改用 -c model_provider=… 选择提供方。", + "argumentsWorkspaceHint": "聊天已在其工作区文件夹中运行。", + "argumentsImageHint": "请改为在聊天中附加图片。", + "removeFromSavedArguments": "请在设置 → 智能体中从 {{agent}} 的参数里删除它。", "hostFault": "Orca 遇到问题,因此此操作未完成。", "hostFaultTryAgain": "Orca 遇到问题,因此此操作未完成。请重试。", "hostStopped": "{{agent}} 始终未完成启动,因此 Orca 已将其停止。", diff --git a/src/shared/agent-session-argument-problem-words.ts b/src/shared/agent-session-argument-problem-words.ts new file mode 100644 index 000000000000..d6582178e001 --- /dev/null +++ b/src/shared/agent-session-argument-problem-words.ts @@ -0,0 +1,74 @@ +// The sentences after "{agent} couldn't start." for a saved Arguments refusal: what is wrong, where +// to change it, and for some options what a chat honors instead. Chosen from the option name alone, +// which every client already receives. + +import { + CODEX_ARGUMENT_SETTINGS, + type AgentSessionArgumentProblem, + type CodexArgumentSetting +} from './agent-session-argument-problem' +import type { + AgentSessionFailureCopyId, + AgentSessionFailureSay +} from './agent-session-failure-copy' + +const PROBLEM_COPY = { + unsupportedOption: 'argumentsUnsupportedOption', + missingValue: 'argumentsMissingValue', + multipleValues: 'argumentsMultipleValues', + invalidValue: 'argumentsInvalidValue', + positionalPrompt: 'argumentsPositionalPrompt', + unclosedQuote: 'argumentsUnclosedQuote' +} as const satisfies Record + +const SETTING_VALUES_COPY = { + sandbox_mode: 'argumentsSandboxValues', + approval_policy: 'argumentsApprovalValues', + approvals_reviewer: 'argumentsReviewerValues' +} as const satisfies Record + +/** Codex options a chat refuses that have an equivalent, or no need, in a chat. */ +const CODEX_OPTION_HINTS = new Map([ + ['--profile', 'argumentsProfileHint'], + ['-p', 'argumentsProfileHint'], + ['--oss', 'argumentsProviderHint'], + ['--local-provider', 'argumentsProviderHint'], + ['-C', 'argumentsWorkspaceHint'], + ['--cd', 'argumentsWorkspaceHint'], + ['--add-dir', 'argumentsWorkspaceHint'], + ['--worktree', 'argumentsWorkspaceHint'], + ['-i', 'argumentsImageHint'], + ['--image', 'argumentsImageHint'] +]) + +function problemCopy({ option, problem }: AgentSessionArgumentProblem): AgentSessionFailureCopyId { + if (problem === 'unsupportedOption' && option === '--?') { + return 'argumentsUnnamedOption' + } + if (problem === 'invalidValue') { + const setting = CODEX_ARGUMENT_SETTINGS.find((candidate) => candidate === option) + return setting ? SETTING_VALUES_COPY[setting] : PROBLEM_COPY.invalidValue + } + return PROBLEM_COPY[problem] +} + +export function agentSessionArgumentProblemSentences( + argumentProblem: AgentSessionArgumentProblem, + say: AgentSessionFailureSay +): string[] { + const { agent, option, problem } = argumentProblem + const hint = + agent === 'Codex' && problem === 'unsupportedOption' + ? CODEX_OPTION_HINTS.get(option) + : undefined + return [ + say(problemCopy(argumentProblem), { agent, option }), + say( + problem === 'unsupportedOption' || problem === 'positionalPrompt' + ? 'removeFromSavedArguments' + : 'editSavedArguments', + { agent } + ), + ...(hint ? [say(hint)] : []) + ] +} diff --git a/src/shared/agent-session-argument-problem.ts b/src/shared/agent-session-argument-problem.ts index 790682aa87ad..c5151db2bb38 100644 --- a/src/shared/agent-session-argument-problem.ts +++ b/src/shared/agent-session-argument-problem.ts @@ -11,10 +11,23 @@ export type AgentSessionArgumentProblem = { | 'unclosedQuote' } -/** Problems that are about no single option carry this fixed word as their option. */ -export const AGENT_SESSION_ARGUMENT_PROBLEM_WORD: Partial< - Record -> = { positionalPrompt: 'prompt', unclosedQuote: 'quote' } +/** The Codex settings a saved value can be invalid for, by their config key. */ +export const CODEX_ARGUMENT_SETTINGS = [ + 'sandbox_mode', + 'approval_policy', + 'approvals_reviewer' +] as const +export type CodexArgumentSetting = (typeof CODEX_ARGUMENT_SETTINGS)[number] + +/** Problems that aren't about the option as typed carry one of these fixed words as their option: + * a value names the setting it is for, however the Arguments spelled it. */ +export const AGENT_SESSION_ARGUMENT_PROBLEM_WORDS: Partial< + Record +> = { + positionalPrompt: ['prompt'], + unclosedQuote: ['quote'], + invalidValue: CODEX_ARGUMENT_SETTINGS +} export function readAgentSessionArgumentProblem( value: unknown @@ -41,9 +54,10 @@ export function readAgentSessionArgumentProblem( problem !== 'positionalPrompt' && problem !== 'unclosedQuote') || typeof option !== 'string' || - (AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] !== undefined - ? option !== AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] - : !/^(?:--[a-zA-Z][a-zA-Z0-9-]{0,63}|-[a-zA-Z]|--\?)$/.test(option)) + !( + AGENT_SESSION_ARGUMENT_PROBLEM_WORDS[problem]?.includes(option) ?? + /^(?:--[a-zA-Z][a-zA-Z0-9-]{0,63}|-[a-zA-Z]|--\?)$/.test(option) + ) ) { return undefined } diff --git a/src/shared/agent-session-failure-copy.ts b/src/shared/agent-session-failure-copy.ts index 8bdbccd7f088..80dc54a03c9d 100644 --- a/src/shared/agent-session-failure-copy.ts +++ b/src/shared/agent-session-failure-copy.ts @@ -17,13 +17,25 @@ export const AGENT_SESSION_FAILURE_COPY = { sendAgainToTryOnceMore: 'Send your message again to try once more.', couldNotStart: "{{agent}} couldn't start.", couldNotRestart: "{{agent}} couldn't restart.", - argumentsUnsupportedOption: 'Saved Arguments contain an unsupported option ({{option}}).', + argumentsUnsupportedOption: "{{agent}} chats can't use {{option}} from saved Arguments.", + argumentsUnnamedOption: "{{agent}} chats can't use one of the options in saved Arguments.", argumentsMissingValue: 'Saved Arguments need a value for {{option}}.', argumentsMultipleValues: 'Saved Arguments give {{option}} more than one value.', argumentsInvalidValue: "Saved Arguments give {{option}} a value that isn't supported.", + argumentsSandboxValues: + 'The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access.', + argumentsApprovalValues: + 'The approval policy in saved Arguments must be untrusted, on-request or never.', + argumentsReviewerValues: 'The approvals reviewer in saved Arguments must be user or auto_review.', argumentsPositionalPrompt: 'Saved Arguments include a prompt.', argumentsUnclosedQuote: "Saved Arguments have a quote that isn't closed.", - editSavedArguments: 'Edit them in Settings > Agents > Arguments.', + argumentsProfileHint: + "To keep the profile's settings, put them in config.toml or pass them with -c.", + argumentsProviderHint: 'Choose the provider with -c model_provider=… instead.', + argumentsWorkspaceHint: 'A chat already works in its workspace folder.', + argumentsImageHint: 'Attach the image in the chat instead.', + removeFromSavedArguments: "Remove it from {{agent}}'s Arguments in Settings → Agents.", + editSavedArguments: "Fix it in {{agent}}'s Arguments in Settings → Agents.", terminalAgentHoldsChat: TERMINAL_AGENT_HOLDS_CHAT, quitTerminalAgent: QUIT_TERMINAL_AGENT, startNewChat: START_NEW_CHAT, diff --git a/src/shared/agent-session-failure-words.ts b/src/shared/agent-session-failure-words.ts index a6b307c526e2..27ca2d481575 100644 --- a/src/shared/agent-session-failure-words.ts +++ b/src/shared/agent-session-failure-words.ts @@ -18,6 +18,7 @@ import { type SubmissionRejectionKind } from './agent-session-failure' import type { AgentSessionConversationCommand } from './agent-session-conversation-command' +import { agentSessionArgumentProblemSentences } from './agent-session-argument-problem-words' import { sayAgentSessionFailureEnglish, type AgentSessionFailureCopyId, @@ -139,18 +140,9 @@ function couldNot(verb: 'couldNotStart' | 'couldNotRestart'): Sentence { return (context, fact, _surface, say) => { const failed = say(verb, agent(say, context)) if (fact.argumentProblem) { - const problemCopy = { - unsupportedOption: 'argumentsUnsupportedOption', - missingValue: 'argumentsMissingValue', - multipleValues: 'argumentsMultipleValues', - invalidValue: 'argumentsInvalidValue', - positionalPrompt: 'argumentsPositionalPrompt', - unclosedQuote: 'argumentsUnclosedQuote' - } as const return joinSentences([ failed, - say(problemCopy[fact.argumentProblem.problem], { option: fact.argumentProblem.option }), - say('editSavedArguments'), + ...agentSessionArgumentProblemSentences(fact.argumentProblem, say), ...startRetry(say, context) ]) } diff --git a/src/shared/agent-session-refusal-notice.test.ts b/src/shared/agent-session-refusal-notice.test.ts index 8c8f8510716f..9eef25017de2 100644 --- a/src/shared/agent-session-refusal-notice.test.ts +++ b/src/shared/agent-session-refusal-notice.test.ts @@ -737,18 +737,50 @@ describe('saved Arguments refusals', () => { throw new Error('saved refusal was not read') } expect(agentSessionWriteNoticeEnglish(agentSessionRefusalCauseParts(parsed))).toBe( - "Codex couldn't start. Saved Arguments contain an unsupported option (--remote). Edit them in Settings > Agents > Arguments." + "Codex couldn't start. Codex chats can't use --remote from saved Arguments. Remove it from Codex's Arguments in Settings → Agents." ) }) it.each([ [ - { agent: 'Codex', option: '-s', problem: 'invalidValue' }, - "Codex couldn't start. Saved Arguments give -s a value that isn't supported. Edit them in Settings > Agents > Arguments." + { agent: 'Codex', option: '--profile', problem: 'unsupportedOption' }, + "Codex couldn't start. Codex chats can't use --profile from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. To keep the profile's settings, put them in config.toml or pass them with -c." + ], + [ + { agent: 'Codex', option: '--oss', problem: 'unsupportedOption' }, + "Codex couldn't start. Codex chats can't use --oss from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Choose the provider with -c model_provider=… instead." + ], + [ + { agent: 'Codex', option: '-C', problem: 'unsupportedOption' }, + "Codex couldn't start. Codex chats can't use -C from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. A chat already works in its workspace folder." + ], + [ + { agent: 'Codex', option: '-i', problem: 'unsupportedOption' }, + "Codex couldn't start. Codex chats can't use -i from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Attach the image in the chat instead." + ], + [ + { agent: 'Codex', option: '--?', problem: 'unsupportedOption' }, + "Codex couldn't start. Codex chats can't use one of the options in saved Arguments. Remove it from Codex's Arguments in Settings → Agents." + ], + [ + { agent: 'Codex', option: 'prompt', problem: 'positionalPrompt' }, + "Codex couldn't start. Saved Arguments include a prompt. Remove it from Codex's Arguments in Settings → Agents." + ], + [ + { agent: 'Codex', option: 'sandbox_mode', problem: 'invalidValue' }, + "Codex couldn't start. The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access. Fix it in Codex's Arguments in Settings → Agents." + ], + [ + { agent: 'Codex', option: 'approval_policy', problem: 'invalidValue' }, + "Codex couldn't start. The approval policy in saved Arguments must be untrusted, on-request or never. Fix it in Codex's Arguments in Settings → Agents." + ], + [ + { agent: 'Codex', option: 'approvals_reviewer', problem: 'invalidValue' }, + "Codex couldn't start. The approvals reviewer in saved Arguments must be user or auto_review. Fix it in Codex's Arguments in Settings → Agents." ], [ { agent: 'Claude', option: 'quote', problem: 'unclosedQuote' }, - "Claude couldn't start. Saved Arguments have a quote that isn't closed. Edit them in Settings > Agents > Arguments." + "Claude couldn't start. Saved Arguments have a quote that isn't closed. Fix it in Claude's Arguments in Settings → Agents." ] ] as const)('names the %j problem and where to fix it', (argumentProblem, sentence) => { const parsed = parseAgentSessionWriteFailure({ @@ -765,7 +797,9 @@ describe('saved Arguments refusals', () => { it.each([ { agent: 'Codex', option: '--remote', problem: 'futureProblem' }, { agent: 'Codex', option: '--remote=private', problem: 'unsupportedOption' }, - { agent: 'Codex', option: '--private', problem: 'unclosedQuote' } + { agent: 'Codex', option: '--private', problem: 'unclosedQuote' }, + { agent: 'Codex', option: '-s', problem: 'invalidValue' }, + { agent: 'Codex', option: 'private_key', problem: 'invalidValue' } ])('ignores an unrecognized or unsafe argument detail', (argumentProblem) => { const refused = parseAgentSessionWriteFailure({ kind: 'refused', From 13ed63f13bdbfdfe7a9f68d505c265bb69c72d9f Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:41:51 -0700 Subject: [PATCH 3/6] fix(native-chat): send the Codex reviewer as spelled and name a local model in the --oss hint The chat translated -c approvals_reviewer=guardian_subagent to auto_review before stating it on the thread. Codex 0.120-0.123 take only guardian_subagent there, so on those versions the chat failed to start where the terminal worked. The reviewer now goes to Codex exactly as the Arguments spell it (user, auto_review or guardian_subagent); every version accepts the spelling its own config accepts. --oss also picks a local default model, so the hint for --oss and --local-provider now says to choose a local model with -m as well. --- .../codex/codex-structured-launch-args.test.ts | 18 +++++++++++++----- src/main/codex/codex-structured-launch-args.ts | 8 ++------ .../codex-structured-permission-policy.ts | 5 +++-- .../codex/codex-structured-thread-open.test.ts | 6 +++--- src/renderer/src/i18n/en-runtime-required.json | 2 +- src/renderer/src/i18n/locales/en.json | 2 +- src/renderer/src/i18n/locales/es.json | 2 +- src/renderer/src/i18n/locales/fr.json | 2 +- src/renderer/src/i18n/locales/ja.json | 2 +- src/renderer/src/i18n/locales/ko.json | 2 +- src/renderer/src/i18n/locales/zh.json | 2 +- src/shared/agent-session-failure-copy.ts | 3 ++- .../agent-session-refusal-notice.test.ts | 2 +- 13 files changed, 31 insertions(+), 25 deletions(-) diff --git a/src/main/codex/codex-structured-launch-args.test.ts b/src/main/codex/codex-structured-launch-args.test.ts index 4e8e04419010..b3b8f4d1aacb 100644 --- a/src/main/codex/codex-structured-launch-args.test.ts +++ b/src/main/codex/codex-structured-launch-args.test.ts @@ -63,16 +63,24 @@ describe('codexStructuredLaunchArgs', () => { }) }) - it("reads Codex's own aliases as the values they stand for", () => { - expect( - codexStructuredLaunchArgs(['-c', 'approvals_reviewer=guardian_subagent', '-a', 'on-failure']) - .permissions - ).toEqual({ approvalsReviewer: 'auto_review', approvalPolicy: 'on-request' }) + it("reads on-failure as Codex's own on-request alias", () => { + expect(codexStructuredLaunchArgs(['-a', 'on-failure']).permissions).toEqual({ + approvalPolicy: 'on-request' + }) expect(codexStructuredLaunchArgs(['-c', 'approval_policy="on-failure"']).permissions).toEqual({ approvalPolicy: 'on-request' }) }) + // Codex before 0.124 takes only `guardian_subagent` on the thread; later versions take either. + it('keeps the reviewer as spelled', () => { + for (const reviewer of ['user', 'auto_review', 'guardian_subagent'] as const) { + expect( + codexStructuredLaunchArgs(['-c', `approvals_reviewer="${reviewer}"`]).permissions + ).toEqual({ approvalsReviewer: reviewer }) + } + }) + it('reads the sandbox and approval the Arguments state, flags over config', () => { expect(codexStructuredLaunchArgs(['-s', 'read-only', '-a', 'untrusted'])).toEqual({ args: [], diff --git a/src/main/codex/codex-structured-launch-args.ts b/src/main/codex/codex-structured-launch-args.ts index c6e5e0fd06fc..a952e9ce6f10 100644 --- a/src/main/codex/codex-structured-launch-args.ts +++ b/src/main/codex/codex-structured-launch-args.ts @@ -113,12 +113,8 @@ const RULES: Record = { '--dangerously-bypass-hook-trust': 'refuse' } -// Codex's own aliases: `on-failure` reads as `on-request` from 0.143; `guardian_subagent` is the -// reviewer's old name. -const VALUE_ALIASES = new Map([ - ['on-failure', 'on-request'], - ['guardian_subagent', 'auto_review'] -]) +// Codex reads `on-failure` as `on-request` from 0.143, when its thread params stopped taking it. +const VALUE_ALIASES = new Map([['on-failure', 'on-request']]) function oneOf( values: readonly T[], diff --git a/src/main/codex/codex-structured-permission-policy.ts b/src/main/codex/codex-structured-permission-policy.ts index 0f9758f769c1..bf6b8e947031 100644 --- a/src/main/codex/codex-structured-permission-policy.ts +++ b/src/main/codex/codex-structured-permission-policy.ts @@ -2,10 +2,11 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import { resolvedTuiAgentArgsBypassPermissions } from '../../shared/tui-agent-launch-defaults' // The values app-server's v2 thread params accept (`AskForApproval` / `SandboxMode` / -// `ApprovalsReviewer`). +// `ApprovalsReviewer`). The reviewer goes as spelled: Codex before 0.124 takes only +// `guardian_subagent`, later versions take either name. export const CODEX_APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'] as const export const CODEX_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'] as const -export const CODEX_APPROVALS_REVIEWERS = ['user', 'auto_review'] as const +export const CODEX_APPROVALS_REVIEWERS = ['user', 'auto_review', 'guardian_subagent'] as const export type CodexStructuredPermissionPolicy = { approvalPolicy: (typeof CODEX_APPROVAL_POLICIES)[number] diff --git a/src/main/codex/codex-structured-thread-open.test.ts b/src/main/codex/codex-structured-thread-open.test.ts index 140285360706..dc284a059d67 100644 --- a/src/main/codex/codex-structured-thread-open.test.ts +++ b/src/main/codex/codex-structured-thread-open.test.ts @@ -23,7 +23,7 @@ describe('openCodexThread', () => { const permissionPolicy = { approvalPolicy: 'never' as const, sandbox: 'danger-full-access' as const, - approvalsReviewer: 'auto_review' as const + approvalsReviewer: 'guardian_subagent' as const } await openCodexThread( @@ -44,7 +44,7 @@ describe('openCodexThread', () => { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access', - approvalsReviewer: 'auto_review' + approvalsReviewer: 'guardian_subagent' }, { timeoutMs: 2_000 } ) @@ -56,7 +56,7 @@ describe('openCodexThread', () => { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access', - approvalsReviewer: 'auto_review', + approvalsReviewer: 'guardian_subagent', excludeTurns: true }, { timeoutMs: 2_000 } diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index e5af67b30029..3479f5829431 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2768,7 +2768,7 @@ "argumentsApprovalValues": "The approval policy in saved Arguments must be untrusted, on-request or never.", "argumentsReviewerValues": "The approvals reviewer in saved Arguments must be user or auto_review.", "argumentsProfileHint": "To keep the profile's settings, put them in config.toml or pass them with -c.", - "argumentsProviderHint": "Choose the provider with -c model_provider=… instead.", + "argumentsProviderHint": "Choose the provider with -c model_provider=… and a local model with -m instead.", "argumentsWorkspaceHint": "A chat already works in its workspace folder.", "argumentsImageHint": "Attach the image in the chat instead.", "removeFromSavedArguments": "Remove it from {{agent}}'s Arguments in Settings → Agents.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 3f86f6ab2cbd..83d144826e3f 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17854,7 +17854,7 @@ "argumentsApprovalValues": "The approval policy in saved Arguments must be untrusted, on-request or never.", "argumentsReviewerValues": "The approvals reviewer in saved Arguments must be user or auto_review.", "argumentsProfileHint": "To keep the profile's settings, put them in config.toml or pass them with -c.", - "argumentsProviderHint": "Choose the provider with -c model_provider=… instead.", + "argumentsProviderHint": "Choose the provider with -c model_provider=… and a local model with -m instead.", "argumentsWorkspaceHint": "A chat already works in its workspace folder.", "argumentsImageHint": "Attach the image in the chat instead.", "removeFromSavedArguments": "Remove it from {{agent}}'s Arguments in Settings → Agents.", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 59e03cc07dc1..e6cf1d24ca6b 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -17623,7 +17623,7 @@ "argumentsApprovalValues": "La política de aprobación de los argumentos guardados debe ser untrusted, on-request o never.", "argumentsReviewerValues": "El revisor de aprobaciones de los argumentos guardados debe ser user o auto_review.", "argumentsProfileHint": "Para conservar los ajustes del perfil, póngalos en config.toml o páselos con -c.", - "argumentsProviderHint": "Elija el proveedor con -c model_provider=… en su lugar.", + "argumentsProviderHint": "Elija el proveedor con -c model_provider=… y un modelo local con -m en su lugar.", "argumentsWorkspaceHint": "Un chat ya trabaja en la carpeta de su espacio de trabajo.", "argumentsImageHint": "En su lugar, adjunte la imagen en el chat.", "removeFromSavedArguments": "Quítelo de los argumentos de {{agent}} en Ajustes → Agentes.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index a09643d9fe31..ccb769984919 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -17724,7 +17724,7 @@ "argumentsApprovalValues": "La politique d'approbation des arguments enregistrés doit être untrusted, on-request ou never.", "argumentsReviewerValues": "Le réviseur des approbations des arguments enregistrés doit être user ou auto_review.", "argumentsProfileHint": "Pour conserver les réglages du profil, placez-les dans config.toml ou passez-les avec -c.", - "argumentsProviderHint": "Choisissez le fournisseur avec -c model_provider=… à la place.", + "argumentsProviderHint": "Choisissez le fournisseur avec -c model_provider=… et un modèle local avec -m à la place.", "argumentsWorkspaceHint": "Un chat travaille déjà dans le dossier de son espace de travail.", "argumentsImageHint": "Joignez plutôt l'image dans le chat.", "removeFromSavedArguments": "Retirez-le des arguments de {{agent}} dans Paramètres → Agents.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 2081e6ca24be..dfa9104f45a1 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -17658,7 +17658,7 @@ "argumentsApprovalValues": "保存された引数の承認ポリシーは untrusted、on-request、never のいずれかである必要があります。", "argumentsReviewerValues": "保存された引数の承認レビュアーは user または auto_review である必要があります。", "argumentsProfileHint": "プロファイルの設定を使い続けるには、config.toml に記述するか -c で指定してください。", - "argumentsProviderHint": "代わりに -c model_provider=… でプロバイダーを選択してください。", + "argumentsProviderHint": "代わりに -c model_provider=… でプロバイダーを、-m でローカルモデルを選択してください。", "argumentsWorkspaceHint": "チャットはすでにワークスペースフォルダーで動作しています。", "argumentsImageHint": "代わりにチャットで画像を添付してください。", "removeFromSavedArguments": "[設定] → [Agent] の {{agent}} の引数から削除してください。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 8c608267f784..24190ecb1b67 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -17658,7 +17658,7 @@ "argumentsApprovalValues": "저장된 인수의 승인 정책은 untrusted, on-request, never 중 하나여야 합니다.", "argumentsReviewerValues": "저장된 인수의 승인 검토자는 user 또는 auto_review여야 합니다.", "argumentsProfileHint": "프로필 설정을 유지하려면 config.toml에 넣거나 -c로 전달하세요.", - "argumentsProviderHint": "대신 -c model_provider=…로 제공자를 선택하세요.", + "argumentsProviderHint": "대신 -c model_provider=…로 제공자를, -m으로 로컬 모델을 선택하세요.", "argumentsWorkspaceHint": "채팅은 이미 워크스페이스 폴더에서 작동합니다.", "argumentsImageHint": "대신 채팅에서 이미지를 첨부하세요.", "removeFromSavedArguments": "설정 → 에이전트의 {{agent}} 인수에서 제거하세요.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 7e2f294f9fa7..804d70bd1267 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -17623,7 +17623,7 @@ "argumentsApprovalValues": "已保存参数中的审批策略必须是 untrusted、on-request 或 never。", "argumentsReviewerValues": "已保存参数中的审批审核者必须是 user 或 auto_review。", "argumentsProfileHint": "如需保留该配置档的设置,请将其写入 config.toml 或通过 -c 传入。", - "argumentsProviderHint": "请改用 -c model_provider=… 选择提供方。", + "argumentsProviderHint": "请改用 -c model_provider=… 选择提供方,并用 -m 选择本地模型。", "argumentsWorkspaceHint": "聊天已在其工作区文件夹中运行。", "argumentsImageHint": "请改为在聊天中附加图片。", "removeFromSavedArguments": "请在设置 → 智能体中从 {{agent}} 的参数里删除它。", diff --git a/src/shared/agent-session-failure-copy.ts b/src/shared/agent-session-failure-copy.ts index 632220130491..b50d961b4c14 100644 --- a/src/shared/agent-session-failure-copy.ts +++ b/src/shared/agent-session-failure-copy.ts @@ -31,7 +31,8 @@ export const AGENT_SESSION_FAILURE_COPY = { argumentsUnclosedQuote: "Saved Arguments have a quote that isn't closed.", argumentsProfileHint: "To keep the profile's settings, put them in config.toml or pass them with -c.", - argumentsProviderHint: 'Choose the provider with -c model_provider=… instead.', + argumentsProviderHint: + 'Choose the provider with -c model_provider=… and a local model with -m instead.', argumentsWorkspaceHint: 'A chat already works in its workspace folder.', argumentsImageHint: 'Attach the image in the chat instead.', removeFromSavedArguments: "Remove it from {{agent}}'s Arguments in Settings → Agents.", diff --git a/src/shared/agent-session-refusal-notice.test.ts b/src/shared/agent-session-refusal-notice.test.ts index 46ac889f4590..2aedfc34de2f 100644 --- a/src/shared/agent-session-refusal-notice.test.ts +++ b/src/shared/agent-session-refusal-notice.test.ts @@ -748,7 +748,7 @@ describe('saved Arguments refusals', () => { ], [ { agent: 'Codex', option: '--oss', problem: 'unsupportedOption' }, - "Codex couldn't start. Codex chats can't use --oss from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Choose the provider with -c model_provider=… instead." + "Codex couldn't start. Codex chats can't use --oss from saved Arguments. Remove it from Codex's Arguments in Settings → Agents. Choose the provider with -c model_provider=… and a local model with -m instead." ], [ { agent: 'Codex', option: '-C', problem: 'unsupportedOption' }, From c0afdc9c52ffe58a33c0d2b88803120ce0906129 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Tue, 6 Oct 2026 23:53:43 -0700 Subject: [PATCH 4/6] fix(native-chat): Codex chats take permissions from Agent Permissions alone Saved Arguments no longer set a Codex chat's sandbox, approval policy or reviewer. -s/--sandbox, -a/--ask-for-approval and --approve-for-me / --not-so-yolo are dropped quietly, like the bypass flag (a value option still consumes its value, and a missing one is still refused), and -c / --config whose top key is approval_policy, sandbox_mode, approvals_reviewer or sandbox_workspace_write never reaches app-server. The thread policy comes from the Agent Permissions setting only, and still states the reviewer, user, on every start and resume so a resumed chat never keeps a stale one. This removes the invalid-value refusal and its copy in every locale, the value aliases, and the policy parameter the Arguments used to feed. --- src/main/codex/codex-model-catalog-probe.ts | 2 +- .../codex-structured-launch-args.test.ts | 146 +++++------------- .../codex/codex-structured-launch-args.ts | 119 ++++---------- ...codex-structured-launch-resolution.test.ts | 28 +--- .../codex-structured-launch-resolution.ts | 13 +- ...codex-structured-permission-policy.test.ts | 52 +++---- .../codex-structured-permission-policy.ts | 31 ++-- .../codex-structured-thread-open.test.ts | 6 +- .../structured-agent-arguments-error.ts | 6 +- .../runtime/orca-runtime-get-worktree-ps.ts | 4 +- ...-structured-launch-settings-wiring.test.ts | 9 +- .../structured-agent-session-runtime.ts | 6 +- ...nt-session-arguments-failure-words-text.ts | 21 --- .../src/i18n/en-runtime-required.json | 4 - src/renderer/src/i18n/locales/en.json | 4 - src/renderer/src/i18n/locales/es.json | 4 - src/renderer/src/i18n/locales/fr.json | 4 - src/renderer/src/i18n/locales/ja.json | 4 - src/renderer/src/i18n/locales/ko.json | 4 - src/renderer/src/i18n/locales/zh.json | 4 - .../agent-session-argument-problem-words.ts | 17 +- src/shared/agent-session-argument-problem.ts | 30 +--- src/shared/agent-session-failure-copy.ts | 6 - .../agent-session-refusal-notice.test.ts | 16 +- 24 files changed, 135 insertions(+), 405 deletions(-) diff --git a/src/main/codex/codex-model-catalog-probe.ts b/src/main/codex/codex-model-catalog-probe.ts index 6291d8f3fc23..b9647aa698ee 100644 --- a/src/main/codex/codex-model-catalog-probe.ts +++ b/src/main/codex/codex-model-catalog-probe.ts @@ -45,7 +45,7 @@ export function createCodexModelCatalogProbe( ): AgentModelCatalogProbe { return async (accountHomePath: string): Promise => { const { command, environment } = await resolveCodexStructuredInvocation(deps) - const { args } = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) + const args = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) const run = deps.runSession ?? runCodexAppServerSession const listing = await run( { diff --git a/src/main/codex/codex-structured-launch-args.test.ts b/src/main/codex/codex-structured-launch-args.test.ts index b3b8f4d1aacb..8fa9e1500e87 100644 --- a/src/main/codex/codex-structured-launch-args.test.ts +++ b/src/main/codex/codex-structured-launch-args.test.ts @@ -15,111 +15,67 @@ describe('codexStructuredLaunchArgs', () => { '-c=model_provider=azure', '--strict-config' ]) - ).toEqual({ - args: [ - '-c', - 'model_reasoning_effort=high', - '--enable', - 'unified_exec', - '--config', - 'web_search="cached"', - '--disable', - 'some_feature', - '-c', - 'model_provider=azure', - '--strict-config' - ], - permissions: {} - }) + ).toEqual([ + '-c', + 'model_reasoning_effort=high', + '--enable', + 'unified_exec', + '--config', + 'web_search="cached"', + '--disable', + 'some_feature', + '-c', + 'model_provider=azure', + '--strict-config' + ]) }) // app-server ignores `-m` and `--search`; Codex's own equivalents are config it applies after `-c`. it('turns -m and --search into the config Codex derives from them, after the user config', () => { expect( - codexStructuredLaunchArgs(['-m', 'gpt-5.6 "sol"', '--search', '-c', 'model="other"']).args + codexStructuredLaunchArgs(['-m', 'gpt-5.6 "sol"', '--search', '-c', 'model="other"']) ).toEqual(['-c', 'model="other"', '-c', 'model="gpt-5.6 \\"sol\\""', '-c', 'web_search="live"']) - expect(codexStructuredLaunchArgs(['--model=gpt-5.6-sol']).args).toEqual([ + expect(codexStructuredLaunchArgs(['--model=gpt-5.6-sol'])).toEqual([ '-c', 'model="gpt-5.6-sol"' ]) }) - it('passes sandbox details and the reviewer, and reads the reviewer for the thread', () => { + // Agent Permissions owns the posture, so neither the options nor their config keys reach Codex. + it('drops permission options and the config that sets permissions', () => { expect( codexStructuredLaunchArgs([ + '-s', + 'read-only', + '--sandbox=bogus', + '-a', + 'untrusted', + '--ask-for-approval=on-failure', + '--approve-for-me', + '--not-so-yolo', '-c', - 'sandbox_workspace_write.network_access=true', + 'approval_policy="never"', + '--config', + 'sandbox_mode=danger-full-access', '-c', - 'approvals_reviewer="auto_review"' - ]) - ).toEqual({ - args: [ + 'approvals_reviewer="auto_review"', '-c', 'sandbox_workspace_write.network_access=true', '-c', - 'approvals_reviewer="auto_review"' - ], - permissions: { approvalsReviewer: 'auto_review' } - }) - }) - - it("reads on-failure as Codex's own on-request alias", () => { - expect(codexStructuredLaunchArgs(['-a', 'on-failure']).permissions).toEqual({ - approvalPolicy: 'on-request' - }) - expect(codexStructuredLaunchArgs(['-c', 'approval_policy="on-failure"']).permissions).toEqual({ - approvalPolicy: 'on-request' - }) - }) - - // Codex before 0.124 takes only `guardian_subagent` on the thread; later versions take either. - it('keeps the reviewer as spelled', () => { - for (const reviewer of ['user', 'auto_review', 'guardian_subagent'] as const) { - expect( - codexStructuredLaunchArgs(['-c', `approvals_reviewer="${reviewer}"`]).permissions - ).toEqual({ approvalsReviewer: reviewer }) - } - }) - - it('reads the sandbox and approval the Arguments state, flags over config', () => { - expect(codexStructuredLaunchArgs(['-s', 'read-only', '-a', 'untrusted'])).toEqual({ - args: [], - permissions: { sandbox: 'read-only', approvalPolicy: 'untrusted' } - }) - expect( - codexStructuredLaunchArgs([ - '-c', - 'sandbox_mode="danger-full-access"', - '--config', - "approval_policy='never'", + '"approval_policy" = "never"', '-c', - 'sandbox_mode=read-only' - ]).permissions - ).toEqual({ sandbox: 'read-only', approvalPolicy: 'never' }) - expect( - codexStructuredLaunchArgs(['--sandbox=workspace-write', '-c', 'sandbox_mode=read-only']) - .permissions - ).toEqual({ sandbox: 'workspace-write' }) + 'model_reasoning_effort=high' + ]) + ).toEqual(['-c', 'model_reasoning_effort=high']) }) - it('applies --approve-for-me as the config Codex folds it into', () => { - expect(codexStructuredLaunchArgs(['--approve-for-me', '-c', 'approval_policy=never'])).toEqual({ - args: [ - '-c', - 'approval_policy=never', - '-c', - 'approvals_reviewer="auto_review"', - '-c', - 'approval_policy="on-request"', - '-c', - 'sandbox_mode="workspace-write"' - ], - permissions: { - approvalsReviewer: 'auto_review', - approvalPolicy: 'on-request', - sandbox: 'workspace-write' - } - }) + it('still needs a value for a dropped permission option', () => { + expect(() => codexStructuredLaunchArgs(['-s'])).toThrow(StructuredAgentArgumentsError) + expect(() => codexStructuredLaunchArgs(['-a', '--search'])).toThrow( + expect.objectContaining({ + argumentProblem: { agent: 'Codex', option: '-a', problem: 'missingValue' } + }) + ) }) it('drops the bypass flag and terminal-only display options', () => { @@ -133,7 +89,7 @@ describe('codexStructuredLaunchArgs', () => { '--version', '--' ]) - ).toEqual({ args: [], permissions: {} }) + ).toEqual([]) }) it.each([ @@ -172,25 +128,7 @@ describe('codexStructuredLaunchArgs', () => { { tokens: ['--unknown-flag=secret'], option: '--unknown-flag', problem: 'unsupportedOption' }, { tokens: ['--search=secret'], option: '--search', problem: 'unsupportedOption' }, { tokens: ['--enable'], option: '--enable', problem: 'missingValue' }, - { tokens: ['-m', '--secret'], option: '-m', problem: 'missingValue' }, - // An invalid value names the setting it is for, however the Arguments spelled it. - { tokens: ['-s', 'private-mode'], option: 'sandbox_mode', problem: 'invalidValue' }, - { - tokens: ['--ask-for-approval=private'], - option: 'approval_policy', - problem: 'invalidValue' - }, - { - tokens: ['-c', 'approval_policy={ secret = true }'], - option: 'approval_policy', - problem: 'invalidValue' - }, - { tokens: ['--config=sandbox_mode=secret'], option: 'sandbox_mode', problem: 'invalidValue' }, - { - tokens: ['-c', 'approvals_reviewer=secret'], - option: 'approvals_reviewer', - problem: 'invalidValue' - } + { tokens: ['-m', '--secret'], option: '-m', problem: 'missingValue' } ] as const)('refuses what a chat cannot honor: %j', ({ tokens, option, problem }) => { const thrown = () => codexStructuredLaunchArgs(tokens) expect(thrown).toThrow(StructuredAgentArgumentsError) diff --git a/src/main/codex/codex-structured-launch-args.ts b/src/main/codex/codex-structured-launch-args.ts index a952e9ce6f10..64b132822803 100644 --- a/src/main/codex/codex-structured-launch-args.ts +++ b/src/main/codex/codex-structured-launch-args.ts @@ -1,31 +1,15 @@ // Saved Arguments are written for the interactive `codex` a terminal runs; a chat runs // `codex app-server`, which reads only config and its own few options. So each interactive option // either passes to app-server, becomes the config Codex itself derives from it, is dropped because -// it only shapes a terminal, or refuses the start by name because a chat can't honor it. +// it sets permissions (Agent Permissions owns those) or only shapes a terminal, or refuses the +// start by name because a chat can't honor it. +import { parseTomlKeyPath } from './config-toml-key-path' import { StructuredAgentArgumentsError } from '../native-chat/structured-agent-arguments-error' -import type { CodexArgumentSetting } from '../../shared/agent-session-argument-problem' -import { - CODEX_APPROVAL_POLICIES, - CODEX_APPROVALS_REVIEWERS, - CODEX_SANDBOX_MODES, - type CodexStructuredPermissionPolicy -} from './codex-structured-permission-policy' - -export type CodexStructuredLaunchArgs = { - /** Options for after `app-server`. */ - args: string[] - /** The sandbox, approval policy and reviewer the Arguments state explicitly. */ - permissions: Partial -} type LaunchPlan = { args: string[] /** Config the interactive CLI derives from its own options; it applies them after every `-c`. */ derived: string[] - /** From `-c`, last one wins. */ - configured: Partial - /** From `-s` / `-a`, which outrank config. */ - flags: Partial } type OptionRule = @@ -36,9 +20,17 @@ const pass = (plan: LaunchPlan, option: string, value: string): void => { plan.args.push(option, ...(value ? [value] : [])) } +// The permission options' own config keys, dropped with them. const passConfig = (plan: LaunchPlan, option: string, value: string): void => { - readConfiguredPermission(plan.configured, value) - pass(plan, option, value) + const key = parseTomlKeyPath(value)?.segments[0] + if ( + key !== 'approval_policy' && + key !== 'sandbox_mode' && + key !== 'approvals_reviewer' && + key !== 'sandbox_workspace_write' + ) { + pass(plan, option, value) + } } const derive = @@ -52,27 +44,11 @@ const deriveModel = (plan: LaunchPlan, _option: string, value: string): void => plan.derived.push(`model=${JSON.stringify(value)}`) } -const flagSandbox = (plan: LaunchPlan, _option: string, value: string): void => { - plan.flags.sandbox = oneOf(CODEX_SANDBOX_MODES, value, 'sandbox_mode') -} - -const flagApproval = (plan: LaunchPlan, _option: string, value: string): void => { - plan.flags.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, 'approval_policy') -} - const VALUE = { takesValue: true } const DROPPED_SWITCH = { takesValue: false } -const APPROVE_FOR_ME = { - takesValue: false, - apply: derive( - 'approvals_reviewer="auto_review"', - 'approval_policy="on-request"', - 'sandbox_mode="workspace-write"' - ) -} -// Equivalents are the ones Codex applies itself: `--search` and `--approve-for-me` in -// codex-rs cli/src/main.rs and utils/cli/src/shared_options.rs, `-m` as `-c model=`. +// Equivalents are the ones Codex applies itself: `--search` in codex-rs cli/src/main.rs, `-m` as +// `-c model=`. const RULES: Record = { '-c': { ...VALUE, apply: passConfig }, '--config': { ...VALUE, apply: passConfig }, @@ -82,13 +58,13 @@ const RULES: Record = { '-m': { ...VALUE, apply: deriveModel }, '--model': { ...VALUE, apply: deriveModel }, '--search': { takesValue: false, apply: derive('web_search="live"') }, - '-s': { ...VALUE, apply: flagSandbox }, - '--sandbox': { ...VALUE, apply: flagSandbox }, - '-a': { ...VALUE, apply: flagApproval }, - '--ask-for-approval': { ...VALUE, apply: flagApproval }, - '--approve-for-me': APPROVE_FOR_ME, - '--not-so-yolo': APPROVE_FOR_ME, - // Agent Permissions reads the bypass flag; the rest only shape a terminal. + // Permissions come from Agent Permissions alone; the rest only shape a terminal. + '-s': VALUE, + '--sandbox': VALUE, + '-a': VALUE, + '--ask-for-approval': VALUE, + '--approve-for-me': DROPPED_SWITCH, + '--not-so-yolo': DROPPED_SWITCH, '--dangerously-bypass-approvals-and-sandbox': DROPPED_SWITCH, '--yolo': DROPPED_SWITCH, '--no-alt-screen': DROPPED_SWITCH, @@ -113,47 +89,6 @@ const RULES: Record = { '--dangerously-bypass-hook-trust': 'refuse' } -// Codex reads `on-failure` as `on-request` from 0.143, when its thread params stopped taking it. -const VALUE_ALIASES = new Map([['on-failure', 'on-request']]) - -function oneOf( - values: readonly T[], - value: string, - setting: CodexArgumentSetting -): T { - const canonical = VALUE_ALIASES.get(value) ?? value - const match = values.find((candidate) => candidate === canonical) - if (match === undefined) { - throw new StructuredAgentArgumentsError('Codex', setting, 'invalidValue') - } - return match -} - -/** The policy a `-c approval_policy=` / `sandbox_mode=` / `approvals_reviewer=` sets, read the - * way Codex reads it. */ -function readConfiguredPermission( - configured: Partial, - override: string -): void { - const separator = override.indexOf('=') - if (separator === -1) { - return - } - const key = override.slice(0, separator).trim() - // Codex takes a value that isn't TOML as its text with the quotes trimmed. - const value = override - .slice(separator + 1) - .trim() - .replace(/^["']+|["']+$/g, '') - if (key === 'approval_policy') { - configured.approvalPolicy = oneOf(CODEX_APPROVAL_POLICIES, value, key) - } else if (key === 'sandbox_mode') { - configured.sandbox = oneOf(CODEX_SANDBOX_MODES, value, key) - } else if (key === 'approvals_reviewer') { - configured.approvalsReviewer = oneOf(CODEX_APPROVALS_REVIEWERS, value, key) - } -} - function optionName(token: string): string { if (token.startsWith('--')) { return token.split('=', 1)[0] @@ -169,9 +104,9 @@ function inlineValue(token: string, option: string): string | undefined { return token.startsWith('--') ? token.slice(option.length + 1) : token.slice(2).replace(/^=/, '') } -/** Turns saved terminal Arguments into a structured app-server launch. */ -export function codexStructuredLaunchArgs(tokens: readonly string[]): CodexStructuredLaunchArgs { - const plan: LaunchPlan = { args: [], derived: [], configured: {}, flags: {} } +/** Turns saved terminal Arguments into options for after `app-server`. */ +export function codexStructuredLaunchArgs(tokens: readonly string[]): string[] { + const plan: LaunchPlan = { args: [], derived: [] } for (let index = 0; index < tokens.length; index += 1) { const token = tokens[index] if (token === '--') { @@ -202,7 +137,7 @@ export function codexStructuredLaunchArgs(tokens: readonly string[]): CodexStruc rule.apply?.(plan, option, value) } for (const override of plan.derived) { - passConfig(plan, '-c', override) + pass(plan, '-c', override) } - return { args: plan.args, permissions: { ...plan.configured, ...plan.flags } } + return plan.args } diff --git a/src/main/codex/codex-structured-launch-resolution.test.ts b/src/main/codex/codex-structured-launch-resolution.test.ts index 44d37cbd6dbb..8b4fb27c95fe 100644 --- a/src/main/codex/codex-structured-launch-resolution.test.ts +++ b/src/main/codex/codex-structured-launch-resolution.test.ts @@ -63,8 +63,7 @@ function resolverFor( resolveCommand: () => '/usr/local/bin/codex', resolveRollout, resolveLaunchArgs: resolveLaunchArgs ?? (() => value?.launchArgs ?? []), - resolvePermissionPolicy: (requested) => - codexStructuredPermissionPolicyForSettings({ agentDefaultArgs }, requested) + resolvePermissionPolicy: () => codexStructuredPermissionPolicyForSettings({ agentDefaultArgs }) }) } @@ -301,30 +300,19 @@ describe('codex structured launch resolution', () => { ]) }) - // A terminal honors these, so a chat that sent Manual's workspace-write would run looser. - it('opens the thread under the sandbox and approval the Arguments state under Manual', async () => { + // Agent Permissions alone sets the thread policy; the Arguments' own permission options are dropped. + it('opens the thread under the setting, not the permission options in Arguments', async () => { const launch = await resolverFor( - record({ launchArgs: ['-s', 'read-only', '-c', 'approval_policy=untrusted'] }) + record({ + launchArgs: ['-s', 'read-only', '-c', 'approval_policy=untrusted', '--approve-for-me'] + }) )({ identity: IDENTITY }) - expect(launch.args).toEqual(['app-server', '-c', 'approval_policy=untrusted']) - expect(launch.permissionPolicy).toEqual({ - approvalPolicy: 'untrusted', - sandbox: 'read-only', - approvalsReviewer: 'user' - }) - }) - - // A resume keeps the thread's saved reviewer unless the request names one. - it('opens the thread with the reviewer --approve-for-me asks for under Manual', async () => { - const launch = await resolverFor(record({ launchArgs: ['--approve-for-me'] }))({ - identity: IDENTITY - }) - + expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'on-request', sandbox: 'workspace-write', - approvalsReviewer: 'auto_review' + approvalsReviewer: 'user' }) }) diff --git a/src/main/codex/codex-structured-launch-resolution.ts b/src/main/codex/codex-structured-launch-resolution.ts index 1acb613d90b3..150793f3fb89 100644 --- a/src/main/codex/codex-structured-launch-resolution.ts +++ b/src/main/codex/codex-structured-launch-resolution.ts @@ -29,12 +29,9 @@ export type CodexStructuredLaunchResolverDeps = { /** Fresh shell/configured environment for this spawn; never written to the session record. */ resolveEnvironment?: () => Promise resolveRollout?: typeof resolvePinnedCodexRolloutProof - /** The user's Agent Permissions setting as thread policy, re-read per acquisition, given the - * policy the Arguments state. States both postures outright — a resume inherits the last one - * for any field left absent. */ - resolvePermissionPolicy?: ( - requested: Partial - ) => CodexStructuredPermissionPolicy + /** The user's Agent Permissions setting as thread policy, re-read per acquisition. + * States both postures outright — a resume inherits the last one for any field left absent. */ + resolvePermissionPolicy?: () => CodexStructuredPermissionPolicy } export type CodexStructuredInvocation = { @@ -87,8 +84,8 @@ export function createCodexStructuredLaunchResolver( throw new Error(`codex sessions pin ${pinned}, not ${accountHome.variable}`) } const { command, environment } = await resolveCodexStructuredInvocation(deps) - const { args, permissions } = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) - const permissionPolicy = deps.resolvePermissionPolicy?.(permissions) + const args = codexStructuredLaunchArgs(await deps.resolveLaunchArgs()) + const permissionPolicy = deps.resolvePermissionPolicy?.() const head = agentSessionProviderHandleChainHead(record.providerHandleChain) // A Codex record's chain holds only Codex handles; the attach admission refuses anything else. const resumeThreadId = head?.handle.nativeId ?? null diff --git a/src/main/codex/codex-structured-permission-policy.test.ts b/src/main/codex/codex-structured-permission-policy.test.ts index bb8824fe9a40..3eba2df436fe 100644 --- a/src/main/codex/codex-structured-permission-policy.test.ts +++ b/src/main/codex/codex-structured-permission-policy.test.ts @@ -65,47 +65,31 @@ describe('codexStructuredPermissionPolicyForSettings', () => { } }) - // A terminal running `codex -s read-only` is read-only; Manual's default would loosen it. - it('takes the sandbox and approval the Arguments state under Manual', () => { - expect( - codexStructuredPermissionPolicyForSettings( - { agentDefaultArgs: { codex: '-s read-only' } }, - { sandbox: 'read-only' } - ) - ).toEqual({ ...MANUAL, sandbox: 'read-only' }) - expect( - codexStructuredPermissionPolicyForSettings( - { agentDefaultArgs: { codex: '-a untrusted -s danger-full-access' } }, - { approvalPolicy: 'untrusted', sandbox: 'danger-full-access' } - ) - ).toEqual({ ...MANUAL, approvalPolicy: 'untrusted', sandbox: 'danger-full-access' }) + // A resume keeps the reviewer the thread last ran with unless the request names one. + it('states Codex default reviewer for both postures', () => { + for (const codex of ['', '--dangerously-bypass-approvals-and-sandbox']) { + expect( + codexStructuredPermissionPolicyForSettings({ agentDefaultArgs: { codex } }) + .approvalsReviewer, + codex + ).toBe('user') + } }) - // A resume keeps the reviewer the thread last ran with unless the request names one, so - // dropping --approve-for-me has to say `user` out loud. - it('states the reviewer under Manual: the one the Arguments name, else Codex default', () => { - expect( - codexStructuredPermissionPolicyForSettings( - { agentDefaultArgs: { codex: '--approve-for-me' } }, - { approvalsReviewer: 'auto_review' } - ) - ).toEqual({ ...MANUAL, approvalsReviewer: 'auto_review' }) + // Arguments' own permission options are dropped; only the toggle's posture is read. + it('ignores the permission options the Arguments state', () => { expect( - codexStructuredPermissionPolicyForSettings({ agentDefaultArgs: { codex: '' } }) - .approvalsReviewer - ).toBe('user') - }) - - it('keeps Yolo whatever else the Arguments state', () => { + codexStructuredPermissionPolicyForSettings({ + agentDefaultArgs: { codex: '-s read-only -a untrusted --approve-for-me' } + }) + ).toEqual(MANUAL) expect( - codexStructuredPermissionPolicyForSettings( - { agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox -s read-only' } }, - { sandbox: 'read-only', approvalsReviewer: 'auto_review' } - ) + codexStructuredPermissionPolicyForSettings({ + agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox -s read-only' } + }) ).toEqual(BYPASS) }) - // The posture alone comes out of the field here; the translator reads the stated policy. it('carries nothing but the permission posture out of the arguments field', () => { expect( codexStructuredPermissionPolicyForSettings({ diff --git a/src/main/codex/codex-structured-permission-policy.ts b/src/main/codex/codex-structured-permission-policy.ts index bf6b8e947031..9a6c4572757d 100644 --- a/src/main/codex/codex-structured-permission-policy.ts +++ b/src/main/codex/codex-structured-permission-policy.ts @@ -1,18 +1,10 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import { resolvedTuiAgentArgsBypassPermissions } from '../../shared/tui-agent-launch-defaults' -// The values app-server's v2 thread params accept (`AskForApproval` / `SandboxMode` / -// `ApprovalsReviewer`). The reviewer goes as spelled: Codex before 0.124 takes only -// `guardian_subagent`, later versions take either name. -export const CODEX_APPROVAL_POLICIES = ['untrusted', 'on-request', 'never'] as const -export const CODEX_SANDBOX_MODES = ['read-only', 'workspace-write', 'danger-full-access'] as const -export const CODEX_APPROVALS_REVIEWERS = ['user', 'auto_review', 'guardian_subagent'] as const - -export type CodexStructuredPermissionPolicy = { - approvalPolicy: (typeof CODEX_APPROVAL_POLICIES)[number] - sandbox: (typeof CODEX_SANDBOX_MODES)[number] - approvalsReviewer: (typeof CODEX_APPROVALS_REVIEWERS)[number] -} +export type CodexStructuredPermissionPolicy = ( + | { approvalPolicy: 'never'; sandbox: 'danger-full-access' } + | { approvalPolicy: 'on-request'; sandbox: 'workspace-write' } +) & { approvalsReviewer: 'user' } /** Yolo: no approval prompts, no sandbox. */ const BYPASS_POLICY = { @@ -38,7 +30,7 @@ const BYPASS_POLICY = { * need an approval it did not need before. This still resets Yolo's `danger-full-access`. * * The reviewer is stated too, as Codex's default `user`: a resume otherwise keeps the reviewer - * the thread last ran with, e.g. `auto_review` after `--approve-for-me` was removed. + * the thread last ran with, and a fresh thread takes the one `config.toml` names. */ const MANUAL_POLICY = { approvalPolicy: 'on-request', @@ -49,10 +41,10 @@ const MANUAL_POLICY = { /** * The Agent Permissions setting as app-server thread policy. * - * The posture is the one the toggle stores in the Arguments field; an untouched profile resolves - * to the default Orca ships, which is the bypass flag. Under Manual, a sandbox, approval policy or - * reviewer the Arguments state explicitly (`requested`) replaces that field's default, as it does - * in a terminal; Yolo ignores it. + * Derived per acquisition from the resolved launch arguments, never from the free-text Arguments + * field: app-server takes a narrower option set than the interactive CLI and the two are versioned + * apart, so the only thing read out of that field is the posture the toggle stores in it. An + * untouched profile resolves to the default Orca ships, which is the bypass flag. * * Always a policy, never `undefined`: both postures have to be said out loud, because the one * that goes unsaid is the one a resume silently inherits from the other. @@ -61,10 +53,9 @@ export function codexStructuredPermissionPolicyForSettings( settings: | Partial> | null - | undefined, - requested: Partial = {} + | undefined ): CodexStructuredPermissionPolicy { return resolvedTuiAgentArgsBypassPermissions('codex', settings, process.platform) ? BYPASS_POLICY - : { ...MANUAL_POLICY, ...requested } + : MANUAL_POLICY } diff --git a/src/main/codex/codex-structured-thread-open.test.ts b/src/main/codex/codex-structured-thread-open.test.ts index dc284a059d67..cd2efb48f352 100644 --- a/src/main/codex/codex-structured-thread-open.test.ts +++ b/src/main/codex/codex-structured-thread-open.test.ts @@ -23,7 +23,7 @@ describe('openCodexThread', () => { const permissionPolicy = { approvalPolicy: 'never' as const, sandbox: 'danger-full-access' as const, - approvalsReviewer: 'guardian_subagent' as const + approvalsReviewer: 'user' as const } await openCodexThread( @@ -44,7 +44,7 @@ describe('openCodexThread', () => { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access', - approvalsReviewer: 'guardian_subagent' + approvalsReviewer: 'user' }, { timeoutMs: 2_000 } ) @@ -56,7 +56,7 @@ describe('openCodexThread', () => { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access', - approvalsReviewer: 'guardian_subagent', + approvalsReviewer: 'user', excludeTurns: true }, { timeoutMs: 2_000 } diff --git a/src/main/native-chat/structured-agent-arguments-error.ts b/src/main/native-chat/structured-agent-arguments-error.ts index 0e86952dc4a3..05a2911d5083 100644 --- a/src/main/native-chat/structured-agent-arguments-error.ts +++ b/src/main/native-chat/structured-agent-arguments-error.ts @@ -1,5 +1,5 @@ import type { AgentSessionArgumentProblem } from '../../shared/agent-session-failure' -import { AGENT_SESSION_ARGUMENT_PROBLEM_WORDS } from '../../shared/agent-session-argument-problem' +import { AGENT_SESSION_ARGUMENT_PROBLEM_WORD } from '../../shared/agent-session-argument-problem' /** A saved Arguments refusal with only the option name, never its value. */ export class StructuredAgentArgumentsError extends Error { @@ -10,10 +10,8 @@ export class StructuredAgentArgumentsError extends Error { option: string, problem: AgentSessionArgumentProblem['problem'] ) { - const words = AGENT_SESSION_ARGUMENT_PROBLEM_WORDS[problem] - // A prompt or quote arrives as the user's own text, which is never carried. const normalizedOption = - (words && (words.find((word) => word === option) ?? words[0])) ?? + AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] ?? option.match(/^--[a-zA-Z][a-zA-Z0-9-]{0,63}/)?.[0] ?? option.match(/^-[a-zA-Z]/)?.[0] ?? '--?' diff --git a/src/main/runtime/orca-runtime-get-worktree-ps.ts b/src/main/runtime/orca-runtime-get-worktree-ps.ts index d95826f115a8..8b7d19af50b0 100644 --- a/src/main/runtime/orca-runtime-get-worktree-ps.ts +++ b/src/main/runtime/orca-runtime-get-worktree-ps.ts @@ -218,8 +218,8 @@ export class OrcaRuntimeWithGetWorktreePs extends OrcaRuntimeWithStartTuiIdleVis // the one copy of this fact, even when Arguments contain permission flags. resolveClaudePermissionMode: () => claudeStructuredPermissionModeForSettings(this.requireStore().getSettings()), - resolveCodexPermissionPolicy: (requested) => - codexStructuredPermissionPolicyForSettings(this.requireStore().getSettings(), requested), + resolveCodexPermissionPolicy: () => + codexStructuredPermissionPolicyForSettings(this.requireStore().getSettings()), resolveAgentFullAccess: (agent) => isTuiAgent(agent) && resolvedTuiAgentArgsBypassPermissions( diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index 76f55148b182..937425378587 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -37,14 +37,15 @@ describe('execution host structured launch settings wiring', () => { settings.agentDefaultArgs = { claude: '--model second', codex: '' } expect(await deps?.resolveLaunchArgs?.('claude')).toEqual(['--model', 'second']) expect(await deps?.resolveLaunchArgs?.('codex')).toEqual([]) - // Manual takes what the Arguments state; Yolo keeps bypassing. - expect(deps?.resolveCodexPermissionPolicy?.({ sandbox: 'read-only' })).toEqual({ + // Only the setting decides; a sandbox in the Arguments doesn't reach the thread policy. + settings.agentDefaultArgs = { codex: '-s read-only' } + expect(deps?.resolveCodexPermissionPolicy?.()).toEqual({ approvalPolicy: 'on-request', - sandbox: 'read-only', + sandbox: 'workspace-write', approvalsReviewer: 'user' }) settings.agentDefaultArgs = { codex: '--dangerously-bypass-approvals-and-sandbox' } - expect(deps?.resolveCodexPermissionPolicy?.({ sandbox: 'read-only' })).toEqual({ + expect(deps?.resolveCodexPermissionPolicy?.()).toEqual({ approvalPolicy: 'never', sandbox: 'danger-full-access', approvalsReviewer: 'user' diff --git a/src/main/runtime/structured-agent-session-runtime.ts b/src/main/runtime/structured-agent-session-runtime.ts index 689642e8cae2..d8a5698b0254 100644 --- a/src/main/runtime/structured-agent-session-runtime.ts +++ b/src/main/runtime/structured-agent-session-runtime.ts @@ -107,10 +107,8 @@ export type StructuredAgentSessionRuntimeDeps = { resolveClaudeAuthPolicy: () => Promise | ClaudeStructuredAuthPolicy /** The user's Agent Permissions setting for Claude; absent means prompting. */ resolveClaudePermissionMode?: () => Promise | PermissionMode - /** The same setting for Codex, as app-server thread policy, given what the Arguments state. */ - resolveCodexPermissionPolicy?: ( - requested: Partial - ) => CodexStructuredPermissionPolicy + /** The same setting for Codex, as app-server thread policy. */ + resolveCodexPermissionPolicy?: () => CodexStructuredPermissionPolicy /** The same setting for a protocol-driven (ACP) agent: whether it runs with full access. */ resolveAgentFullAccess?: (agent: string) => boolean /** The user's per-agent environment overlay, for agents with no lane-specific resolver. */ diff --git a/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts b/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts index 52f2b29654c6..0d5eba118acf 100644 --- a/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts +++ b/src/renderer/src/components/native-chat/agent-session-arguments-failure-words-text.ts @@ -40,27 +40,6 @@ export const SAVED_ARGUMENTS_PIECES: Record< COPY.argumentsMultipleValues, values ), - argumentsInvalidValue: (values) => - translate( - 'components.native-chat.failureWords.argumentsInvalidValue', - COPY.argumentsInvalidValue, - values - ), - argumentsSandboxValues: () => - translate( - 'components.native-chat.failureWords.argumentsSandboxValues', - COPY.argumentsSandboxValues - ), - argumentsApprovalValues: () => - translate( - 'components.native-chat.failureWords.argumentsApprovalValues', - COPY.argumentsApprovalValues - ), - argumentsReviewerValues: () => - translate( - 'components.native-chat.failureWords.argumentsReviewerValues', - COPY.argumentsReviewerValues - ), argumentsPositionalPrompt: () => translate( 'components.native-chat.failureWords.argumentsPositionalPrompt', diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json index 3479f5829431..f76c068006f6 100644 --- a/src/renderer/src/i18n/en-runtime-required.json +++ b/src/renderer/src/i18n/en-runtime-required.json @@ -2759,14 +2759,10 @@ "answerUnconfirmed": "Your answer was recorded but the agent did not confirm it.", "argumentsMissingValue": "Saved Arguments need a value for {{option}}.", "argumentsMultipleValues": "Saved Arguments give {{option}} more than one value.", - "argumentsInvalidValue": "Saved Arguments give {{option}} a value that isn't supported.", "argumentsUnclosedQuote": "Saved Arguments have a quote that isn't closed.", "argumentsPositionalPrompt": "Saved Arguments include a prompt.", "argumentsUnsupportedOption": "{{agent}} chats can't use {{option}} from saved Arguments.", "argumentsUnnamedOption": "{{agent}} chats can't use one of the options in saved Arguments.", - "argumentsSandboxValues": "The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access.", - "argumentsApprovalValues": "The approval policy in saved Arguments must be untrusted, on-request or never.", - "argumentsReviewerValues": "The approvals reviewer in saved Arguments must be user or auto_review.", "argumentsProfileHint": "To keep the profile's settings, put them in config.toml or pass them with -c.", "argumentsProviderHint": "Choose the provider with -c model_provider=… and a local model with -m instead.", "argumentsWorkspaceHint": "A chat already works in its workspace folder.", diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index aa986be9f08d..5ff61bc0a72b 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -17850,9 +17850,6 @@ "sessionNotRestored": "{{agent}} couldn't reopen its earlier session, so this chat continues in a new one. {{agent}} doesn't remember the earlier messages.", "argumentsUnsupportedOption": "{{agent}} chats can't use {{option}} from saved Arguments.", "argumentsUnnamedOption": "{{agent}} chats can't use one of the options in saved Arguments.", - "argumentsSandboxValues": "The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access.", - "argumentsApprovalValues": "The approval policy in saved Arguments must be untrusted, on-request or never.", - "argumentsReviewerValues": "The approvals reviewer in saved Arguments must be user or auto_review.", "argumentsProfileHint": "To keep the profile's settings, put them in config.toml or pass them with -c.", "argumentsProviderHint": "Choose the provider with -c model_provider=… and a local model with -m instead.", "argumentsWorkspaceHint": "A chat already works in its workspace folder.", @@ -17860,7 +17857,6 @@ "removeFromSavedArguments": "Remove it from {{agent}}'s Arguments in Settings → Agents.", "argumentsMissingValue": "Saved Arguments need a value for {{option}}.", "argumentsMultipleValues": "Saved Arguments give {{option}} more than one value.", - "argumentsInvalidValue": "Saved Arguments give {{option}} a value that isn't supported.", "argumentsUnclosedQuote": "Saved Arguments have a quote that isn't closed.", "argumentsPositionalPrompt": "Saved Arguments include a prompt.", "editSavedArguments": "Fix it in {{agent}}'s Arguments in Settings → Agents." diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 0793b97ef69b..173f7ff902ec 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -17614,14 +17614,10 @@ "answerUnconfirmed": "Su respuesta se registró, pero el agente no la confirmó.", "argumentsMissingValue": "Los argumentos guardados necesitan un valor para {{option}}.", "argumentsMultipleValues": "Los argumentos guardados asignan más de un valor a {{option}}.", - "argumentsInvalidValue": "Los argumentos guardados asignan a {{option}} un valor no compatible.", "argumentsUnclosedQuote": "Los argumentos guardados tienen una comilla sin cerrar.", "argumentsPositionalPrompt": "Los argumentos guardados incluyen un mensaje.", "argumentsUnsupportedOption": "Los chats de {{agent}} no pueden usar {{option}} de los argumentos guardados.", "argumentsUnnamedOption": "Los chats de {{agent}} no pueden usar una de las opciones de los argumentos guardados.", - "argumentsSandboxValues": "El sandbox de los argumentos guardados debe ser read-only, workspace-write o danger-full-access.", - "argumentsApprovalValues": "La política de aprobación de los argumentos guardados debe ser untrusted, on-request o never.", - "argumentsReviewerValues": "El revisor de aprobaciones de los argumentos guardados debe ser user o auto_review.", "argumentsProfileHint": "Para conservar los ajustes del perfil, póngalos en config.toml o páselos con -c.", "argumentsProviderHint": "Elija el proveedor con -c model_provider=… y un modelo local con -m en su lugar.", "argumentsWorkspaceHint": "Un chat ya trabaja en la carpeta de su espacio de trabajo.", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index be059309bc1b..2d04c003acf7 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -17715,14 +17715,10 @@ "answerUnconfirmed": "Votre réponse a été enregistrée, mais l'agent ne l'a pas confirmée.", "argumentsMissingValue": "Les arguments enregistrés nécessitent une valeur pour {{option}}.", "argumentsMultipleValues": "Les arguments enregistrés donnent plusieurs valeurs à {{option}}.", - "argumentsInvalidValue": "Les arguments enregistrés donnent à {{option}} une valeur non prise en charge.", "argumentsUnclosedQuote": "Les arguments enregistrés contiennent un guillemet non fermé.", "argumentsPositionalPrompt": "Les arguments enregistrés contiennent un message.", "argumentsUnsupportedOption": "Les chats {{agent}} ne peuvent pas utiliser {{option}} depuis les arguments enregistrés.", "argumentsUnnamedOption": "Les chats {{agent}} ne peuvent pas utiliser l'une des options des arguments enregistrés.", - "argumentsSandboxValues": "Le sandbox des arguments enregistrés doit être read-only, workspace-write ou danger-full-access.", - "argumentsApprovalValues": "La politique d'approbation des arguments enregistrés doit être untrusted, on-request ou never.", - "argumentsReviewerValues": "Le réviseur des approbations des arguments enregistrés doit être user ou auto_review.", "argumentsProfileHint": "Pour conserver les réglages du profil, placez-les dans config.toml ou passez-les avec -c.", "argumentsProviderHint": "Choisissez le fournisseur avec -c model_provider=… et un modèle local avec -m à la place.", "argumentsWorkspaceHint": "Un chat travaille déjà dans le dossier de son espace de travail.", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 187838c675cc..9efb2c13ac65 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -17649,14 +17649,10 @@ "answerUnconfirmed": "回答は記録されましたが、Agent は確認しませんでした。", "argumentsMissingValue": "保存された引数の {{option}} に値が必要です。", "argumentsMultipleValues": "保存された引数の {{option}} に複数の値が指定されています。", - "argumentsInvalidValue": "保存された引数の {{option}} に未対応の値が指定されています。", "argumentsUnclosedQuote": "保存された引数に閉じられていない引用符があります。", "argumentsPositionalPrompt": "保存された引数にプロンプトが含まれています。", "argumentsUnsupportedOption": "{{agent}} のチャットでは、保存された引数の {{option}} を使用できません。", "argumentsUnnamedOption": "{{agent}} のチャットでは、保存された引数のオプションの 1 つを使用できません。", - "argumentsSandboxValues": "保存された引数のサンドボックスは read-only、workspace-write、danger-full-access のいずれかである必要があります。", - "argumentsApprovalValues": "保存された引数の承認ポリシーは untrusted、on-request、never のいずれかである必要があります。", - "argumentsReviewerValues": "保存された引数の承認レビュアーは user または auto_review である必要があります。", "argumentsProfileHint": "プロファイルの設定を使い続けるには、config.toml に記述するか -c で指定してください。", "argumentsProviderHint": "代わりに -c model_provider=… でプロバイダーを、-m でローカルモデルを選択してください。", "argumentsWorkspaceHint": "チャットはすでにワークスペースフォルダーで動作しています。", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 1d7bbe99e900..579ee90e0847 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -17649,14 +17649,10 @@ "answerUnconfirmed": "응답이 기록되었지만 에이전트가 확인하지 않았습니다.", "argumentsMissingValue": "저장된 인수의 {{option}}에 값이 필요합니다.", "argumentsMultipleValues": "저장된 인수에 {{option}}의 값이 여러 개 지정되어 있습니다.", - "argumentsInvalidValue": "저장된 인수에 {{option}}의 지원하지 않는 값이 지정되어 있습니다.", "argumentsUnclosedQuote": "저장된 인수에 닫히지 않은 따옴표가 있습니다.", "argumentsPositionalPrompt": "저장된 인수에 프롬프트가 포함되어 있습니다.", "argumentsUnsupportedOption": "{{agent}} 채팅에서는 저장된 인수의 {{option}}을(를) 사용할 수 없습니다.", "argumentsUnnamedOption": "{{agent}} 채팅에서는 저장된 인수의 옵션 중 하나를 사용할 수 없습니다.", - "argumentsSandboxValues": "저장된 인수의 샌드박스는 read-only, workspace-write, danger-full-access 중 하나여야 합니다.", - "argumentsApprovalValues": "저장된 인수의 승인 정책은 untrusted, on-request, never 중 하나여야 합니다.", - "argumentsReviewerValues": "저장된 인수의 승인 검토자는 user 또는 auto_review여야 합니다.", "argumentsProfileHint": "프로필 설정을 유지하려면 config.toml에 넣거나 -c로 전달하세요.", "argumentsProviderHint": "대신 -c model_provider=…로 제공자를, -m으로 로컬 모델을 선택하세요.", "argumentsWorkspaceHint": "채팅은 이미 워크스페이스 폴더에서 작동합니다.", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 23b4bc858a21..01ce50184435 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -17614,14 +17614,10 @@ "answerUnconfirmed": "您的回答已记录,但智能体未确认。", "argumentsMissingValue": "已保存的参数需要为 {{option}} 指定一个值。", "argumentsMultipleValues": "已保存的参数为 {{option}} 指定了多个值。", - "argumentsInvalidValue": "已保存的参数为 {{option}} 指定了不支持的值。", "argumentsUnclosedQuote": "已保存的参数中有未闭合的引号。", "argumentsPositionalPrompt": "已保存的参数中包含提示词。", "argumentsUnsupportedOption": "{{agent}} 聊天无法使用已保存参数中的 {{option}}。", "argumentsUnnamedOption": "{{agent}} 聊天无法使用已保存参数中的某个选项。", - "argumentsSandboxValues": "已保存参数中的沙盒必须是 read-only、workspace-write 或 danger-full-access。", - "argumentsApprovalValues": "已保存参数中的审批策略必须是 untrusted、on-request 或 never。", - "argumentsReviewerValues": "已保存参数中的审批审核者必须是 user 或 auto_review。", "argumentsProfileHint": "如需保留该配置档的设置,请将其写入 config.toml 或通过 -c 传入。", "argumentsProviderHint": "请改用 -c model_provider=… 选择提供方,并用 -m 选择本地模型。", "argumentsWorkspaceHint": "聊天已在其工作区文件夹中运行。", diff --git a/src/shared/agent-session-argument-problem-words.ts b/src/shared/agent-session-argument-problem-words.ts index d6582178e001..94c65a615d06 100644 --- a/src/shared/agent-session-argument-problem-words.ts +++ b/src/shared/agent-session-argument-problem-words.ts @@ -2,11 +2,7 @@ // to change it, and for some options what a chat honors instead. Chosen from the option name alone, // which every client already receives. -import { - CODEX_ARGUMENT_SETTINGS, - type AgentSessionArgumentProblem, - type CodexArgumentSetting -} from './agent-session-argument-problem' +import type { AgentSessionArgumentProblem } from './agent-session-argument-problem' import type { AgentSessionFailureCopyId, AgentSessionFailureSay @@ -16,17 +12,10 @@ const PROBLEM_COPY = { unsupportedOption: 'argumentsUnsupportedOption', missingValue: 'argumentsMissingValue', multipleValues: 'argumentsMultipleValues', - invalidValue: 'argumentsInvalidValue', positionalPrompt: 'argumentsPositionalPrompt', unclosedQuote: 'argumentsUnclosedQuote' } as const satisfies Record -const SETTING_VALUES_COPY = { - sandbox_mode: 'argumentsSandboxValues', - approval_policy: 'argumentsApprovalValues', - approvals_reviewer: 'argumentsReviewerValues' -} as const satisfies Record - /** Codex options a chat refuses that have an equivalent, or no need, in a chat. */ const CODEX_OPTION_HINTS = new Map([ ['--profile', 'argumentsProfileHint'], @@ -45,10 +34,6 @@ function problemCopy({ option, problem }: AgentSessionArgumentProblem): AgentSes if (problem === 'unsupportedOption' && option === '--?') { return 'argumentsUnnamedOption' } - if (problem === 'invalidValue') { - const setting = CODEX_ARGUMENT_SETTINGS.find((candidate) => candidate === option) - return setting ? SETTING_VALUES_COPY[setting] : PROBLEM_COPY.invalidValue - } return PROBLEM_COPY[problem] } diff --git a/src/shared/agent-session-argument-problem.ts b/src/shared/agent-session-argument-problem.ts index c5151db2bb38..1cb10a42d6fe 100644 --- a/src/shared/agent-session-argument-problem.ts +++ b/src/shared/agent-session-argument-problem.ts @@ -6,28 +6,14 @@ export type AgentSessionArgumentProblem = { | 'unsupportedOption' | 'missingValue' | 'multipleValues' - | 'invalidValue' | 'positionalPrompt' | 'unclosedQuote' } -/** The Codex settings a saved value can be invalid for, by their config key. */ -export const CODEX_ARGUMENT_SETTINGS = [ - 'sandbox_mode', - 'approval_policy', - 'approvals_reviewer' -] as const -export type CodexArgumentSetting = (typeof CODEX_ARGUMENT_SETTINGS)[number] - -/** Problems that aren't about the option as typed carry one of these fixed words as their option: - * a value names the setting it is for, however the Arguments spelled it. */ -export const AGENT_SESSION_ARGUMENT_PROBLEM_WORDS: Partial< - Record -> = { - positionalPrompt: ['prompt'], - unclosedQuote: ['quote'], - invalidValue: CODEX_ARGUMENT_SETTINGS -} +/** Problems that are about no single option carry this fixed word as their option. */ +export const AGENT_SESSION_ARGUMENT_PROBLEM_WORD: Partial< + Record +> = { positionalPrompt: 'prompt', unclosedQuote: 'quote' } export function readAgentSessionArgumentProblem( value: unknown @@ -50,14 +36,12 @@ export function readAgentSessionArgumentProblem( (problem !== 'unsupportedOption' && problem !== 'missingValue' && problem !== 'multipleValues' && - problem !== 'invalidValue' && problem !== 'positionalPrompt' && problem !== 'unclosedQuote') || typeof option !== 'string' || - !( - AGENT_SESSION_ARGUMENT_PROBLEM_WORDS[problem]?.includes(option) ?? - /^(?:--[a-zA-Z][a-zA-Z0-9-]{0,63}|-[a-zA-Z]|--\?)$/.test(option) - ) + (AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] !== undefined + ? option !== AGENT_SESSION_ARGUMENT_PROBLEM_WORD[problem] + : !/^(?:--[a-zA-Z][a-zA-Z0-9-]{0,63}|-[a-zA-Z]|--\?)$/.test(option)) ) { return undefined } diff --git a/src/shared/agent-session-failure-copy.ts b/src/shared/agent-session-failure-copy.ts index b50d961b4c14..8df2553204de 100644 --- a/src/shared/agent-session-failure-copy.ts +++ b/src/shared/agent-session-failure-copy.ts @@ -21,12 +21,6 @@ export const AGENT_SESSION_FAILURE_COPY = { argumentsUnnamedOption: "{{agent}} chats can't use one of the options in saved Arguments.", argumentsMissingValue: 'Saved Arguments need a value for {{option}}.', argumentsMultipleValues: 'Saved Arguments give {{option}} more than one value.', - argumentsInvalidValue: "Saved Arguments give {{option}} a value that isn't supported.", - argumentsSandboxValues: - 'The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access.', - argumentsApprovalValues: - 'The approval policy in saved Arguments must be untrusted, on-request or never.', - argumentsReviewerValues: 'The approvals reviewer in saved Arguments must be user or auto_review.', argumentsPositionalPrompt: 'Saved Arguments include a prompt.', argumentsUnclosedQuote: "Saved Arguments have a quote that isn't closed.", argumentsProfileHint: diff --git a/src/shared/agent-session-refusal-notice.test.ts b/src/shared/agent-session-refusal-notice.test.ts index 2aedfc34de2f..ff6f009be88c 100644 --- a/src/shared/agent-session-refusal-notice.test.ts +++ b/src/shared/agent-session-refusal-notice.test.ts @@ -766,18 +766,6 @@ describe('saved Arguments refusals', () => { { agent: 'Codex', option: 'prompt', problem: 'positionalPrompt' }, "Codex couldn't start. Saved Arguments include a prompt. Remove it from Codex's Arguments in Settings → Agents." ], - [ - { agent: 'Codex', option: 'sandbox_mode', problem: 'invalidValue' }, - "Codex couldn't start. The sandbox in saved Arguments must be read-only, workspace-write or danger-full-access. Fix it in Codex's Arguments in Settings → Agents." - ], - [ - { agent: 'Codex', option: 'approval_policy', problem: 'invalidValue' }, - "Codex couldn't start. The approval policy in saved Arguments must be untrusted, on-request or never. Fix it in Codex's Arguments in Settings → Agents." - ], - [ - { agent: 'Codex', option: 'approvals_reviewer', problem: 'invalidValue' }, - "Codex couldn't start. The approvals reviewer in saved Arguments must be user or auto_review. Fix it in Codex's Arguments in Settings → Agents." - ], [ { agent: 'Claude', option: 'quote', problem: 'unclosedQuote' }, "Claude couldn't start. Saved Arguments have a quote that isn't closed. Fix it in Claude's Arguments in Settings → Agents." @@ -797,9 +785,7 @@ describe('saved Arguments refusals', () => { it.each([ { agent: 'Codex', option: '--remote', problem: 'futureProblem' }, { agent: 'Codex', option: '--remote=private', problem: 'unsupportedOption' }, - { agent: 'Codex', option: '--private', problem: 'unclosedQuote' }, - { agent: 'Codex', option: '-s', problem: 'invalidValue' }, - { agent: 'Codex', option: 'private_key', problem: 'invalidValue' } + { agent: 'Codex', option: '--private', problem: 'unclosedQuote' } ])('ignores an unrecognized or unsafe argument detail', (argumentProblem) => { const refused = parseAgentSessionWriteFailure({ kind: 'refused', From 73e1ca9f564b8d547fe4ff5358d874dd588a9e7e Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:15:27 -0700 Subject: [PATCH 5/6] fix(native-chat): keep #26059 to saved Arguments, and keep plugins off in the model probe Scope: the per-chat permission picker already states the approvals reviewer on every thread open and removes the Agent Permissions tooltip, and this PR now drops every reviewer key from Arguments. So the reviewer statement, its tests, the tooltip copy and its six translations, and the permission-policy wiring test come out; those files match main again. The model-list probe put its own config after the saved Arguments, but Codex applies --enable/--disable after every -c, so a saved --enable plugins re-enabled plugin startup in the short-lived probe. The probe now also passes --disable plugins, which Codex applies last. --- .../codex/codex-model-catalog-probe.test.ts | 26 +++++++++++- src/main/codex/codex-model-catalog-probe.ts | 12 +++++- ...codex-structured-launch-resolution.test.ts | 40 +++---------------- ...codex-structured-permission-policy.test.ts | 35 ++-------------- .../codex-structured-permission-policy.ts | 18 ++------- .../codex-structured-thread-open.test.ts | 21 ++-------- ...-structured-launch-settings-wiring.test.ts | 13 ------ .../src/components/settings/AgentsPane.tsx | 2 +- src/renderer/src/i18n/locales/en.json | 2 +- src/renderer/src/i18n/locales/es.json | 2 +- src/renderer/src/i18n/locales/fr.json | 2 +- src/renderer/src/i18n/locales/ja.json | 2 +- src/renderer/src/i18n/locales/ko.json | 2 +- src/renderer/src/i18n/locales/zh.json | 2 +- 14 files changed, 59 insertions(+), 120 deletions(-) diff --git a/src/main/codex/codex-model-catalog-probe.test.ts b/src/main/codex/codex-model-catalog-probe.test.ts index 1d86fdb5c5f2..a06b065e8a77 100644 --- a/src/main/codex/codex-model-catalog-probe.test.ts +++ b/src/main/codex/codex-model-catalog-probe.test.ts @@ -104,10 +104,34 @@ describe('codex model catalog probe', () => { '-c', 'approval_policy=never', '-c', - 'features.plugins=false' + 'features.plugins=false', + '--disable', + 'plugins' ]) }) + // Codex folds --enable/--disable in after every -c, so only a later --disable beats a saved + // --enable; a probe that starts plugins can leave marketplace clones behind. + it('keeps plugins off when the saved Arguments enable them', async () => { + const invocations: CodexAppServerInvocation[] = [] + const probe = createCodexModelCatalogProbe({ + resolveEnvironment: async () => ({ PATH: '/bin' }), + resolveCommand: () => '/bin/codex', + resolveLaunchArgs: () => ['--enable', 'plugins'], + runSession: async (invocation, body) => { + invocations.push(invocation) + return body({ + request: async () => ({ data: [MODEL_ROW], nextCursor: null }), + notify: () => {} + }) + } + }) + await probe('/homes/a') + const args = invocations[0]!.args + expect(args.slice(0, 3)).toEqual(['app-server', '--enable', 'plugins']) + expect(args.slice(-2)).toEqual(['--disable', 'plugins']) + }) + it('lists nothing when the saved Arguments refuse a chat', async () => { const runSession = vi.fn() const probe = createCodexModelCatalogProbe({ diff --git a/src/main/codex/codex-model-catalog-probe.ts b/src/main/codex/codex-model-catalog-probe.ts index b9647aa698ee..fe16b3106289 100644 --- a/src/main/codex/codex-model-catalog-probe.ts +++ b/src/main/codex/codex-model-catalog-probe.ts @@ -50,8 +50,16 @@ export function createCodexModelCatalogProbe( const listing = await run( { command, - // Codex applies `-c` in order, so the probe's own config comes last and wins. - args: ['app-server', ...args, ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS], + // The probe's own config comes after the Arguments' `-c`, which Codex applies in order. Codex + // folds `--enable`/`--disable` in after every `-c`, disables last, so only `--disable` + // outranks a saved `--enable plugins`. + args: [ + 'app-server', + ...args, + ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS, + '--disable', + 'plugins' + ], cliPath: command, env: { ...definedEnv(environment), CODEX_HOME: accountHomePath }, timeoutMs: CODEX_MODEL_CATALOG_PROBE_TIMEOUT_MS diff --git a/src/main/codex/codex-structured-launch-resolution.test.ts b/src/main/codex/codex-structured-launch-resolution.test.ts index 8b4fb27c95fe..08ac7970fe73 100644 --- a/src/main/codex/codex-structured-launch-resolution.test.ts +++ b/src/main/codex/codex-structured-launch-resolution.test.ts @@ -150,11 +150,7 @@ describe('codex structured launch resolution', () => { codexHome: '/home/work/.codex', resumeThreadId: null, // Every launch now carries a posture; neither one is left for config.toml to decide. - permissionPolicy: { - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' - } + permissionPolicy: { approvalPolicy: 'on-request', sandbox: 'workspace-write' } }) }) @@ -245,8 +241,7 @@ describe('codex structured launch resolution', () => { expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'never', - sandbox: 'danger-full-access', - approvalsReviewer: 'user' + sandbox: 'danger-full-access' }) }) @@ -256,8 +251,7 @@ describe('codex structured launch resolution', () => { expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'never', - sandbox: 'danger-full-access', - approvalsReviewer: 'user' + sandbox: 'danger-full-access' }) }) @@ -269,8 +263,7 @@ describe('codex structured launch resolution', () => { expect(launch.args).toEqual(['app-server']) expect(launch.permissionPolicy).toEqual({ approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' + sandbox: 'workspace-write' }) }) @@ -300,8 +293,8 @@ describe('codex structured launch resolution', () => { ]) }) - // Agent Permissions alone sets the thread policy; the Arguments' own permission options are dropped. - it('opens the thread under the setting, not the permission options in Arguments', async () => { + // Permissions come from Agent Permissions, never from Arguments' own permission options. + it('keeps permission options in Arguments out of the app-server launch', async () => { const launch = await resolverFor( record({ launchArgs: ['-s', 'read-only', '-c', 'approval_policy=untrusted', '--approve-for-me'] @@ -309,27 +302,6 @@ describe('codex structured launch resolution', () => { )({ identity: IDENTITY }) expect(launch.args).toEqual(['app-server']) - expect(launch.permissionPolicy).toEqual({ - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' - }) - }) - - it('keeps Yolo when the Arguments also state a sandbox', async () => { - const launch = await resolverFor( - record(), - undefined, - undefined, - { codex: '--dangerously-bypass-approvals-and-sandbox' }, - () => ['--dangerously-bypass-approvals-and-sandbox', '-s', 'read-only'] - )({ identity: IDENTITY }) - - expect(launch.permissionPolicy).toEqual({ - approvalPolicy: 'never', - sandbox: 'danger-full-access', - approvalsReviewer: 'user' - }) }) it('pins resume to the rollout file that proved the durable thread', async () => { diff --git a/src/main/codex/codex-structured-permission-policy.test.ts b/src/main/codex/codex-structured-permission-policy.test.ts index 3eba2df436fe..67d08f3f109c 100644 --- a/src/main/codex/codex-structured-permission-policy.test.ts +++ b/src/main/codex/codex-structured-permission-policy.test.ts @@ -1,15 +1,11 @@ import { describe, expect, it } from 'vitest' import { codexStructuredPermissionPolicyForSettings } from './codex-structured-permission-policy' -const BYPASS = { approvalPolicy: 'never', sandbox: 'danger-full-access', approvalsReviewer: 'user' } +const BYPASS = { approvalPolicy: 'never', sandbox: 'danger-full-access' } // Approvals on, writes confined to the workspace. Verified against codex 0.153.4: both values are // accepted on thread/start and thread/resume, and the reply echoes them back as the effective // policy even when the home's config.toml asks for `never` / `danger-full-access`. -const MANUAL = { - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' -} +const MANUAL = { approvalPolicy: 'on-request', sandbox: 'workspace-write' } describe('codexStructuredPermissionPolicyForSettings', () => { it('bypasses when the user has never opened Agent settings', () => { @@ -65,31 +61,8 @@ describe('codexStructuredPermissionPolicyForSettings', () => { } }) - // A resume keeps the reviewer the thread last ran with unless the request names one. - it('states Codex default reviewer for both postures', () => { - for (const codex of ['', '--dangerously-bypass-approvals-and-sandbox']) { - expect( - codexStructuredPermissionPolicyForSettings({ agentDefaultArgs: { codex } }) - .approvalsReviewer, - codex - ).toBe('user') - } - }) - - // Arguments' own permission options are dropped; only the toggle's posture is read. - it('ignores the permission options the Arguments state', () => { - expect( - codexStructuredPermissionPolicyForSettings({ - agentDefaultArgs: { codex: '-s read-only -a untrusted --approve-for-me' } - }) - ).toEqual(MANUAL) - expect( - codexStructuredPermissionPolicyForSettings({ - agentDefaultArgs: { codex: '--dangerously-bypass-approvals-and-sandbox -s read-only' } - }) - ).toEqual(BYPASS) - }) - + // The passthrough that used to carry these to app-server is gone on purpose; only the + // permission posture is derived, and nothing else from the field reaches argv. it('carries nothing but the permission posture out of the arguments field', () => { expect( codexStructuredPermissionPolicyForSettings({ diff --git a/src/main/codex/codex-structured-permission-policy.ts b/src/main/codex/codex-structured-permission-policy.ts index 9a6c4572757d..3dbf5a07085c 100644 --- a/src/main/codex/codex-structured-permission-policy.ts +++ b/src/main/codex/codex-structured-permission-policy.ts @@ -1,17 +1,12 @@ import type { GlobalSettings } from '../../shared/global-settings-types' import { resolvedTuiAgentArgsBypassPermissions } from '../../shared/tui-agent-launch-defaults' -export type CodexStructuredPermissionPolicy = ( +export type CodexStructuredPermissionPolicy = | { approvalPolicy: 'never'; sandbox: 'danger-full-access' } | { approvalPolicy: 'on-request'; sandbox: 'workspace-write' } -) & { approvalsReviewer: 'user' } /** Yolo: no approval prompts, no sandbox. */ -const BYPASS_POLICY = { - approvalPolicy: 'never', - sandbox: 'danger-full-access', - approvalsReviewer: 'user' -} as const +const BYPASS_POLICY = { approvalPolicy: 'never', sandbox: 'danger-full-access' } as const /** * Manual: approvals on, writes confined to the workspace. @@ -28,15 +23,8 @@ const BYPASS_POLICY = { * Why `workspace-write` and not codex's built-in `read-only`: read-only would override a * deliberate `sandbox_mode = "workspace-write"` and make every file write in a Manual session * need an approval it did not need before. This still resets Yolo's `danger-full-access`. - * - * The reviewer is stated too, as Codex's default `user`: a resume otherwise keeps the reviewer - * the thread last ran with, and a fresh thread takes the one `config.toml` names. */ -const MANUAL_POLICY = { - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' -} as const +const MANUAL_POLICY = { approvalPolicy: 'on-request', sandbox: 'workspace-write' } as const /** * The Agent Permissions setting as app-server thread policy. diff --git a/src/main/codex/codex-structured-thread-open.test.ts b/src/main/codex/codex-structured-thread-open.test.ts index cd2efb48f352..efd32b666cd5 100644 --- a/src/main/codex/codex-structured-thread-open.test.ts +++ b/src/main/codex/codex-structured-thread-open.test.ts @@ -22,8 +22,7 @@ describe('openCodexThread', () => { const connection = connectionFor(request) const permissionPolicy = { approvalPolicy: 'never' as const, - sandbox: 'danger-full-access' as const, - approvalsReviewer: 'user' as const + sandbox: 'danger-full-access' as const } await openCodexThread( @@ -40,12 +39,7 @@ describe('openCodexThread', () => { expect(request).toHaveBeenNthCalledWith( 1, 'thread/start', - { - cwd: '/workspace', - approvalPolicy: 'never', - sandbox: 'danger-full-access', - approvalsReviewer: 'user' - }, + { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access' }, { timeoutMs: 2_000 } ) expect(request).toHaveBeenNthCalledWith( @@ -56,7 +50,6 @@ describe('openCodexThread', () => { cwd: '/workspace', approvalPolicy: 'never', sandbox: 'danger-full-access', - approvalsReviewer: 'user', excludeTurns: true }, { timeoutMs: 2_000 } @@ -82,15 +75,9 @@ describe('openCodexThread', () => { ) const params = request.mock.calls[0]?.[1] ?? {} - expect(params).toMatchObject({ - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' - }) + expect(params).toMatchObject({ approvalPolicy: 'on-request', sandbox: 'workspace-write' }) // Absence is the bug, so assert the keys are carried, not merely that they are not Yolo. - expect(Object.keys(params)).toEqual( - expect.arrayContaining(['approvalPolicy', 'sandbox', 'approvalsReviewer']) - ) + expect(Object.keys(params)).toEqual(expect.arrayContaining(['approvalPolicy', 'sandbox'])) }) it('preserves an explicitly reported service tier, including Standard', async () => { diff --git a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts index 937425378587..dd81e303b41d 100644 --- a/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts +++ b/src/main/runtime/orca-runtime-structured-launch-settings-wiring.test.ts @@ -37,19 +37,6 @@ describe('execution host structured launch settings wiring', () => { settings.agentDefaultArgs = { claude: '--model second', codex: '' } expect(await deps?.resolveLaunchArgs?.('claude')).toEqual(['--model', 'second']) expect(await deps?.resolveLaunchArgs?.('codex')).toEqual([]) - // Only the setting decides; a sandbox in the Arguments doesn't reach the thread policy. - settings.agentDefaultArgs = { codex: '-s read-only' } - expect(deps?.resolveCodexPermissionPolicy?.()).toEqual({ - approvalPolicy: 'on-request', - sandbox: 'workspace-write', - approvalsReviewer: 'user' - }) - settings.agentDefaultArgs = { codex: '--dangerously-bypass-approvals-and-sandbox' } - expect(deps?.resolveCodexPermissionPolicy?.()).toEqual({ - approvalPolicy: 'never', - sandbox: 'danger-full-access', - approvalsReviewer: 'user' - }) const notRunnable = expect.objectContaining({ reason: 'agentCommandNotRunnable' }) settings.agentCmdOverrides = { claude: 'wrapper --arg' } expect(() => deps?.resolveClaudeCommand?.()).toThrow(notRunnable) diff --git a/src/renderer/src/components/settings/AgentsPane.tsx b/src/renderer/src/components/settings/AgentsPane.tsx index 790e0098584c..b4d9be1ceea0 100644 --- a/src/renderer/src/components/settings/AgentsPane.tsx +++ b/src/renderer/src/components/settings/AgentsPane.tsx @@ -100,7 +100,7 @@ export function AgentPermissionsSetting({ {translate( 'auto.components.settings.AgentsPane.agentPermissionsTooltip', - "Switching modes doesn't change Arguments you've edited for an agent." + "Doesn't apply to agents where you've overridden launch arguments." )} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 5ff61bc0a72b..5db792565a5d 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -6959,7 +6959,7 @@ "2d133152fa": "No default environment", "agentPermissions": "Agent Permissions", "agentPermissionsInfo": "Agent permissions info", - "agentPermissionsTooltip": "Switching modes doesn't change Arguments you've edited for an agent.", + "agentPermissionsTooltip": "Doesn't apply to agents where you've overridden launch arguments.", "agentPermissionsDescription": "Choose whether Orca launches agents with fewer permission prompts or with manual checks.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manual", diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json index 173f7ff902ec..e005fabc3b66 100644 --- a/src/renderer/src/i18n/locales/es.json +++ b/src/renderer/src/i18n/locales/es.json @@ -6736,7 +6736,7 @@ "2d133152fa": "Sin entorno predeterminado", "agentPermissions": "Permisos de Agents", "agentPermissionsInfo": "Información de permisos de Agents", - "agentPermissionsTooltip": "Cambiar de modo no modifica los argumentos que hayas editado para un agente.", + "agentPermissionsTooltip": "No se aplica a agents con argumentos de lanzamiento sobrescritos.", "agentPermissionsDescription": "Elige si Orca lanza agents con menos prompts de permisos o con checks manuales.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manual", diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json index 2d04c003acf7..b803fe9f630d 100644 --- a/src/renderer/src/i18n/locales/fr.json +++ b/src/renderer/src/i18n/locales/fr.json @@ -6845,7 +6845,7 @@ "2d133152fa": "Aucun environnement par défaut", "agentPermissions": "Permissions des agents", "agentPermissionsInfo": "Infos permissions des agents", - "agentPermissionsTooltip": "Changer de mode ne modifie pas les arguments que vous avez modifiés pour un agent.", + "agentPermissionsTooltip": "Ne s'applique pas aux agents dont vous avez remplacé les arguments de lancement.", "agentPermissionsDescription": "Choisissez si Orca lance les agents avec moins de demandes de permission ou avec des vérifications manuelles.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "Manuel", diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json index 9efb2c13ac65..eccd25079000 100644 --- a/src/renderer/src/i18n/locales/ja.json +++ b/src/renderer/src/i18n/locales/ja.json @@ -6721,7 +6721,7 @@ "2d133152fa": "デフォルト環境なし", "agentPermissions": "Agent の権限", "agentPermissionsInfo": "Agent 権限情報", - "agentPermissionsTooltip": "モードを切り替えても、Agent ごとに編集した引数は変更されません。", + "agentPermissionsTooltip": "起動引数を上書きしている Agent には適用されません。", "agentPermissionsDescription": "Orca が権限プロンプトの少ない Agent を起動するか、手動チェックを含む Agent を起動するかを選択します。", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "手動", diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json index 579ee90e0847..3af6c5011774 100644 --- a/src/renderer/src/i18n/locales/ko.json +++ b/src/renderer/src/i18n/locales/ko.json @@ -6721,7 +6721,7 @@ "2d133152fa": "기본 환경 없음", "agentPermissions": "Agent 권한", "agentPermissionsInfo": "Agent 권한 정보", - "agentPermissionsTooltip": "모드를 전환해도 에이전트별로 수정한 인수는 변경되지 않습니다.", + "agentPermissionsTooltip": "Custom agent arguments stay unchanged when switching modes.", "agentPermissionsDescription": "Orca가 agents를 실행할 때 권한 프롬프트를 줄일지, 수동 확인을 유지할지 선택합니다.", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "수동", diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json index 01ce50184435..77e7e718c4af 100644 --- a/src/renderer/src/i18n/locales/zh.json +++ b/src/renderer/src/i18n/locales/zh.json @@ -6721,7 +6721,7 @@ "2d133152fa": "无默认环境", "agentPermissions": "智能体权限", "agentPermissionsInfo": "智能体权限信息", - "agentPermissionsTooltip": "切换模式不会更改你为智能体编辑过的参数。", + "agentPermissionsTooltip": "切换模式时,自定义智能体参数保持不变。", "agentPermissionsDescription": "选择 Orca 是以更少的权限提示启动智能体,还是手动检查。", "agentPermissionsYolo": "Yolo", "agentPermissionsManual": "手动", From 787a3da6f05b7ac5355cea85a007502e7a179703 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 7 Oct 2026 00:17:53 -0700 Subject: [PATCH 6/6] test(native-chat): the Windows shim probe test expects the model probe's --disable plugins --- src/main/codex/codex-short-lived-app-server-spawn.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/codex/codex-short-lived-app-server-spawn.test.ts b/src/main/codex/codex-short-lived-app-server-spawn.test.ts index d3dfe7149744..f6d044d3f247 100644 --- a/src/main/codex/codex-short-lived-app-server-spawn.test.ts +++ b/src/main/codex/codex-short-lived-app-server-spawn.test.ts @@ -62,6 +62,11 @@ describe('short-lived Codex app-server invocations on Windows', () => { }) await probe('C:\\homes\\a') expect(invocations[0]?.command).toBe(NPM_CODEX_SHIM) - expect(invocations[0]?.args).toEqual(['app-server', ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS]) + expect(invocations[0]?.args).toEqual([ + 'app-server', + ...CODEX_SHORT_LIVED_PROBE_CONFIG_ARGS, + '--disable', + 'plugins' + ]) }) })