diff --git a/packages/persona-kit/src/interactive-spec.test.ts b/packages/persona-kit/src/interactive-spec.test.ts index 1ef1417..8e89a0f 100644 --- a/packages/persona-kit/src/interactive-spec.test.ts +++ b/packages/persona-kit/src/interactive-spec.test.ts @@ -127,6 +127,27 @@ test('codex translates sandbox harness settings to launch flags', () => { ]); }); +test('codex emits the single bypass flag when dangerouslyBypassApprovalsAndSandbox is set', () => { + const result = buildInteractiveSpec({ + harness: 'codex', + personaId: 'test-persona', + model: 'openai-codex/gpt-5.3-codex', + systemPrompt: 'x', + harnessSettings: { + reasoning: 'high', + timeoutSeconds: 1200, + dangerouslyBypassApprovalsAndSandbox: true, + webSearch: true + } + }); + assert.deepEqual(result.args, [ + '-m', + 'gpt-5.3-codex', + '--dangerously-bypass-approvals-and-sandbox', + '--search' + ]); +}); + test('codex translates http mcpServers into --config mcp_servers.* args', () => { const result = buildInteractiveSpec({ harness: 'codex', diff --git a/packages/persona-kit/src/interactive-spec.ts b/packages/persona-kit/src/interactive-spec.ts index 3e93635..5f18cb9 100644 --- a/packages/persona-kit/src/interactive-spec.ts +++ b/packages/persona-kit/src/interactive-spec.ts @@ -86,10 +86,14 @@ function hasCodexLaunchSettings(settings: HarnessSettings | undefined): boolean settings.sandboxMode || settings.approvalPolicy || settings.workspaceWriteNetworkAccess !== undefined || - settings.webSearch + settings.webSearch || + settings.dangerouslyBypassApprovalsAndSandbox !== undefined ); } +const CODEX_ONLY_WARNING = + 'persona declares codex-only harnessSettings but the {harness} harness ignores sandboxMode, approvalPolicy, workspaceWriteNetworkAccess, webSearch, and dangerouslyBypassApprovalsAndSandbox.'; + function toTomlBasicString(value: string): string { // JSON string escaping is compatible with TOML basic strings. @@ -229,7 +233,7 @@ export function buildInteractiveSpec(input: BuildInteractiveSpecInput): Interact } if (hasCodexLaunchSettings(harnessSettings)) { warnings.push( - 'persona declares codex-only harnessSettings but the claude harness ignores sandboxMode, approvalPolicy, workspaceWriteNetworkAccess, and webSearch.' + CODEX_ONLY_WARNING.replace('{harness}', 'claude') ); } return { bin: 'claude', args, initialPrompt: null, warnings, configFiles: [] }; @@ -249,19 +253,26 @@ export function buildInteractiveSpec(input: BuildInteractiveSpecInput): Interact if (mcpServers && Object.keys(mcpServers).length > 0) { appendCodexMcpServerArgs(args, mcpServers, warnings); } - if (harnessSettings?.sandboxMode) { - args.push('--sandbox', harnessSettings.sandboxMode); - } - if (harnessSettings?.approvalPolicy) { - args.push('--ask-for-approval', harnessSettings.approvalPolicy); - } - if (harnessSettings?.workspaceWriteNetworkAccess !== undefined) { - args.push( - '-c', - `sandbox_workspace_write.network_access=${String( - harnessSettings.workspaceWriteNetworkAccess - )}` - ); + if (harnessSettings?.dangerouslyBypassApprovalsAndSandbox) { + // Single combined flag — collapses "no sandbox + never ask" and + // suppresses codex's interactive "are you sure?" startup + // confirmation. The two-flag form below still prompts. + args.push('--dangerously-bypass-approvals-and-sandbox'); + } else { + if (harnessSettings?.sandboxMode) { + args.push('--sandbox', harnessSettings.sandboxMode); + } + if (harnessSettings?.approvalPolicy) { + args.push('--ask-for-approval', harnessSettings.approvalPolicy); + } + if (harnessSettings?.workspaceWriteNetworkAccess !== undefined) { + args.push( + '-c', + `sandbox_workspace_write.network_access=${String( + harnessSettings.workspaceWriteNetworkAccess + )}` + ); + } } if (harnessSettings?.webSearch) { args.push('--search'); @@ -292,7 +303,7 @@ export function buildInteractiveSpec(input: BuildInteractiveSpecInput): Interact } if (hasCodexLaunchSettings(harnessSettings)) { warnings.push( - 'persona declares codex-only harnessSettings but the opencode harness ignores sandboxMode, approvalPolicy, workspaceWriteNetworkAccess, and webSearch.' + CODEX_ONLY_WARNING.replace('{harness}', 'opencode') ); } // opencode resolves a persona's system prompt + model through its own diff --git a/packages/persona-kit/src/parse.test.ts b/packages/persona-kit/src/parse.test.ts index 10717ec..eea7a43 100644 --- a/packages/persona-kit/src/parse.test.ts +++ b/packages/persona-kit/src/parse.test.ts @@ -183,6 +183,65 @@ test('parseHarnessSettings accepts optional codex fields and rejects bad ones', ); }); +test('parseHarnessSettings accepts dangerouslyBypassApprovalsAndSandbox alone', () => { + for (const value of [true, false]) { + const ok = parseHarnessSettings( + { + reasoning: 'high', + timeoutSeconds: 60, + dangerouslyBypassApprovalsAndSandbox: value + }, + 'rt' + ); + assert.equal(ok.dangerouslyBypassApprovalsAndSandbox, value); + } +}); + +test('parseHarnessSettings rejects dangerouslyBypassApprovalsAndSandbox with conflicting fields', () => { + for (const conflict of ['sandboxMode', 'approvalPolicy', 'workspaceWriteNetworkAccess']) { + const overlay: Record = { + reasoning: 'high', + timeoutSeconds: 60, + dangerouslyBypassApprovalsAndSandbox: true + }; + if (conflict === 'sandboxMode') overlay.sandboxMode = 'workspace-write'; + if (conflict === 'approvalPolicy') overlay.approvalPolicy = 'never'; + if (conflict === 'workspaceWriteNetworkAccess') overlay.workspaceWriteNetworkAccess = true; + assert.throws( + () => parseHarnessSettings(overlay, 'rt'), + new RegExp(`mutually exclusive with: .*${conflict}`) + ); + } +}); + +test('parseHarnessSettings rejects dangerouslyBypassApprovalsAndSandbox:false with conflicting fields', () => { + for (const conflict of ['sandboxMode', 'approvalPolicy', 'workspaceWriteNetworkAccess']) { + const overlay: Record = { + reasoning: 'high', + timeoutSeconds: 60, + dangerouslyBypassApprovalsAndSandbox: false + }; + if (conflict === 'sandboxMode') overlay.sandboxMode = 'workspace-write'; + if (conflict === 'approvalPolicy') overlay.approvalPolicy = 'never'; + if (conflict === 'workspaceWriteNetworkAccess') overlay.workspaceWriteNetworkAccess = true; + assert.throws( + () => parseHarnessSettings(overlay, 'rt'), + new RegExp(`mutually exclusive with: .*${conflict}`) + ); + } +}); + +test('parseHarnessSettings rejects non-boolean dangerouslyBypassApprovalsAndSandbox', () => { + assert.throws( + () => + parseHarnessSettings( + { reasoning: 'high', timeoutSeconds: 60, dangerouslyBypassApprovalsAndSandbox: 'yes' }, + 'rt' + ), + /dangerouslyBypassApprovalsAndSandbox must be a boolean/ + ); +}); + test('parseTags rejects empty arrays and unknown tags', () => { assert.throws(() => parseTags([], 'tags'), /must be a non-empty array/); assert.throws(() => parseTags(['nonsense-tag'], 'tags'), /tags\[0\] must be one of:/); diff --git a/packages/persona-kit/src/parse.ts b/packages/persona-kit/src/parse.ts index e00c660..c894ec2 100644 --- a/packages/persona-kit/src/parse.ts +++ b/packages/persona-kit/src/parse.ts @@ -95,7 +95,8 @@ export function parseHarnessSettings(value: unknown, context: string): HarnessSe sandboxMode, approvalPolicy, workspaceWriteNetworkAccess, - webSearch + webSearch, + dangerouslyBypassApprovalsAndSandbox } = value; if (!['low', 'medium', 'high'].includes(String(reasoning))) { throw new Error(`${context}.reasoning must be low|medium|high`); @@ -132,6 +133,25 @@ export function parseHarnessSettings(value: unknown, context: string): HarnessSe } out.webSearch = webSearch; } + if (dangerouslyBypassApprovalsAndSandbox !== undefined) { + if (typeof dangerouslyBypassApprovalsAndSandbox !== 'boolean') { + throw new Error(`${context}.dangerouslyBypassApprovalsAndSandbox must be a boolean`); + } + // Reject mixed-shape configs whenever the field is *present*, not only + // when true. Co-declaring sandboxMode/approvalPolicy/workspaceWriteNetworkAccess + // with an explicit `false` is still a contradictory shape — the two-flag + // form and the single-flag form are mutually exclusive concepts. + const conflicts: string[] = []; + if (sandboxMode !== undefined) conflicts.push('sandboxMode'); + if (approvalPolicy !== undefined) conflicts.push('approvalPolicy'); + if (workspaceWriteNetworkAccess !== undefined) conflicts.push('workspaceWriteNetworkAccess'); + if (conflicts.length > 0) { + throw new Error( + `${context}.dangerouslyBypassApprovalsAndSandbox is mutually exclusive with: ${conflicts.join(', ')}` + ); + } + out.dangerouslyBypassApprovalsAndSandbox = dangerouslyBypassApprovalsAndSandbox; + } return out; } diff --git a/packages/persona-kit/src/types.ts b/packages/persona-kit/src/types.ts index 5d45e63..5683ae1 100644 --- a/packages/persona-kit/src/types.ts +++ b/packages/persona-kit/src/types.ts @@ -38,6 +38,15 @@ export interface HarnessSettings { workspaceWriteNetworkAccess?: boolean; /** Enable the Codex live web-search tool for this runtime. */ webSearch?: boolean; + /** + * Emit codex's single `--dangerously-bypass-approvals-and-sandbox` flag, + * which collapses "no sandbox + never ask for approval" and also + * suppresses codex's interactive "are you sure?" startup confirmation. + * Mutually exclusive with `sandboxMode`, `approvalPolicy`, and + * `workspaceWriteNetworkAccess` — those translate to the two-flag form + * which still prompts. + */ + dangerouslyBypassApprovalsAndSandbox?: boolean; } export interface PersonaRuntime {