From c7e74b027ed08a9b65c0c0cc834feca1d11f55f3 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 11 May 2026 22:37:23 +0200 Subject: [PATCH 1/3] feat(persona-kit): add dangerouslyBypassApprovalsAndSandbox HarnessSettings The two-flag form (--sandbox danger-full-access + --ask-for-approval never) still trips codex's interactive "are you sure?" startup confirmation. The combined --dangerously-bypass-approvals-and-sandbox flag suppresses it. Expose it as a first-class HarnessSettings field, mutually exclusive with sandboxMode / approvalPolicy / workspaceWriteNetworkAccess at parse time so personas cannot half-set both shapes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../persona-kit/src/interactive-spec.test.ts | 21 ++++++++++ packages/persona-kit/src/interactive-spec.ts | 36 ++++++++++------- packages/persona-kit/src/parse.test.ts | 40 +++++++++++++++++++ packages/persona-kit/src/parse.ts | 20 +++++++++- packages/persona-kit/src/types.ts | 9 +++++ 5 files changed, 111 insertions(+), 15 deletions(-) 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..cbc9657 100644 --- a/packages/persona-kit/src/interactive-spec.ts +++ b/packages/persona-kit/src/interactive-spec.ts @@ -86,7 +86,8 @@ function hasCodexLaunchSettings(settings: HarnessSettings | undefined): boolean settings.sandboxMode || settings.approvalPolicy || settings.workspaceWriteNetworkAccess !== undefined || - settings.webSearch + settings.webSearch || + settings.dangerouslyBypassApprovalsAndSandbox ); } @@ -249,19 +250,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'); diff --git a/packages/persona-kit/src/parse.test.ts b/packages/persona-kit/src/parse.test.ts index 10717ec..8ba865f 100644 --- a/packages/persona-kit/src/parse.test.ts +++ b/packages/persona-kit/src/parse.test.ts @@ -183,6 +183,46 @@ test('parseHarnessSettings accepts optional codex fields and rejects bad ones', ); }); +test('parseHarnessSettings accepts dangerouslyBypassApprovalsAndSandbox alone', () => { + const ok = parseHarnessSettings( + { + reasoning: 'high', + timeoutSeconds: 60, + dangerouslyBypassApprovalsAndSandbox: true + }, + 'rt' + ); + assert.equal(ok.dangerouslyBypassApprovalsAndSandbox, true); +}); + +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 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..151398f 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,23 @@ 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`); + } + if (dangerouslyBypassApprovalsAndSandbox) { + 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 { From 9fb0eabce4baa0dbae73cd09d12bc11f35847e28 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 11 May 2026 22:43:21 +0200 Subject: [PATCH 2/3] fix(persona-kit): tighten dangerouslyBypass field handling per PR review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address feedback on #86: - hasCodexLaunchSettings now treats an explicit `false` as "field present" via !== undefined, matching workspaceWriteNetworkAccess's shape semantics. - Claude and opencode warning enumerations name the new field so the message lines up with what the predicate actually detects. - parseHarnessSettings enforces mutual exclusion whenever the field is present, not only when true — co-declaring the two-flag fields with an explicit `false` was still a contradictory shape. - New regression test covers the explicit-false conflict path. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/persona-kit/src/interactive-spec.ts | 9 +++++--- packages/persona-kit/src/parse.test.ts | 17 +++++++++++++++ packages/persona-kit/src/parse.ts | 22 +++++++++++--------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/packages/persona-kit/src/interactive-spec.ts b/packages/persona-kit/src/interactive-spec.ts index cbc9657..5f18cb9 100644 --- a/packages/persona-kit/src/interactive-spec.ts +++ b/packages/persona-kit/src/interactive-spec.ts @@ -87,10 +87,13 @@ function hasCodexLaunchSettings(settings: HarnessSettings | undefined): boolean settings.approvalPolicy || settings.workspaceWriteNetworkAccess !== undefined || settings.webSearch || - settings.dangerouslyBypassApprovalsAndSandbox + 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. @@ -230,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: [] }; @@ -300,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 8ba865f..6c3b0c8 100644 --- a/packages/persona-kit/src/parse.test.ts +++ b/packages/persona-kit/src/parse.test.ts @@ -212,6 +212,23 @@ test('parseHarnessSettings rejects dangerouslyBypassApprovalsAndSandbox with con } }); +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( () => diff --git a/packages/persona-kit/src/parse.ts b/packages/persona-kit/src/parse.ts index 151398f..c894ec2 100644 --- a/packages/persona-kit/src/parse.ts +++ b/packages/persona-kit/src/parse.ts @@ -137,16 +137,18 @@ export function parseHarnessSettings(value: unknown, context: string): HarnessSe if (typeof dangerouslyBypassApprovalsAndSandbox !== 'boolean') { throw new Error(`${context}.dangerouslyBypassApprovalsAndSandbox must be a boolean`); } - if (dangerouslyBypassApprovalsAndSandbox) { - 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(', ')}` - ); - } + // 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; } From a86e8426e7abdb03499d9b3e044800556867aaf9 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Mon, 11 May 2026 22:47:02 +0200 Subject: [PATCH 3/3] test(persona-kit): cover explicit `false` happy-path for bypass field Lock in that `dangerouslyBypassApprovalsAndSandbox: false` parses and is preserved on output, matching the field-presence semantics introduced in 9fb0eab. Addresses PR #86 review feedback. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/persona-kit/src/parse.test.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/persona-kit/src/parse.test.ts b/packages/persona-kit/src/parse.test.ts index 6c3b0c8..eea7a43 100644 --- a/packages/persona-kit/src/parse.test.ts +++ b/packages/persona-kit/src/parse.test.ts @@ -184,15 +184,17 @@ test('parseHarnessSettings accepts optional codex fields and rejects bad ones', }); test('parseHarnessSettings accepts dangerouslyBypassApprovalsAndSandbox alone', () => { - const ok = parseHarnessSettings( - { - reasoning: 'high', - timeoutSeconds: 60, - dangerouslyBypassApprovalsAndSandbox: true - }, - 'rt' - ); - assert.equal(ok.dangerouslyBypassApprovalsAndSandbox, true); + 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', () => {