From 0a32c8b3967dbd6a49222053a22b6a53571dbb81 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 23:08:34 -0700 Subject: [PATCH 1/2] app-server-protocol: mark permission profiles experimental (#19899) ## Why `PermissionProfile` is now the canonical internal permissions representation, but the app-server wire shape is still intentionally unstable while the migration continues. Stable app-server clients should not see or generate code for these fields until the wire format settles. ## What changed - Marks every app-server v2 field that sends `PermissionProfile` as experimental, including `command/exec`, `thread/start`, `thread/resume`, `thread/fork`, and `turn/start` request/response payloads. - Enables per-field experimental inspection for `command/exec`, so `permissionProfile` is gated without making the entire method experimental. - Fixes the generated TypeScript schema filter to be comment-aware. The previous scanner treated apostrophes inside doc comments as string delimiters, so some experimental fields leaked into stable TypeScript even though stable JSON was filtered correctly. ## Verification - `cargo test -p codex-app-server-protocol` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19899). * #19900 * __->__ #19899 --- .../schema/json/ClientRequest.json | 55 -------- .../codex_app_server_protocol.schemas.json | 91 ------------- .../codex_app_server_protocol.v2.schemas.json | 91 ------------- .../schema/json/v2/CommandExecParams.json | 11 -- .../schema/json/v2/ThreadForkParams.json | 11 -- .../schema/json/v2/ThreadForkResponse.json | 12 -- .../schema/json/v2/ThreadResumeParams.json | 11 -- .../schema/json/v2/ThreadResumeResponse.json | 12 -- .../schema/json/v2/ThreadStartParams.json | 11 -- .../schema/json/v2/ThreadStartResponse.json | 12 -- .../schema/json/v2/TurnStartParams.json | 11 -- .../schema/typescript/v2/CommandExecParams.ts | 49 ++----- .../CommandExecutionRequestApprovalParams.ts | 36 ++---- .../schema/typescript/v2/ThreadForkParams.ts | 17 +-- .../typescript/v2/ThreadForkResponse.ts | 16 +-- .../typescript/v2/ThreadResumeParams.ts | 23 +--- .../typescript/v2/ThreadResumeResponse.ts | 16 +-- .../schema/typescript/v2/ThreadStartParams.ts | 15 +-- .../typescript/v2/ThreadStartResponse.ts | 16 +-- .../schema/typescript/v2/TurnStartParams.ts | 15 +-- codex-rs/app-server-protocol/src/export.rs | 120 +++++++++++++++++- .../src/protocol/common.rs | 28 ++++ .../app-server-protocol/src/protocol/v2.rs | 10 +- 23 files changed, 193 insertions(+), 496 deletions(-) diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 079a8a2d76..31cfd542b7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -218,17 +218,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -3271,17 +3260,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { @@ -3687,17 +3665,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -3881,17 +3848,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -4103,17 +4059,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 7e34967470..1c0ace7fd8 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -6536,17 +6536,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -14443,17 +14432,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { @@ -14522,18 +14500,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -15967,17 +15933,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -16056,18 +16011,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -16284,17 +16227,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -16383,18 +16315,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -17100,17 +17020,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/v2/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 406795ff54..cf721d82af 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -3055,17 +3055,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ @@ -12329,17 +12318,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { @@ -12408,18 +12386,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -13853,17 +13819,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -13942,18 +13897,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -14170,17 +14113,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { @@ -14269,18 +14201,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { @@ -14986,17 +14906,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index 7db4d635ee..f29483862c 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -505,17 +505,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Optional full permissions profile for this command.\n\nDefaults to the user's configured permissions when omitted. Cannot be combined with `sandboxPolicy`." - }, "processId": { "description": "Optional client-supplied, connection-scoped process id.\n\nRequired for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up `command/exec/write`, `command/exec/resize`, and `command/exec/terminate` calls. When omitted, buffered execution gets an internal id that is not exposed to the client.", "type": [ diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json index 504dbdc67e..eb8f3bdb4f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -473,17 +473,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the forked thread. Cannot be combined with `sandbox`." - }, "sandbox": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 3ab0077d0a..b5d6b139b2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -2485,18 +2485,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json index 63d0345dae..425fc52a48 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -1328,17 +1328,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for the resumed thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 12c38db5dc..7135a53174 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -2485,18 +2485,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json index bc8d83d2bc..d5e76f05e3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -526,17 +526,6 @@ "null" ] }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Full permissions override for this thread. Cannot be combined with `sandbox`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index 6e1637ec80..5deca9f699 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -2485,18 +2485,6 @@ "modelProvider": { "type": "string" }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "default": null, - "description": "Canonical active permissions view for this thread." - }, "reasoningEffort": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index d866c03152..719a1bedc8 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -829,17 +829,6 @@ "outputSchema": { "description": "Optional JSON Schema used to constrain the final assistant message for this turn." }, - "permissionProfile": { - "anyOf": [ - { - "$ref": "#/definitions/PermissionProfile" - }, - { - "type": "null" - } - ], - "description": "Override the full permissions profile for this turn and subsequent turns. Cannot be combined with `sandboxPolicy`." - }, "personality": { "anyOf": [ { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts index 659974feaf..221a2399c1 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecParams.ts @@ -2,7 +2,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; /** @@ -13,12 +12,10 @@ import type { SandboxPolicy } from "./SandboxPolicy"; * sent only after all `command/exec/outputDelta` notifications for that * connection have been emitted. */ -export type CommandExecParams = { -/** +export type CommandExecParams = {/** * Command argv vector. Empty arrays are rejected. */ -command: Array, -/** +command: Array, /** * Optional client-supplied, connection-scoped process id. * * Required for `tty`, `streamStdin`, `streamStdoutStderr`, and follow-up @@ -26,81 +23,63 @@ command: Array, * `command/exec/terminate` calls. When omitted, buffered execution gets an * internal id that is not exposed to the client. */ -processId?: string | null, -/** +processId?: string | null, /** * Enable PTY mode. * * This implies `streamStdin` and `streamStdoutStderr`. */ -tty?: boolean, -/** +tty?: boolean, /** * Allow follow-up `command/exec/write` requests to write stdin bytes. * * Requires a client-supplied `processId`. */ -streamStdin?: boolean, -/** +streamStdin?: boolean, /** * Stream stdout/stderr via `command/exec/outputDelta` notifications. * * Streamed bytes are not duplicated into the final response and require a * client-supplied `processId`. */ -streamStdoutStderr?: boolean, -/** +streamStdoutStderr?: boolean, /** * Optional per-stream stdout/stderr capture cap in bytes. * * When omitted, the server default applies. Cannot be combined with * `disableOutputCap`. */ -outputBytesCap?: number | null, -/** +outputBytesCap?: number | null, /** * Disable stdout/stderr capture truncation for this request. * * Cannot be combined with `outputBytesCap`. */ -disableOutputCap?: boolean, -/** +disableOutputCap?: boolean, /** * Disable the timeout entirely for this request. * * Cannot be combined with `timeoutMs`. */ -disableTimeout?: boolean, -/** +disableTimeout?: boolean, /** * Optional timeout in milliseconds. * * When omitted, the server default applies. Cannot be combined with * `disableTimeout`. */ -timeoutMs?: number | null, -/** +timeoutMs?: number | null, /** * Optional working directory. Defaults to the server cwd. */ -cwd?: string | null, -/** +cwd?: string | null, /** * Optional environment overrides merged into the server-computed * environment. * * Matching names override inherited values. Set a key to `null` to unset * an inherited variable. */ -env?: { [key in string]?: string | null } | null, -/** +env?: { [key in string]?: string | null } | null, /** * Optional initial PTY size in character cells. Only valid when `tty` is * true. */ -size?: CommandExecTerminalSize | null, -/** +size?: CommandExecTerminalSize | null, /** * Optional sandbox policy for this command. * * Uses the same shape as thread/turn execution sandbox configuration and * defaults to the user's configured policy when omitted. Cannot be * combined with `permissionProfile`. */ -sandboxPolicy?: SandboxPolicy | null, -/** - * Optional full permissions profile for this command. - * - * Defaults to the user's configured permissions when omitted. Cannot be - * combined with `sandboxPolicy`. - */ -permissionProfile?: PermissionProfile | null, }; +sandboxPolicy?: SandboxPolicy | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts index 59da1de945..ca2d0b0aa0 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/CommandExecutionRequestApprovalParams.ts @@ -2,15 +2,12 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; -import type { AdditionalPermissionProfile } from "./AdditionalPermissionProfile"; import type { CommandAction } from "./CommandAction"; -import type { CommandExecutionApprovalDecision } from "./CommandExecutionApprovalDecision"; import type { ExecPolicyAmendment } from "./ExecPolicyAmendment"; import type { NetworkApprovalContext } from "./NetworkApprovalContext"; import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment"; -export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: string, itemId: string, -/** +export type CommandExecutionRequestApprovalParams = {threadId: string, turnId: string, itemId: string, /** * Unique identifier for this specific approval callback. * * For regular shell/unified_exec approvals, this is null. @@ -19,40 +16,25 @@ export type CommandExecutionRequestApprovalParams = { threadId: string, turnId: * one parent `itemId`, so `approvalId` is a distinct opaque callback id * (a UUID) used to disambiguate routing. */ -approvalId?: string | null, -/** +approvalId?: string | null, /** * Optional explanatory reason (e.g. request for network access). */ -reason?: string | null, -/** +reason?: string | null, /** * Optional context for a managed-network approval prompt. */ -networkApprovalContext?: NetworkApprovalContext | null, -/** +networkApprovalContext?: NetworkApprovalContext | null, /** * The command to be executed. */ -command?: string | null, -/** +command?: string | null, /** * The command's working directory. */ -cwd?: AbsolutePathBuf | null, -/** +cwd?: AbsolutePathBuf | null, /** * Best-effort parsed command actions for friendly display. */ -commandActions?: Array | null, -/** - * Optional additional permissions requested for this command. - */ -additionalPermissions?: AdditionalPermissionProfile | null, -/** +commandActions?: Array | null, /** * Optional proposed execpolicy amendment to allow similar commands without prompting. */ -proposedExecpolicyAmendment?: ExecPolicyAmendment | null, -/** +proposedExecpolicyAmendment?: ExecPolicyAmendment | null, /** * Optional proposed network policy amendments (allow/deny host) for future requests. */ -proposedNetworkPolicyAmendments?: Array | null, -/** - * Ordered list of decisions the client may present for this prompt. - */ -availableDecisions?: Array | null, }; +proposedNetworkPolicyAmendments?: Array | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts index f5f3f1878c..a40e406df9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkParams.ts @@ -5,7 +5,6 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -18,27 +17,15 @@ import type { SandboxMode } from "./SandboxMode"; * Prefer using thread_id whenever possible. */ export type ThreadForkParams = {threadId: string, /** - * [UNSTABLE] Specify the rollout path to fork from. - * If specified, the thread_id param will be ignored. - */ -path?: string | null, /** * Configuration overrides for the forked thread, if any. */ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** - * Full permissions override for the forked thread. Cannot be combined - * with `sandbox`. - */ -permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, ephemeral?: boolean, /** * When true, return only thread metadata and live fork state without * populating `thread.turns`. This is useful when the client plans to call * `thread/turns/list` immediately after forking. */ -excludeTurns?: boolean, /** - * If true, persist additional rollout EventMsg variants required to - * reconstruct a richer thread history on subsequent resume/fork/read. - */ -persistExtendedHistory: boolean}; +excludeTurns?: boolean}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts index b69f1da012..1207002514 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -6,26 +6,18 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadForkResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, -/** +export type ThreadForkResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, -/** +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, -/** +approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. New clients should use * `permissionProfile` when present as the canonical active permissions * view. */ -sandbox: SandboxPolicy, -/** - * Canonical active permissions view for this thread. - */ -permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; +sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts index 452126be46..f9821585fb 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeParams.ts @@ -2,12 +2,10 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { Personality } from "../Personality"; -import type { ResponseItem } from "../ResponseItem"; import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; /** @@ -22,32 +20,15 @@ import type { SandboxMode } from "./SandboxMode"; * Prefer using thread_id whenever possible. */ export type ThreadResumeParams = {threadId: string, /** - * [UNSTABLE] FOR CODEX CLOUD - DO NOT USE. - * If specified, the thread will be resumed with the provided history - * instead of loaded from disk. - */ -history?: Array | null, /** - * [UNSTABLE] Specify the rollout path to resume from. - * If specified, the thread_id param will be ignored. - */ -path?: string | null, /** * Configuration overrides for the resumed thread, if any. */ model?: string | null, modelProvider?: string | null, serviceTier?: ServiceTier | null | null, cwd?: string | null, approvalPolicy?: AskForApproval | null, /** * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** - * Full permissions override for the resumed thread. Cannot be combined - * with `sandbox`. - */ -permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, /** * When true, return only thread metadata and live-resume state without * populating `thread.turns`. This is useful when the client plans to call * `thread/turns/list` immediately after resuming. */ -excludeTurns?: boolean, /** - * If true, persist additional rollout EventMsg variants required to - * reconstruct a richer thread history on subsequent resume/fork/read. - */ -persistExtendedHistory: boolean}; +excludeTurns?: boolean}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts index 5ceec7f3fe..20f91b3e91 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -6,26 +6,18 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadResumeResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, -/** +export type ThreadResumeResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, -/** +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, -/** +approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. New clients should use * `permissionProfile` when present as the canonical active permissions * view. */ -sandbox: SandboxPolicy, -/** - * Canonical active permissions view for this thread. - */ -permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; +sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts index 8b9dafec9f..374ac2e681 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartParams.ts @@ -6,7 +6,6 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxMode } from "./SandboxMode"; import type { ThreadStartSource } from "./ThreadStartSource"; @@ -14,16 +13,4 @@ export type ThreadStartParams = {model?: string | null, modelProvider?: string | * Override where approval requests are routed for review on this thread * and subsequent turns. */ -approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, /** - * Full permissions override for this thread. Cannot be combined with - * `sandbox`. - */ -permissionProfile?: PermissionProfile | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null, /** - * If true, opt into emitting raw Responses API items on the event stream. - * This is for internal use only (e.g. Codex Cloud). - */ -experimentalRawEvents: boolean, /** - * If true, persist additional rollout EventMsg variants required to - * reconstruct a richer thread history on resume/fork/read. - */ -persistExtendedHistory: boolean}; +approvalsReviewer?: ApprovalsReviewer | null, sandbox?: SandboxMode | null, config?: { [key in string]?: JsonValue } | null, serviceName?: string | null, baseInstructions?: string | null, developerInstructions?: string | null, personality?: Personality | null, ephemeral?: boolean | null, sessionStartSource?: ThreadStartSource | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts index 61d268afe8..87e15411a2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -6,26 +6,18 @@ import type { ReasoningEffort } from "../ReasoningEffort"; import type { ServiceTier } from "../ServiceTier"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { Thread } from "./Thread"; -export type ThreadStartResponse = { thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, -/** +export type ThreadStartResponse = {thread: Thread, model: string, modelProvider: string, serviceTier: ServiceTier | null, cwd: AbsolutePathBuf, /** * Instruction source files currently loaded for this thread. */ -instructionSources: Array, approvalPolicy: AskForApproval, -/** +instructionSources: Array, approvalPolicy: AskForApproval, /** * Reviewer currently used for approval requests on this thread. */ -approvalsReviewer: ApprovalsReviewer, -/** +approvalsReviewer: ApprovalsReviewer, /** * Legacy sandbox policy retained for compatibility. New clients should use * `permissionProfile` when present as the canonical active permissions * view. */ -sandbox: SandboxPolicy, -/** - * Canonical active permissions view for this thread. - */ -permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; +sandbox: SandboxPolicy, reasoningEffort: ReasoningEffort | null}; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts index 3d12e6001c..4af17115c8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/TurnStartParams.ts @@ -1,7 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { CollaborationMode } from "../CollaborationMode"; import type { Personality } from "../Personality"; import type { ReasoningEffort } from "../ReasoningEffort"; import type { ReasoningSummary } from "../ReasoningSummary"; @@ -9,7 +8,6 @@ import type { ServiceTier } from "../ServiceTier"; import type { JsonValue } from "../serde_json/JsonValue"; import type { ApprovalsReviewer } from "./ApprovalsReviewer"; import type { AskForApproval } from "./AskForApproval"; -import type { PermissionProfile } from "./PermissionProfile"; import type { SandboxPolicy } from "./SandboxPolicy"; import type { UserInput } from "./UserInput"; @@ -27,10 +25,6 @@ approvalsReviewer?: ApprovalsReviewer | null, /** * Override the sandbox policy for this turn and subsequent turns. */ sandboxPolicy?: SandboxPolicy | null, /** - * Override the full permissions profile for this turn and subsequent - * turns. Cannot be combined with `sandboxPolicy`. - */ -permissionProfile?: PermissionProfile | null, /** * Override the model for this turn and subsequent turns. */ model?: string | null, /** @@ -49,11 +43,4 @@ personality?: Personality | null, /** * Optional JSON Schema used to constrain the final assistant message for * this turn. */ -outputSchema?: JsonValue | null, /** - * EXPERIMENTAL - Set a pre-set collaboration mode. - * Takes precedence over model, reasoning_effort, and developer instructions if set. - * - * For `collaboration_mode.settings.developer_instructions`, `null` means - * "use the built-in instructions for the selected mode". - */ -collaborationMode?: CollaborationMode | null}; +outputSchema?: JsonValue | null}; diff --git a/codex-rs/app-server-protocol/src/export.rs b/codex-rs/app-server-protocol/src/export.rs index 96bb8d17a9..0f9b33671b 100644 --- a/codex-rs/app-server-protocol/src/export.rs +++ b/codex-rs/app-server-protocol/src/export.rs @@ -736,11 +736,11 @@ fn find_top_level_brace_span(input: &str) -> Option<(usize, usize)> { let mut state = ScanState::default(); let mut open_index = None; for (index, ch) in input.char_indices() { - if !state.in_string() && ch == '{' && state.depth.is_top_level() { + if !state.in_ignored_syntax() && ch == '{' && state.depth.is_top_level() { open_index = Some(index); } state.observe(ch); - if !state.in_string() + if !state.in_ignored_syntax() && ch == '}' && state.depth.is_top_level() && let Some(open) = open_index @@ -760,7 +760,7 @@ fn split_top_level_multi(input: &str, delimiters: &[char]) -> Vec { let mut start = 0usize; let mut parts = Vec::new(); for (index, ch) in input.char_indices() { - if !state.in_string() && state.depth.is_top_level() && delimiters.contains(&ch) { + if !state.in_ignored_syntax() && state.depth.is_top_level() && delimiters.contains(&ch) { let part = input[start..index].trim(); if !part.is_empty() { parts.push(part.to_string()); @@ -882,22 +882,58 @@ struct ScanState { depth: Depth, string_delim: Option, escape: bool, + block_comment: bool, + line_comment: bool, + previous_char: Option, } impl ScanState { fn observe(&mut self, ch: char) { + if self.line_comment { + if ch == '\n' { + self.line_comment = false; + } + self.previous_char = Some(ch); + return; + } + + if self.block_comment { + if self.previous_char == Some('*') && ch == '/' { + self.block_comment = false; + self.previous_char = None; + } else { + self.previous_char = Some(ch); + } + return; + } + if let Some(delim) = self.string_delim { if self.escape { self.escape = false; + self.previous_char = Some(ch); return; } if ch == '\\' { self.escape = true; + self.previous_char = Some(ch); return; } if ch == delim { self.string_delim = None; } + self.previous_char = Some(ch); + return; + } + + if self.previous_char == Some('/') && ch == '/' { + self.line_comment = true; + self.previous_char = Some(ch); + return; + } + + if self.previous_char == Some('/') && ch == '*' { + self.block_comment = true; + self.previous_char = Some(ch); return; } @@ -919,10 +955,11 @@ impl ScanState { } _ => {} } + self.previous_char = Some(ch); } - fn in_string(&self) -> bool { - self.string_delim.is_some() + fn in_ignored_syntax(&self) -> bool { + self.string_delim.is_some() || self.block_comment || self.line_comment } } @@ -2694,6 +2731,79 @@ export type Config = { stableField: Keep, unstableField: string | null } & ({ [k Ok(()) } + #[test] + fn experimental_type_fields_ts_filter_handles_generated_command_params_shape() -> Result<()> { + let output_dir = std::env::temp_dir().join(format!("codex_ts_filter_{}", Uuid::now_v7())); + fs::create_dir_all(&output_dir)?; + + struct TempDirGuard(PathBuf); + + impl Drop for TempDirGuard { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } + } + + let _guard = TempDirGuard(output_dir.clone()); + let path = output_dir.join("CommandExecParams.ts"); + let content = r#"import type { CommandExecTerminalSize } from "./CommandExecTerminalSize"; +import type { PermissionProfile } from "./PermissionProfile"; +import type { SandboxPolicy } from "./SandboxPolicy"; + +export type CommandExecParams = {/** + * Command argv vector. Empty arrays are rejected. + */ +command: Array, /** + * Optional environment overrides merged into the server-computed + * environment. + */ +env?: { [key in string]?: string | null } | null, /** + * Optional initial PTY size in character cells. Only valid when `tty` is + * true. + */ +size?: CommandExecTerminalSize | null, /** + * Optional sandbox policy for this command. + * + * Uses the same shape as thread/turn execution sandbox configuration and + * defaults to the user's configured policy when omitted. Cannot be + * combined with `permissionProfile`. + */ +sandboxPolicy?: SandboxPolicy | null, +/** + * Optional full permissions profile for this command. + * + * Defaults to the user's configured permissions when omitted. Cannot be + * combined with `sandboxPolicy`. + */ +permissionProfile?: PermissionProfile | null}; +"#; + fs::write(&path, content)?; + + static CUSTOM_FIELD: crate::experimental_api::ExperimentalField = + crate::experimental_api::ExperimentalField { + type_name: "CommandExecParams", + field_name: "permissionProfile", + reason: "command/exec.permissionProfile", + }; + filter_experimental_type_fields_ts(&output_dir, &[&CUSTOM_FIELD])?; + + let filtered = fs::read_to_string(&path)?; + assert_eq!( + filtered.contains("permissionProfile?: PermissionProfile"), + false + ); + assert_eq!( + filtered.contains(r#"import type { PermissionProfile } from "./PermissionProfile";"#), + false + ); + assert_eq!(filtered.contains("sandboxPolicy?: SandboxPolicy"), true); + assert_eq!( + filtered.contains(r#"import type { SandboxPolicy } from "./SandboxPolicy";"#), + true + ); + Ok(()) + } + #[test] fn stable_schema_filter_removes_mock_experimental_method() -> Result<()> { let output_dir = std::env::temp_dir().join(format!("codex_schema_{}", Uuid::now_v7())); diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 016d6e16b8..167435ac88 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -581,6 +581,7 @@ client_request_definitions! { /// Execute a standalone command (argv vector) under the server's sandbox. OneOffCommandExec => "command/exec" { params: v2::CommandExecParams, + inspect_params: true, response: v2::CommandExecResponse, }, /// Write stdin bytes to a running `command/exec` session or close stdin. @@ -2049,6 +2050,33 @@ mod tests { let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); assert_eq!(reason, Some("mock/experimentalMethod")); } + + #[test] + fn command_exec_permission_profile_is_marked_experimental() { + let request = ClientRequest::OneOffCommandExec { + request_id: RequestId::Integer(1), + params: v2::CommandExecParams { + command: vec!["pwd".to_string()], + process_id: None, + tty: false, + stream_stdin: false, + stream_stdout_stderr: false, + output_bytes_cap: None, + disable_output_cap: false, + disable_timeout: false, + timeout_ms: None, + cwd: None, + env: None, + size: None, + sandbox_policy: None, + permission_profile: Some(v2::PermissionProfile::Disabled), + }, + }; + + let reason = crate::experimental_api::ExperimentalApi::experimental_reason(&request); + assert_eq!(reason, Some("command/exec.permissionProfile")); + } + #[test] fn thread_realtime_start_is_marked_experimental() { let request = ClientRequest::ThreadRealtimeStart { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index e1c941019d..bfb1eaac07 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -3163,7 +3163,7 @@ pub struct CommandExecTerminalSize { /// The final `command/exec` response is deferred until the process exits and is /// sent only after all `command/exec/outputDelta` notifications for that /// connection have been emitted. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct CommandExecParams { @@ -3242,6 +3242,7 @@ pub struct CommandExecParams { /// /// Defaults to the user's configured permissions when omitted. Cannot be /// combined with `sandboxPolicy`. + #[experimental("command/exec.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, } @@ -3364,6 +3365,7 @@ pub struct ThreadStartParams { pub sandbox: Option, /// Full permissions override for this thread. Cannot be combined with /// `sandbox`. + #[experimental("thread/start.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, #[ts(optional = nullable)] @@ -3447,6 +3449,7 @@ pub struct ThreadStartResponse { /// view. pub sandbox: SandboxPolicy, /// Canonical active permissions view for this thread. + #[experimental("thread/start.permissionProfile")] #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3508,6 +3511,7 @@ pub struct ThreadResumeParams { pub sandbox: Option, /// Full permissions override for the resumed thread. Cannot be combined /// with `sandbox`. + #[experimental("thread/resume.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, #[ts(optional = nullable)] @@ -3551,6 +3555,7 @@ pub struct ThreadResumeResponse { /// view. pub sandbox: SandboxPolicy, /// Canonical active permissions view for this thread. + #[experimental("thread/resume.permissionProfile")] #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3603,6 +3608,7 @@ pub struct ThreadForkParams { pub sandbox: Option, /// Full permissions override for the forked thread. Cannot be combined /// with `sandbox`. + #[experimental("thread/fork.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, #[ts(optional = nullable)] @@ -3646,6 +3652,7 @@ pub struct ThreadForkResponse { /// view. pub sandbox: SandboxPolicy, /// Canonical active permissions view for this thread. + #[experimental("thread/fork.permissionProfile")] #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -5184,6 +5191,7 @@ pub struct TurnStartParams { pub sandbox_policy: Option, /// Override the full permissions profile for this turn and subsequent /// turns. Cannot be combined with `sandboxPolicy`. + #[experimental("turn/start.permissionProfile")] #[ts(optional = nullable)] pub permission_profile: Option, /// Override the model for this turn and subsequent turns. From 346a2a10dbb4a6d4423df4b61681cfbdea381111 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 27 Apr 2026 23:09:37 -0700 Subject: [PATCH 2/2] permissions: add built-in default profiles --- codex-rs/Cargo.lock | 1 + codex-rs/config/src/config_toml.rs | 3 +- codex-rs/core/config.schema.json | 2 +- codex-rs/core/src/config/config_tests.rs | 287 ++++++++++++++++-- codex-rs/core/src/config/mod.rs | 77 +++-- codex-rs/core/src/config/permissions.rs | 144 ++++++++- codex-rs/core/src/config/permissions_tests.rs | 29 ++ codex-rs/core/tests/common/Cargo.toml | 1 + codex-rs/core/tests/common/lib.rs | 30 ++ codex-rs/core/tests/common/test_codex.rs | 15 +- codex-rs/core/tests/suite/approvals.rs | 89 ++---- codex-rs/core/tests/suite/hooks.rs | 35 +-- 12 files changed, 546 insertions(+), 167 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index a00873ee48..43c0cd59da 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4059,6 +4059,7 @@ dependencies = [ "assert_cmd", "base64 0.22.1", "codex-arg0", + "codex-config", "codex-core", "codex-exec-server", "codex-features", diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index a1692b2cb1..3bd19f5568 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -114,7 +114,8 @@ pub struct ConfigToml { /// Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`. pub sandbox_workspace_write: Option, - /// Default named permissions profile to apply from the `[permissions]` + /// Default permissions profile to apply. Names starting with `:` refer to + /// built-in profiles; other names are resolved from the `[permissions]` /// table. pub default_permissions: Option, diff --git a/codex-rs/core/config.schema.json b/codex-rs/core/config.schema.json index bdccafcb53..0758d14ba9 100644 --- a/codex-rs/core/config.schema.json +++ b/codex-rs/core/config.schema.json @@ -2494,7 +2494,7 @@ "type": "string" }, "default_permissions": { - "description": "Default named permissions profile to apply from the `[permissions]` table.", + "description": "Default permissions profile to apply. Names starting with `:` refer to built-in profiles; other names are resolved from the `[permissions]` table.", "type": "string" }, "developer_instructions": { diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 21a86dcadd..ca6a77a340 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -49,6 +49,8 @@ use codex_config::types::SkillsConfig; use codex_config::types::ToolSuggestDiscoverableType; use codex_config::types::Tui; use codex_config::types::TuiNotificationSettings; +use codex_config::types::WindowsSandboxModeToml; +use codex_config::types::WindowsToml; use codex_exec_server::LOCAL_FS; use codex_features::Feature; use codex_features::FeaturesToml; @@ -661,8 +663,8 @@ allow_upstream_proxy = false } #[tokio::test] -async fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> std::io::Result<()> -{ +async fn permissions_profiles_network_enabled_allows_runtime_network_without_proxy() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; std::fs::write(cwd.path().join(".git"), "gitdir: nowhere")?; @@ -699,14 +701,14 @@ async fn permissions_profiles_network_populates_runtime_network_proxy_spec() -> codex_home.abs(), ) .await?; - let network = config - .permissions - .network - .as_ref() - .expect("enabled profile network should produce a NetworkProxySpec"); - - assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128"); - assert!(!network.socks_enabled()); + assert_eq!( + config.permissions.network_sandbox_policy(), + NetworkSandboxPolicy::Enabled + ); + assert!( + config.permissions.network.is_none(), + "profile network.enabled should not start the managed network proxy" + ); Ok(()) } @@ -1014,7 +1016,8 @@ async fn permission_profile_override_applies_runtime_roots_to_legacy_projection( } #[tokio::test] -async fn permission_profile_override_preserves_configured_network_proxy() -> std::io::Result<()> { +async fn permission_profile_override_preserves_configured_network_policy_without_starting_proxy() +-> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; let permission_profile = PermissionProfile::Disabled; @@ -1059,14 +1062,10 @@ async fn permission_profile_override_preserves_configured_network_proxy() -> std codex_home.abs(), ) .await?; - let network = config - .permissions - .network - .as_ref() - .expect("network-enabled override should preserve configured proxy"); - - assert_eq!(network.proxy_host_and_port(), "127.0.0.1:43128"); - assert!(!network.socks_enabled()); + assert!( + config.permissions.network.is_none(), + "profile network.enabled should not start the managed network proxy" + ); assert_eq!(config.permissions.permission_profile(), permission_profile); Ok(()) } @@ -1189,6 +1188,256 @@ async fn permissions_profiles_require_default_permissions() -> std::io::Result<( Ok(()) } +#[tokio::test] +async fn default_permissions_can_select_builtin_profile_without_permissions_table() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":workspace".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert!( + policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected :workspace to allow writing the project root, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(&cwd.path().join(".git"), cwd.path()), + "expected :workspace to protect project metadata, policy: {policy:?}" + ); + Ok(()) +} + +#[tokio::test] +async fn empty_config_defaults_to_builtin_profile_for_trusted_project() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let project_key = cwd.path().to_string_lossy().to_string(); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + projects: Some(HashMap::from([( + project_key, + ProjectConfig { + trust_level: Some(TrustLevel::Trusted), + }, + )])), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + if cfg!(target_os = "windows") { + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected trusted project fallback to stay read-only without Windows sandbox support, policy: {policy:?}" + ); + } else { + assert!( + policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected trusted project fallback to use :workspace, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(&cwd.path().join(".codex"), cwd.path()), + "expected :workspace metadata carveouts, policy: {policy:?}" + ); + } + Ok(()) +} + +#[tokio::test] +async fn implicit_builtin_workspace_profile_preserves_sandbox_workspace_write_settings() +-> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + let extra_root = TempDir::new()?; + let extra_root = extra_root.path().abs(); + let project_key = cwd.path().to_string_lossy().to_string(); + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + projects: Some(HashMap::from([( + project_key, + ProjectConfig { + trust_level: Some(TrustLevel::Trusted), + }, + )])), + sandbox_workspace_write: Some(SandboxWorkspaceWrite { + writable_roots: vec![extra_root.clone()], + network_access: true, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: false, + }), + windows: Some(WindowsToml { + sandbox: Some(WindowsSandboxModeToml::Elevated), + sandbox_private_desktop: None, + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert!( + policy.can_write_path_with_cwd(extra_root.as_path(), cwd.path()), + "expected implicit :workspace to preserve sandbox_workspace_write.writable_roots, policy: {policy:?}" + ); + assert_eq!( + config.permissions.network_sandbox_policy(), + NetworkSandboxPolicy::Enabled + ); + match config.legacy_sandbox_policy() { + SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + } => { + assert!(writable_roots.contains(&extra_root)); + assert!(network_access); + assert!(exclude_tmpdir_env_var); + assert!(!exclude_slash_tmp); + } + sandbox_policy => panic!("expected workspace-write projection, got {sandbox_policy:?}"), + } + Ok(()) +} + +#[tokio::test] +async fn empty_config_defaults_to_builtin_read_only_without_trust_decision() -> std::io::Result<()> +{ + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml::default(), + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + let policy = config.permissions.file_system_sandbox_policy(); + assert!( + policy.can_read_path_with_cwd(cwd.path(), cwd.path()), + "expected :read-only to allow reads, policy: {policy:?}" + ); + assert!( + !policy.can_write_path_with_cwd(cwd.path(), cwd.path()), + "expected :read-only to deny writes, policy: {policy:?}" + ); + Ok(()) +} + +#[tokio::test] +async fn default_permissions_can_select_builtin_no_sandbox_profile() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let config = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":danger-no-sandbox".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await?; + + assert_eq!( + config.permissions.permission_profile(), + PermissionProfile::Disabled + ); + Ok(()) +} + +#[tokio::test] +async fn user_defined_permission_profile_names_cannot_use_builtin_prefix() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let err = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":custom".to_string()), + permissions: Some(PermissionsToml { + entries: BTreeMap::from([( + ":custom".to_string(), + PermissionProfileToml::default(), + )]), + }), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await + .expect_err("reserved profile name should be rejected"); + + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "permissions profile `:custom` uses a reserved built-in profile prefix" + ); + Ok(()) +} + +#[tokio::test] +async fn unknown_builtin_permission_profile_name_is_rejected() -> std::io::Result<()> { + let codex_home = TempDir::new()?; + let cwd = TempDir::new()?; + + let err = Config::load_from_base_config_with_overrides( + ConfigToml { + default_permissions: Some(":unknown".to_string()), + ..Default::default() + }, + ConfigOverrides { + cwd: Some(cwd.path().to_path_buf()), + ..Default::default() + }, + codex_home.abs(), + ) + .await + .expect_err("unknown built-in profile name should be rejected"); + + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + assert_eq!( + err.to_string(), + "default_permissions refers to unknown built-in profile `:unknown`" + ); + Ok(()) +} + #[tokio::test] async fn permissions_profiles_allow_direct_write_roots_outside_workspace_root() -> std::io::Result<()> { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index f4a73dd25f..bccab4de5e 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -99,9 +99,12 @@ use std::path::Path; use std::path::PathBuf; use std::sync::Arc; -use crate::config::permissions::compile_permission_profile; +use crate::config::permissions::builtin_permission_profile; +use crate::config::permissions::compile_permission_profile_selection; +use crate::config::permissions::default_builtin_permission_profile_name; use crate::config::permissions::get_readable_roots_required_for_codex_runtime; -use crate::config::permissions::network_proxy_config_from_profile_network; +use crate::config::permissions::network_proxy_config_for_profile_selection; +use crate::config::permissions::validate_user_permission_profile_names; use codex_network_proxy::NetworkProxyConfig; use toml::Value as TomlValue; use toml_edit::DocumentMut; @@ -1861,6 +1864,7 @@ impl Config { .permissions .as_ref() .is_some_and(|profiles| !profiles.is_empty()); + validate_user_permission_profile_names(cfg.permissions.as_ref())?; if has_permission_profiles && !matches!( permission_config_syntax, @@ -1891,8 +1895,7 @@ impl Config { let profiles_are_active = matches!( permission_config_syntax, Some(PermissionConfigSyntax::Profiles) - ) || (permission_config_syntax.is_none() - && has_permission_profiles); + ) || permission_config_syntax.is_none(); let ( configured_network_proxy_config, permission_profile, @@ -1902,24 +1905,19 @@ impl Config { permission_profile.to_runtime_permissions(); let configured_network_proxy_config = if network_sandbox_policy.is_enabled() && profiles_are_active { - let permissions = cfg.permissions.as_ref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a `[permissions]` table", - ) - })?; - let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a named permissions profile", - ) - })?; - let profile = resolve_permission_profile(permissions, default_permissions)?; - // PermissionProfile carries the active network sandbox bit, not the configured // proxy/allowlist policy. Keep that config so active profiles can round-trip // without broadening network behavior. - network_proxy_config_from_profile_network(profile.network.as_ref()) + let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| { + default_builtin_permission_profile_name( + &active_project, + windows_sandbox_level, + ) + }); + network_proxy_config_for_profile_selection( + cfg.permissions.as_ref(), + default_permissions, + )? } else { NetworkProxyConfig::default() }; @@ -1947,32 +1945,31 @@ impl Config { file_system_sandbox_policy, ) } else if profiles_are_active { - let permissions = cfg.permissions.as_ref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a `[permissions]` table", - ) - })?; - let default_permissions = cfg.default_permissions.as_deref().ok_or_else(|| { - std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "default_permissions requires a named permissions profile", - ) - })?; - let profile = resolve_permission_profile(permissions, default_permissions)?; - let configured_network_proxy_config = - network_proxy_config_from_profile_network(profile.network.as_ref()); + let default_permissions = cfg.default_permissions.as_deref().unwrap_or_else(|| { + default_builtin_permission_profile_name(&active_project, windows_sandbox_level) + }); + let configured_network_proxy_config = network_proxy_config_for_profile_selection( + cfg.permissions.as_ref(), + default_permissions, + )?; let (mut file_system_sandbox_policy, network_sandbox_policy) = - compile_permission_profile( - permissions, + compile_permission_profile_selection( + cfg.permissions.as_ref(), default_permissions, + cfg.sandbox_workspace_write.as_ref(), resolved_cwd.as_path(), &mut startup_warnings, )?; - let mut permission_profile = PermissionProfile::from_runtime_permissions( - &file_system_sandbox_policy, - network_sandbox_policy, - ); + let mut permission_profile = if let Some(permission_profile) = + builtin_permission_profile(default_permissions, cfg.sandbox_workspace_write.as_ref()) + { + permission_profile + } else { + PermissionProfile::from_runtime_permissions( + &file_system_sandbox_policy, + network_sandbox_policy, + ) + }; let sandbox_policy = compatibility_sandbox_policy_for_permission_profile( &permission_profile, &file_system_sandbox_policy, diff --git a/codex-rs/core/src/config/permissions.rs b/codex-rs/core/src/config/permissions.rs index 6d938e9185..d6845635ec 100644 --- a/codex-rs/core/src/config/permissions.rs +++ b/codex-rs/core/src/config/permissions.rs @@ -9,9 +9,12 @@ use codex_config::permissions_toml::FilesystemPermissionsToml; use codex_config::permissions_toml::NetworkToml; use codex_config::permissions_toml::PermissionProfileToml; use codex_config::permissions_toml::PermissionsToml; +use codex_config::types::SandboxWorkspaceWrite; use codex_network_proxy::NetworkProxyConfig; #[cfg(test)] use codex_network_proxy::NetworkUnixSocketPermission as ProxyNetworkUnixSocketPermission; +use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -20,13 +23,97 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; +use super::ProjectConfig; + +pub(crate) const BUILT_IN_READ_ONLY_PROFILE: &str = ":read-only"; +pub(crate) const BUILT_IN_WORKSPACE_PROFILE: &str = ":workspace"; +pub(crate) const BUILT_IN_DANGER_NO_SANDBOX_PROFILE: &str = ":danger-no-sandbox"; + +pub(crate) fn default_builtin_permission_profile_name( + active_project: &ProjectConfig, + windows_sandbox_level: WindowsSandboxLevel, +) -> &'static str { + if (active_project.is_trusted() || active_project.is_untrusted()) + && !(cfg!(target_os = "windows") && windows_sandbox_level == WindowsSandboxLevel::Disabled) + { + BUILT_IN_WORKSPACE_PROFILE + } else { + BUILT_IN_READ_ONLY_PROFILE + } +} + +pub(crate) fn is_builtin_permission_profile_name(profile_name: &str) -> bool { + matches!( + profile_name, + BUILT_IN_READ_ONLY_PROFILE + | BUILT_IN_WORKSPACE_PROFILE + | BUILT_IN_DANGER_NO_SANDBOX_PROFILE + ) +} + +pub(crate) fn builtin_permission_profile( + profile_name: &str, + workspace_write: Option<&SandboxWorkspaceWrite>, +) -> Option { + match profile_name { + BUILT_IN_READ_ONLY_PROFILE => Some(PermissionProfile::read_only()), + BUILT_IN_WORKSPACE_PROFILE => Some(match workspace_write { + Some(SandboxWorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + }) => PermissionProfile::workspace_write_with( + writable_roots, + if *network_access { + NetworkSandboxPolicy::Enabled + } else { + NetworkSandboxPolicy::Restricted + }, + *exclude_tmpdir_env_var, + *exclude_slash_tmp, + ), + None => PermissionProfile::workspace_write(), + }), + BUILT_IN_DANGER_NO_SANDBOX_PROFILE => Some(PermissionProfile::Disabled), + _ => None, + } +} + +pub(crate) fn validate_user_permission_profile_names( + permissions: Option<&PermissionsToml>, +) -> io::Result<()> { + let Some(permissions) = permissions else { + return Ok(()); + }; + + for profile_name in permissions.entries.keys() { + if profile_name.starts_with(':') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "permissions profile `{profile_name}` uses a reserved built-in profile prefix" + ), + )); + } + } + + Ok(()) +} + pub(crate) fn network_proxy_config_from_profile_network( network: Option<&NetworkToml>, ) -> NetworkProxyConfig { - network.map_or_else( + let mut config = network.map_or_else( NetworkProxyConfig::default, NetworkToml::to_network_proxy_config, - ) + ); + // Profile `network.enabled` controls sandbox network access. Managed proxy + // startup is controlled separately by network requirements, so keep the + // configured proxy disabled while preserving the rest of the profile's proxy + // policy for requirement-driven startup. + config.network.enabled = false; + config } pub(crate) fn resolve_permission_profile<'a>( @@ -41,6 +128,27 @@ pub(crate) fn resolve_permission_profile<'a>( }) } +pub(crate) fn network_proxy_config_for_profile_selection( + permissions: Option<&PermissionsToml>, + profile_name: &str, +) -> io::Result { + if is_builtin_permission_profile_name(profile_name) { + return Ok(NetworkProxyConfig::default()); + } + reject_unknown_builtin_permission_profile(profile_name)?; + + let permissions = permissions.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "default_permissions requires a `[permissions]` table", + ) + })?; + let profile = resolve_permission_profile(permissions, profile_name)?; + Ok(network_proxy_config_from_profile_network( + profile.network.as_ref(), + )) +} + pub(crate) fn compile_permission_profile( permissions: &PermissionsToml, profile_name: &str, @@ -103,6 +211,38 @@ pub(crate) fn compile_permission_profile( Ok((file_system_sandbox_policy, network_sandbox_policy)) } +pub(crate) fn compile_permission_profile_selection( + permissions: Option<&PermissionsToml>, + profile_name: &str, + workspace_write: Option<&SandboxWorkspaceWrite>, + policy_cwd: &Path, + startup_warnings: &mut Vec, +) -> io::Result<(FileSystemSandboxPolicy, NetworkSandboxPolicy)> { + if let Some(permission_profile) = builtin_permission_profile(profile_name, workspace_write) { + return Ok(permission_profile.to_runtime_permissions()); + } + reject_unknown_builtin_permission_profile(profile_name)?; + + let permissions = permissions.ok_or_else(|| { + io::Error::new( + io::ErrorKind::InvalidInput, + "default_permissions requires a `[permissions]` table", + ) + })?; + compile_permission_profile(permissions, profile_name, policy_cwd, startup_warnings) +} + +fn reject_unknown_builtin_permission_profile(profile_name: &str) -> io::Result<()> { + if profile_name.starts_with(':') { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("default_permissions refers to unknown built-in profile `{profile_name}`"), + )); + } + + Ok(()) +} + /// Returns a list of paths that must be readable by shell tools in order /// for Codex to function. These should always be added to the /// `FileSystemSandboxPolicy` for a thread. diff --git a/codex-rs/core/src/config/permissions_tests.rs b/codex-rs/core/src/config/permissions_tests.rs index e021db3d2d..c8dfe18fc8 100644 --- a/codex-rs/core/src/config/permissions_tests.rs +++ b/codex-rs/core/src/config/permissions_tests.rs @@ -236,6 +236,35 @@ fn network_toml_overlays_unix_socket_permissions_by_path() { ); } +#[test] +fn profile_network_proxy_config_preserves_policy_without_enabling_proxy() { + let config = network_proxy_config_from_profile_network(Some(&NetworkToml { + enabled: Some(true), + proxy_url: Some("http://127.0.0.1:43128".to_string()), + enable_socks5: Some(false), + domains: Some(NetworkDomainPermissionsToml { + entries: BTreeMap::from([( + "openai.com".to_string(), + NetworkDomainPermissionToml::Allow, + )]), + }), + ..Default::default() + })); + + assert!(!config.network.enabled); + assert_eq!(config.network.proxy_url, "http://127.0.0.1:43128"); + assert!(!config.network.enable_socks5); + assert_eq!( + config.network.domains, + Some(codex_network_proxy::NetworkDomainPermissions { + entries: vec![codex_network_proxy::NetworkDomainPermissionEntry { + pattern: "openai.com".to_string(), + permission: codex_network_proxy::NetworkDomainPermission::Allow, + }], + }) + ); +} + #[test] fn read_write_glob_warnings_skip_supported_deny_read_globs_and_trailing_subpaths() { let filesystem = FilesystemPermissionsToml { diff --git a/codex-rs/core/tests/common/Cargo.toml b/codex-rs/core/tests/common/Cargo.toml index e2765e8be6..f710aa36cc 100644 --- a/codex-rs/core/tests/common/Cargo.toml +++ b/codex-rs/core/tests/common/Cargo.toml @@ -15,6 +15,7 @@ anyhow = { workspace = true } assert_cmd = { workspace = true } base64 = { workspace = true } codex-arg0 = { workspace = true } +codex-config = { workspace = true } codex-core = { workspace = true } codex-exec-server = { workspace = true } codex-features = { workspace = true } diff --git a/codex-rs/core/tests/common/lib.rs b/codex-rs/core/tests/common/lib.rs index a11d5ee6a4..c89e6a5188 100644 --- a/codex-rs/core/tests/common/lib.rs +++ b/codex-rs/core/tests/common/lib.rs @@ -8,6 +8,9 @@ use ctor::ctor; use std::sync::OnceLock; use tempfile::TempDir; +use codex_config::CloudRequirementsLoader; +use codex_config::ConfigRequirementsToml; +use codex_config::NetworkRequirementsToml; use codex_core::CodexThread; use codex_core::config::Config; use codex_core::config::ConfigBuilder; @@ -164,14 +167,41 @@ pub fn fetch_dotslash_file( /// temporary directory. Using a per-test directory keeps tests hermetic and /// avoids clobbering a developer’s real `~/.codex`. pub async fn load_default_config_for_test(codex_home: &TempDir) -> Config { + load_default_config_for_test_with_cloud_requirements( + codex_home, + CloudRequirementsLoader::default(), + ) + .await +} + +/// Returns a default `Config` with test-provided cloud requirements applied +/// during config construction. +pub async fn load_default_config_for_test_with_cloud_requirements( + codex_home: &TempDir, + cloud_requirements: CloudRequirementsLoader, +) -> Config { ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) .harness_overrides(default_test_overrides()) + .cloud_requirements(cloud_requirements) .build() .await .expect("defaults for test should always succeed") } +pub fn managed_network_requirements_loader() -> CloudRequirementsLoader { + CloudRequirementsLoader::new(async { + Ok(Some(ConfigRequirementsToml { + network: Some(NetworkRequirementsToml { + enabled: Some(true), + allow_local_binding: Some(true), + ..Default::default() + }), + ..Default::default() + })) + }) +} + #[cfg(target_os = "linux")] fn default_test_overrides() -> ConfigOverrides { ConfigOverrides { diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 9a010417fb..d77d55956a 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -12,6 +12,7 @@ use std::time::Duration; use anyhow::Context; use anyhow::Result; use anyhow::anyhow; +use codex_config::CloudRequirementsLoader; use codex_core::CodexThread; use codex_core::ThreadManager; use codex_core::config::Config; @@ -47,6 +48,7 @@ use crate::PathBufExt; use crate::TempDirExt; use crate::get_remote_test_env; use crate::load_default_config_for_test; +use crate::load_default_config_for_test_with_cloud_requirements; use crate::responses::WebSocketTestServer; use crate::responses::output_value_to_text; use crate::responses::start_mock_server; @@ -206,6 +208,7 @@ pub struct TestCodexBuilder { pre_build_hooks: Vec>, workspace_setups: Vec>, home: Option>, + cloud_requirements: Option, user_shell_override: Option, exec_server_url: Option, } @@ -254,6 +257,11 @@ impl TestCodexBuilder { self } + pub fn with_cloud_requirements(mut self, cloud_requirements: CloudRequirementsLoader) -> Self { + self.cloud_requirements = Some(cloud_requirements); + self + } + pub fn with_user_shell(mut self, user_shell: Shell) -> Self { self.user_shell_override = Some(user_shell); self @@ -485,7 +493,11 @@ impl TestCodexBuilder { for hook in self.pre_build_hooks.drain(..) { hook(home.path()); } - let mut config = load_default_config_for_test(home).await; + let mut config = if let Some(cloud_requirements) = self.cloud_requirements.take() { + load_default_config_for_test_with_cloud_requirements(home, cloud_requirements).await + } else { + load_default_config_for_test(home).await + }; config.cwd = cwd_override; config.model_provider = model_provider; if let Ok(path) = codex_utils_cargo_bin::cargo_bin("codex") { @@ -923,6 +935,7 @@ pub fn test_codex() -> TestCodexBuilder { pre_build_hooks: vec![], workspace_setups: vec![], home: None, + cloud_requirements: None, user_shell_override: None, exec_server_url: None, } diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 4209fc2100..ef22cfbfc1 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -2,12 +2,6 @@ use anyhow::Context; use anyhow::Result; -use codex_config::ConfigLayerStack; -use codex_config::ConfigLayerStackOrdering; -use codex_config::NetworkConstraints; -use codex_config::NetworkRequirementsToml; -use codex_config::RequirementSource; -use codex_config::Sourced; use codex_config::types::ApprovalsReviewer; use codex_core::CodexThread; use codex_core::config::Constrained; @@ -25,6 +19,7 @@ use codex_protocol::protocol::Op; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; +use core_test_support::managed_network_requirements_loader; use core_test_support::responses::ev_apply_patch_function_call; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -2590,38 +2585,15 @@ allow_local_binding = true exclude_slash_tmp: false, }; let sandbox_policy_for_config = sandbox_policy.clone(); - let mut builder = test_codex().with_home(home).with_config(move |config| { - config.permissions.approval_policy = Constrained::allow_any(approval_policy); - config - .set_legacy_sandbox_policy(sandbox_policy_for_config) - .expect("set sandbox policy"); - let layers = config - .config_layer_stack - .get_layers( - ConfigLayerStackOrdering::LowestPrecedenceFirst, - /*include_disabled*/ true, - ) - .into_iter() - .cloned() - .collect(); - let mut requirements = config.config_layer_stack.requirements().clone(); - requirements.network = Some(Sourced::new( - NetworkConstraints { - enabled: Some(true), - allow_local_binding: Some(true), - ..Default::default() - }, - RequirementSource::CloudRequirements, - )); - let mut requirements_toml = config.config_layer_stack.requirements_toml().clone(); - requirements_toml.network = Some(NetworkRequirementsToml { - enabled: Some(true), - allow_local_binding: Some(true), - ..Default::default() + let mut builder = test_codex() + .with_home(home) + .with_cloud_requirements(managed_network_requirements_loader()) + .with_config(move |config| { + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + config + .set_legacy_sandbox_policy(sandbox_policy_for_config) + .expect("set sandbox policy"); }); - config.config_layer_stack = ConfigLayerStack::new(layers, requirements, requirements_toml) - .expect("rebuild config layer stack with network requirements"); - }); let test = builder.build(&server).await?; assert!( test.config.managed_network_requirements_enabled(), @@ -2892,40 +2864,17 @@ allow_local_binding = true exclude_tmpdir_env_var: false, exclude_slash_tmp: false, }; - let mut builder = test_codex().with_home(home).with_config(move |config| { - config.permissions.approval_policy = Constrained::allow_any(approval_policy); - let cwd = config.cwd.clone(); - config - .permissions - .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) - .expect("test setup should allow sandbox policy"); - let layers = config - .config_layer_stack - .get_layers( - ConfigLayerStackOrdering::LowestPrecedenceFirst, - /*include_disabled*/ true, - ) - .into_iter() - .cloned() - .collect(); - let mut requirements = config.config_layer_stack.requirements().clone(); - requirements.network = Some(Sourced::new( - NetworkConstraints { - enabled: Some(true), - allow_local_binding: Some(true), - ..Default::default() - }, - RequirementSource::CloudRequirements, - )); - let mut requirements_toml = config.config_layer_stack.requirements_toml().clone(); - requirements_toml.network = Some(NetworkRequirementsToml { - enabled: Some(true), - allow_local_binding: Some(true), - ..Default::default() + let mut builder = test_codex() + .with_home(home) + .with_cloud_requirements(managed_network_requirements_loader()) + .with_config(move |config| { + config.permissions.approval_policy = Constrained::allow_any(approval_policy); + let cwd = config.cwd.clone(); + config + .permissions + .set_legacy_sandbox_policy(SandboxPolicy::DangerFullAccess, cwd.as_path()) + .expect("test setup should allow sandbox policy"); }); - config.config_layer_stack = ConfigLayerStack::new(layers, requirements, requirements_toml) - .expect("rebuild config layer stack with network requirements"); - }); let test = builder.build(&server).await?; assert!( !test.config.managed_network_requirements_enabled(), diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 74e9a7a682..28185a0a5c 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -3,12 +3,6 @@ use std::path::Path; use anyhow::Context; use anyhow::Result; -use codex_config::ConfigLayerStack; -use codex_config::ConfigLayerStackOrdering; -use codex_config::NetworkConstraints; -use codex_config::NetworkRequirementsToml; -use codex_config::RequirementSource; -use codex_config::Sourced; use codex_core::config::Constrained; use codex_features::Feature; use codex_protocol::items::parse_hook_prompt_fragment; @@ -21,6 +15,7 @@ use codex_protocol::protocol::RolloutItem; use codex_protocol::protocol::RolloutLine; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::user_input::UserInput; +use core_test_support::managed_network_requirements_loader; use core_test_support::responses::ev_apply_patch_function_call; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; @@ -1577,6 +1572,7 @@ allow_local_binding = true panic!("failed to write permission request hook test fixture: {error}"); } }) + .with_cloud_requirements(managed_network_requirements_loader()) .with_config(move |config| { config .features @@ -1586,33 +1582,6 @@ allow_local_binding = true config .set_legacy_sandbox_policy(sandbox_policy_for_config) .expect("set sandbox policy"); - let layers = config - .config_layer_stack - .get_layers( - ConfigLayerStackOrdering::LowestPrecedenceFirst, - /*include_disabled*/ true, - ) - .into_iter() - .cloned() - .collect(); - let mut requirements = config.config_layer_stack.requirements().clone(); - requirements.network = Some(Sourced::new( - NetworkConstraints { - enabled: Some(true), - allow_local_binding: Some(true), - ..Default::default() - }, - RequirementSource::CloudRequirements, - )); - let mut requirements_toml = config.config_layer_stack.requirements_toml().clone(); - requirements_toml.network = Some(NetworkRequirementsToml { - enabled: Some(true), - allow_local_binding: Some(true), - ..Default::default() - }); - config.config_layer_stack = - ConfigLayerStack::new(layers, requirements, requirements_toml) - .expect("rebuild config layer stack with network requirements"); }) .build(&server) .await?;