From 4816b892044084de6ab5a55ea0b5854c330843fd Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Thu, 23 Apr 2026 23:02:18 -0700 Subject: [PATCH] permissions: make profiles represent enforcement (#19231) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why `PermissionProfile` is becoming the canonical permissions abstraction, but the old shape only carried optional filesystem and network fields. It could describe allowed access, but not who is responsible for enforcing it. That made `DangerFullAccess` and `ExternalSandbox` lossy when profiles were exported, cached, or round-tripped through app-server APIs. The important model change is that active permissions are now a disjoint union over the enforcement mode. Conceptually: ```rust pub enum PermissionProfile { Managed { file_system: FileSystemSandboxPolicy, network: NetworkSandboxPolicy, }, Disabled, External { network: NetworkSandboxPolicy, }, } ``` This distinction matters because `Disabled` means Codex should apply no outer sandbox at all, while `External` means filesystem isolation is owned by an outside caller. Those are not equivalent to a broad managed sandbox. For example, macOS cannot nest Seatbelt inside Seatbelt, so an inner sandbox may require the outer Codex layer to use no sandbox rather than a permissive one. ## How Existing Modeling Maps Legacy `SandboxPolicy` remains a boundary projection, but it now maps into the higher-fidelity profile model: - `ReadOnly` and `WorkspaceWrite` map to `PermissionProfile::Managed` with restricted filesystem entries plus the corresponding network policy. - `DangerFullAccess` maps to `PermissionProfile::Disabled`, preserving the “no outer sandbox” intent instead of treating it as a lax managed sandbox. - `ExternalSandbox { network_access }` maps to `PermissionProfile::External { network }`, preserving external filesystem enforcement while still carrying the active network policy. - Split runtime policies that legacy `SandboxPolicy` cannot faithfully express, such as managed unrestricted filesystem plus restricted network, stay `Managed` instead of being collapsed into `ExternalSandbox`. - Per-command/session/turn grants remain partial overlays via `AdditionalPermissionProfile`; full `PermissionProfile` is reserved for complete active runtime permissions. ## What Changed - Change active `PermissionProfile` into a tagged union: `managed`, `disabled`, and `external`. - Keep partial permission grants separate with `AdditionalPermissionProfile` for command/session/turn overlays. - Represent managed filesystem permissions as either `restricted` entries or `unrestricted`; `glob_scan_max_depth` is non-zero when present. - Preserve old rollout compatibility by accepting the pre-tagged `{ network, file_system }` profile shape during deserialization. - Preserve fidelity for important edge cases: `DangerFullAccess` round-trips as `disabled`, `ExternalSandbox` round-trips as `external`, and managed unrestricted filesystem + restricted network stays managed instead of being mistaken for external enforcement. - Preserve configured deny-read entries and bounded glob scan depth when full profiles are projected back into runtime policies, including unrestricted replacements that now become `:root = write` plus deny entries. - Regenerate the experimental app-server v2 JSON/TypeScript schema and update the `command/exec` README example for the tagged `permissionProfile` shape. ## Compatibility Legacy `SandboxPolicy` remains available at config/API boundaries as the compatibility projection. Existing rollout lines with the old `PermissionProfile` shape continue to load. The app-server `permissionProfile` field is experimental, so its v2 wire shape is intentionally updated to match the higher-fidelity model. ## Verification - `just write-app-server-schema` - `cargo check --tests` - `cargo test -p codex-protocol permission_profile` - `cargo test -p codex-protocol preserving_deny_entries_keeps_unrestricted_policy_enforceable` - `cargo test -p codex-app-server-protocol permission_profile_file_system_permissions` - `cargo test -p codex-app-server-protocol serialize_client_response` - `cargo test -p codex-core session_configured_reports_permission_profile_for_external_sandbox` - `just fix` - `just fix -p codex-protocol` - `just fix -p codex-app-server-protocol` - `just fix -p codex-core` - `just fix -p codex-app-server` --- codex-rs/analytics/src/events.rs | 8 +- .../schema/json/ClientRequest.json | 147 ++++-- ...CommandExecutionRequestApprovalParams.json | 3 +- .../schema/json/ServerRequest.json | 3 +- .../codex_app_server_protocol.schemas.json | 156 +++++-- .../codex_app_server_protocol.v2.schemas.json | 153 +++++-- .../schema/json/v2/CommandExecParams.json | 147 ++++-- .../schema/json/v2/ThreadForkParams.json | 147 ++++-- .../schema/json/v2/ThreadForkResponse.json | 149 ++++-- .../schema/json/v2/ThreadResumeParams.json | 147 ++++-- .../schema/json/v2/ThreadResumeResponse.json | 149 ++++-- .../schema/json/v2/ThreadStartParams.json | 147 ++++-- .../schema/json/v2/ThreadStartResponse.json | 149 ++++-- .../schema/json/v2/TurnStartParams.json | 147 ++++-- .../v2/AdditionalPermissionProfile.ts | 6 +- .../schema/typescript/v2/PermissionProfile.ts | 2 +- .../PermissionProfileFileSystemPermissions.ts | 2 +- .../v2/PermissionProfileNetworkPermissions.ts | 2 +- .../typescript/v2/ThreadForkResponse.ts | 4 +- .../typescript/v2/ThreadResumeResponse.ts | 4 +- .../typescript/v2/ThreadStartResponse.ts | 4 +- .../src/protocol/common.rs | 17 +- .../app-server-protocol/src/protocol/v2.rs | 169 ++++--- codex-rs/app-server/README.md | 3 +- .../app-server/src/bespoke_event_handling.rs | 4 +- .../app-server/src/codex_message_processor.rs | 72 +-- .../app-server/tests/suite/v2/command_exec.rs | 33 +- .../app-server/tests/suite/v2/turn_start.rs | 9 +- codex-rs/core/src/config/config_tests.rs | 32 +- codex-rs/core/src/config/mod.rs | 10 +- .../core/src/guardian/approval_request.rs | 16 +- codex-rs/core/src/session/mod.rs | 12 +- codex-rs/core/src/session/session.rs | 35 +- codex-rs/core/src/session/tests.rs | 14 +- .../core/src/session/tests/guardian_tests.rs | 2 +- codex-rs/core/src/session/turn_context.rs | 27 +- codex-rs/core/src/state/session.rs | 8 +- codex-rs/core/src/state/turn.rs | 8 +- .../core/src/tools/handlers/apply_patch.rs | 6 +- codex-rs/core/src/tools/handlers/mod.rs | 30 +- codex-rs/core/src/tools/handlers/shell.rs | 4 +- .../core/src/tools/handlers/unified_exec.rs | 4 +- .../src/tools/handlers/unified_exec_tests.rs | 2 +- .../core/src/tools/runtimes/apply_patch.rs | 21 +- .../src/tools/runtimes/apply_patch_tests.rs | 19 +- codex-rs/core/src/tools/runtimes/mod.rs | 4 +- codex-rs/core/src/tools/runtimes/shell.rs | 6 +- .../tools/runtimes/shell/unix_escalation.rs | 24 +- .../runtimes/shell/unix_escalation_tests.rs | 3 +- .../core/src/tools/runtimes/unified_exec.rs | 6 +- codex-rs/core/src/unified_exec/mod.rs | 4 +- .../core/tests/suite/request_permissions.rs | 2 +- codex-rs/exec-server/src/file_system.rs | 7 +- codex-rs/exec-server/src/fs_sandbox.rs | 8 +- codex-rs/exec-server/tests/file_system.rs | 41 +- codex-rs/protocol/src/approvals.rs | 7 +- codex-rs/protocol/src/models.rs | 433 ++++++++++++++++-- codex-rs/protocol/src/permissions.rs | 57 +++ codex-rs/protocol/src/protocol.rs | 4 +- codex-rs/protocol/src/request_permissions.rs | 8 +- codex-rs/rollout-trace/src/tool_dispatch.rs | 4 +- codex-rs/sandboxing/src/manager.rs | 4 +- codex-rs/sandboxing/src/manager_tests.rs | 2 +- codex-rs/sandboxing/src/policy_transforms.rs | 38 +- .../sandboxing/src/policy_transforms_tests.rs | 2 +- .../src/unix/escalate_server.rs | 2 +- codex-rs/tui/src/app.rs | 2 +- codex-rs/tui/src/app/tests.rs | 3 +- codex-rs/tui/src/app/thread_session_state.rs | 18 +- codex-rs/tui/src/app_server_session.rs | 29 +- .../tui/src/bottom_pane/approval_overlay.rs | 16 +- .../src/chatwidget/tests/approval_requests.rs | 2 +- .../chatwidget/tests/composer_submission.rs | 10 +- 73 files changed, 2090 insertions(+), 889 deletions(-) diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 73f2886f2f..98d0e6ff6b 100644 --- a/codex-rs/analytics/src/events.rs +++ b/codex-rs/analytics/src/events.rs @@ -23,7 +23,7 @@ use codex_app_server_protocol::CodexErrorInfo; use codex_login::default_client::originator; use codex_plugin::PluginTelemetryMetadata; use codex_protocol::approvals::NetworkApprovalProtocol; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::SandboxPermissions; use codex_protocol::protocol::GuardianAssessmentOutcome; use codex_protocol::protocol::GuardianCommandSource; @@ -180,17 +180,17 @@ pub enum GuardianApprovalRequestSource { pub enum GuardianReviewedAction { Shell { sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, }, UnifiedExec { sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, tty: bool, }, Execve { source: GuardianCommandSource, program: String, - additional_permissions: Option, + additional_permissions: Option, }, ApplyPatch {}, NetworkAccess { diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index db2065f750..d7631e1572 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1892,61 +1892,132 @@ "type": "string" }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { diff --git a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json index 78e75d7c46..76d265c591 100644 --- a/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json +++ b/codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalParams.json @@ -78,7 +78,8 @@ { "type": "null" } - ] + ], + "description": "Partial overlay used for per-command permission requests." } }, "type": "object" diff --git a/codex-rs/app-server-protocol/schema/json/ServerRequest.json b/codex-rs/app-server-protocol/schema/json/ServerRequest.json index 84bf524739..50510adf98 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ServerRequest.json @@ -78,7 +78,8 @@ { "type": "null" } - ] + ], + "description": "Partial overlay used for per-command permission requests." } }, "type": "object" 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 ccce74bc0c..cdc9da679e 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 @@ -25,7 +25,8 @@ { "type": "null" } - ] + ], + "description": "Partial overlay used for per-command permission requests." } }, "type": "object" @@ -11285,61 +11286,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/v2/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/v2/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/v2/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/v2/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/v2/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { @@ -14560,7 +14632,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -16003,7 +16075,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -16330,7 +16402,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "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 8560fb6c84..04c91b5a28 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 @@ -8000,61 +8000,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { @@ -12447,7 +12518,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -13890,7 +13961,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "anyOf": [ @@ -14217,7 +14288,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "description": "Canonical active permissions view for this thread." }, "reasoningEffort": { "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 4def45c049..6ba2fc0db4 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -246,61 +246,132 @@ "type": "string" }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { 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 a603aff1e8..d120fc8b5d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkParams.json @@ -276,61 +276,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "SandboxMode": { 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 47677da41e..281650bb3a 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -900,61 +900,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { @@ -2506,7 +2577,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "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 19ccad14c1..40ff83aeb3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeParams.json @@ -542,61 +542,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { 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 38b0eb0d37..573cbe92d0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -900,61 +900,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { @@ -2506,7 +2577,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "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 ae599d3e61..5a59e280ea 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartParams.json @@ -302,61 +302,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { 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 879fa5c687..1de06c6039 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -900,61 +900,132 @@ ] }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "ReadOnlyAccess": { @@ -2506,7 +2577,7 @@ } ], "default": null, - "description": "Canonical active permissions view for this thread when representable. This is `null` for external sandbox policies because external enforcement cannot be round-tripped as a `PermissionProfile`." + "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 062771029e..245c57886e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -326,61 +326,132 @@ "type": "string" }, "PermissionProfile": { - "properties": { - "fileSystem": { - "anyOf": [ - { + "oneOf": [ + { + "description": "Codex owns sandbox construction for this profile.", + "properties": { + "fileSystem": { "$ref": "#/definitions/PermissionProfileFileSystemPermissions" }, - { - "type": "null" - } - ] - }, - "network": { - "anyOf": [ - { + "network": { "$ref": "#/definitions/PermissionProfileNetworkPermissions" }, - { - "type": "null" + "type": { + "enum": [ + "managed" + ], + "title": "ManagedPermissionProfileType", + "type": "string" } - ] + }, + "required": [ + "fileSystem", + "network", + "type" + ], + "title": "ManagedPermissionProfile", + "type": "object" + }, + { + "description": "Do not apply an outer sandbox.", + "properties": { + "type": { + "enum": [ + "disabled" + ], + "title": "DisabledPermissionProfileType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "DisabledPermissionProfile", + "type": "object" + }, + { + "description": "Filesystem isolation is enforced by an external caller.", + "properties": { + "network": { + "$ref": "#/definitions/PermissionProfileNetworkPermissions" + }, + "type": { + "enum": [ + "external" + ], + "title": "ExternalPermissionProfileType", + "type": "string" + } + }, + "required": [ + "network", + "type" + ], + "title": "ExternalPermissionProfile", + "type": "object" } - }, - "type": "object" + ] }, "PermissionProfileFileSystemPermissions": { - "properties": { - "entries": { - "items": { - "$ref": "#/definitions/FileSystemSandboxEntry" + "oneOf": [ + { + "properties": { + "entries": { + "items": { + "$ref": "#/definitions/FileSystemSandboxEntry" + }, + "type": "array" + }, + "globScanMaxDepth": { + "format": "uint", + "minimum": 1.0, + "type": [ + "integer", + "null" + ] + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } }, - "type": "array" + "required": [ + "entries", + "type" + ], + "title": "RestrictedPermissionProfileFileSystemPermissions", + "type": "object" }, - "globScanMaxDepth": { - "format": "uint", - "minimum": 1.0, - "type": [ - "integer", - "null" - ] + { + "properties": { + "type": { + "enum": [ + "unrestricted" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissionsType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "UnrestrictedPermissionProfileFileSystemPermissions", + "type": "object" } - }, - "required": [ - "entries" - ], - "type": "object" + ] }, "PermissionProfileNetworkPermissions": { "properties": { "enabled": { - "type": [ - "boolean", - "null" - ] + "type": "boolean" } }, + "required": [ + "enabled" + ], "type": "object" }, "Personality": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts index 65836c119d..5120ec3135 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/AdditionalPermissionProfile.ts @@ -4,4 +4,8 @@ import type { AdditionalFileSystemPermissions } from "./AdditionalFileSystemPermissions"; import type { AdditionalNetworkPermissions } from "./AdditionalNetworkPermissions"; -export type AdditionalPermissionProfile = { network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, }; +export type AdditionalPermissionProfile = { +/** + * Partial overlay used for per-command permission requests. + */ +network: AdditionalNetworkPermissions | null, fileSystem: AdditionalFileSystemPermissions | null, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts index c38bde54b0..7642c27650 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfile.ts @@ -4,4 +4,4 @@ import type { PermissionProfileFileSystemPermissions } from "./PermissionProfileFileSystemPermissions"; import type { PermissionProfileNetworkPermissions } from "./PermissionProfileNetworkPermissions"; -export type PermissionProfile = { network: PermissionProfileNetworkPermissions | null, fileSystem: PermissionProfileFileSystemPermissions | null, }; +export type PermissionProfile = { "type": "managed", network: PermissionProfileNetworkPermissions, fileSystem: PermissionProfileFileSystemPermissions, } | { "type": "disabled" } | { "type": "external", network: PermissionProfileNetworkPermissions, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts index 204a42764c..29aeceb433 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileFileSystemPermissions.ts @@ -3,4 +3,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry"; -export type PermissionProfileFileSystemPermissions = { entries: Array, globScanMaxDepth?: number, }; +export type PermissionProfileFileSystemPermissions = { "type": "restricted", entries: Array, globScanMaxDepth?: number, } | { "type": "unrestricted" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts index 9aa130412a..0b25a769a9 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/PermissionProfileNetworkPermissions.ts @@ -2,4 +2,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type PermissionProfileNetworkPermissions = { enabled: boolean | null, }; +export type PermissionProfileNetworkPermissions = { enabled: 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 5dc6b82a34..b69f1da012 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadForkResponse.ts @@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer, */ sandbox: SandboxPolicy, /** - * Canonical active permissions view for this thread when representable. - * This is `null` for external sandbox policies because external - * enforcement cannot be round-tripped as a `PermissionProfile`. + * Canonical active permissions view for this thread. */ permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; 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 d76ad5a58a..5ceec7f3fe 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadResumeResponse.ts @@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer, */ sandbox: SandboxPolicy, /** - * Canonical active permissions view for this thread when representable. - * This is `null` for external sandbox policies because external - * enforcement cannot be round-tripped as a `PermissionProfile`. + * Canonical active permissions view for this thread. */ permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | 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 5a83011abd..61d268afe8 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadStartResponse.ts @@ -26,8 +26,6 @@ approvalsReviewer: ApprovalsReviewer, */ sandbox: SandboxPolicy, /** - * Canonical active permissions view for this thread when representable. - * This is `null` for external sandbox policies because external - * enforcement cannot be round-tripped as a `PermissionProfile`. + * Canonical active permissions view for this thread. */ permissionProfile: PermissionProfile | null, reasoningEffort: ReasoningEffort | null, }; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index 9fa6d98e7b..40855a0952 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -1526,22 +1526,7 @@ mod tests { "type": "dangerFullAccess" }, "permissionProfile": { - "network": { - "enabled": true, - }, - "fileSystem": { - "entries": [ - { - "path": { - "type": "special", - "value": { - "kind": "root", - }, - }, - "access": "write", - }, - ], - }, + "type": "disabled" }, "reasoningEffort": null } diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a30306b345..5936b3e142 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -38,7 +38,9 @@ use codex_protocol::mcp::ResourceTemplate as McpResourceTemplate; use codex_protocol::mcp::Tool as McpTool; use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation; use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions as CoreFileSystemPermissions; +use codex_protocol::models::ManagedFileSystemPermissions as CoreManagedFileSystemPermissions; use codex_protocol::models::MessagePhase; use codex_protocol::models::NetworkPermissions as CoreNetworkPermissions; use codex_protocol::models::PermissionProfile as CorePermissionProfile; @@ -52,6 +54,7 @@ use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMod use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSpecialPath as CoreFileSystemSpecialPath; +use codex_protocol::permissions::NetworkSandboxPolicy as CoreNetworkSandboxPolicy; use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg; use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus; use codex_protocol::protocol::AgentStatus as CoreAgentStatus; @@ -1356,7 +1359,7 @@ pub struct AdditionalNetworkPermissions { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct PermissionProfileNetworkPermissions { - pub enabled: Option, + pub enabled: bool, } impl From for AdditionalNetworkPermissions { @@ -1375,18 +1378,20 @@ impl From for CoreNetworkPermissions { } } -impl From for PermissionProfileNetworkPermissions { - fn from(value: CoreNetworkPermissions) -> Self { +impl From for PermissionProfileNetworkPermissions { + fn from(value: CoreNetworkSandboxPolicy) -> Self { Self { - enabled: value.enabled, + enabled: value.is_enabled(), } } } -impl From for CoreNetworkPermissions { +impl From for CoreNetworkSandboxPolicy { fn from(value: PermissionProfileNetworkPermissions) -> Self { - Self { - enabled: value.enabled, + if value.enabled { + Self::Enabled + } else { + Self::Restricted } } } @@ -1534,65 +1539,111 @@ impl From for CoreFileSystemSandboxEntry { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] #[ts(export_to = "v2/")] -pub struct PermissionProfileFileSystemPermissions { - pub entries: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - #[ts(optional)] - pub glob_scan_max_depth: Option, +pub enum PermissionProfileFileSystemPermissions { + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Restricted { + entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + glob_scan_max_depth: Option, + }, + Unrestricted, } -impl From for PermissionProfileFileSystemPermissions { - fn from(value: CoreFileSystemPermissions) -> Self { - Self { - entries: value - .entries - .into_iter() - .map(FileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth: value.glob_scan_max_depth, +impl From for PermissionProfileFileSystemPermissions { + fn from(value: CoreManagedFileSystemPermissions) -> Self { + match value { + CoreManagedFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => Self::Restricted { + entries: entries + .into_iter() + .map(FileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth, + }, + CoreManagedFileSystemPermissions::Unrestricted => Self::Unrestricted, } } } -impl From for CoreFileSystemPermissions { +impl From for CoreManagedFileSystemPermissions { fn from(value: PermissionProfileFileSystemPermissions) -> Self { - Self { - entries: value - .entries - .into_iter() - .map(CoreFileSystemSandboxEntry::from) - .collect(), - glob_scan_max_depth: value.glob_scan_max_depth, + match value { + PermissionProfileFileSystemPermissions::Restricted { + entries, + glob_scan_max_depth, + } => Self::Restricted { + entries: entries + .into_iter() + .map(CoreFileSystemSandboxEntry::from) + .collect(), + glob_scan_max_depth, + }, + PermissionProfileFileSystemPermissions::Unrestricted => Self::Unrestricted, } } } -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] #[ts(export_to = "v2/")] -pub struct PermissionProfile { - pub network: Option, - pub file_system: Option, +pub enum PermissionProfile { + /// Codex owns sandbox construction for this profile. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + Managed { + network: PermissionProfileNetworkPermissions, + file_system: PermissionProfileFileSystemPermissions, + }, + /// Do not apply an outer sandbox. + Disabled, + /// Filesystem isolation is enforced by an external caller. + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + External { + network: PermissionProfileNetworkPermissions, + }, } impl From for PermissionProfile { fn from(value: CorePermissionProfile) -> Self { - Self { - network: value.network.map(PermissionProfileNetworkPermissions::from), - file_system: value - .file_system - .map(PermissionProfileFileSystemPermissions::from), + match value { + CorePermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + network: network.into(), + file_system: file_system.into(), + }, + CorePermissionProfile::Disabled => Self::Disabled, + CorePermissionProfile::External { network } => Self::External { + network: network.into(), + }, } } } impl From for CorePermissionProfile { fn from(value: PermissionProfile) -> Self { - Self { - network: value.network.map(CoreNetworkPermissions::from), - file_system: value.file_system.map(CoreFileSystemPermissions::from), + match value { + PermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + file_system: file_system.into(), + network: network.into(), + }, + PermissionProfile::Disabled => Self::Disabled, + PermissionProfile::External { network } => Self::External { + network: network.into(), + }, } } } @@ -1601,12 +1652,13 @@ impl From for CorePermissionProfile { #[serde(rename_all = "camelCase")] #[ts(export_to = "v2/")] pub struct AdditionalPermissionProfile { + /// Partial overlay used for per-command permission requests. pub network: Option, pub file_system: Option, } -impl From for AdditionalPermissionProfile { - fn from(value: CorePermissionProfile) -> Self { +impl From for AdditionalPermissionProfile { + fn from(value: CoreAdditionalPermissionProfile) -> Self { Self { network: value.network.map(AdditionalNetworkPermissions::from), file_system: value.file_system.map(AdditionalFileSystemPermissions::from), @@ -1614,7 +1666,7 @@ impl From for AdditionalPermissionProfile { } } -impl From for CorePermissionProfile { +impl From for CoreAdditionalPermissionProfile { fn from(value: AdditionalPermissionProfile) -> Self { Self { network: value.network.map(CoreNetworkPermissions::from), @@ -1635,7 +1687,7 @@ pub struct GrantedPermissionProfile { pub file_system: Option, } -impl From for CorePermissionProfile { +impl From for CoreAdditionalPermissionProfile { fn from(value: GrantedPermissionProfile) -> Self { Self { network: value.network.map(CoreNetworkPermissions::from), @@ -3379,9 +3431,7 @@ pub struct ThreadStartResponse { /// `permissionProfile` when present as the canonical active permissions /// view. pub sandbox: SandboxPolicy, - /// Canonical active permissions view for this thread when representable. - /// This is `null` for external sandbox policies because external - /// enforcement cannot be round-tripped as a `PermissionProfile`. + /// Canonical active permissions view for this thread. #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3485,9 +3535,7 @@ pub struct ThreadResumeResponse { /// `permissionProfile` when present as the canonical active permissions /// view. pub sandbox: SandboxPolicy, - /// Canonical active permissions view for this thread when representable. - /// This is `null` for external sandbox policies because external - /// enforcement cannot be round-tripped as a `PermissionProfile`. + /// Canonical active permissions view for this thread. #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -3582,9 +3630,7 @@ pub struct ThreadForkResponse { /// `permissionProfile` when present as the canonical active permissions /// view. pub sandbox: SandboxPolicy, - /// Canonical active permissions view for this thread when representable. - /// This is `null` for external sandbox policies because external - /// enforcement cannot be round-tripped as a `PermissionProfile`. + /// Canonical active permissions view for this thread. #[serde(default)] pub permission_profile: Option, pub reasoning_effort: Option, @@ -7896,7 +7942,7 @@ mod tests { #[test] fn permission_profile_file_system_permissions_preserves_glob_scan_depth() { - let core_permissions = CoreFileSystemPermissions { + let core_permissions = CoreManagedFileSystemPermissions::Restricted { entries: vec![CoreFileSystemSandboxEntry { path: CoreFileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -7910,7 +7956,7 @@ mod tests { assert_eq!( permissions, - PermissionProfileFileSystemPermissions { + PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::GlobPattern { pattern: "**/*.env".to_string(), @@ -7921,7 +7967,7 @@ mod tests { } ); assert_eq!( - CoreFileSystemPermissions::from(permissions), + CoreManagedFileSystemPermissions::from(permissions), core_permissions ); } @@ -7929,6 +7975,7 @@ mod tests { #[test] fn permission_profile_file_system_permissions_rejects_zero_glob_scan_depth() { serde_json::from_value::(json!({ + "type": "restricted", "entries": [], "globScanMaxDepth": 0, })) @@ -7982,8 +8029,8 @@ mod tests { ); assert_eq!( - CorePermissionProfile::from(response.permissions), - CorePermissionProfile { + CoreAdditionalPermissionProfile::from(response.permissions), + CoreAdditionalPermissionProfile { network: Some(CoreNetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a28021bb91..a038493440 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -837,7 +837,8 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin "env": { "FOO": "override" }, // optional; merges into the server env and overrides matching names "size": { "rows": 40, "cols": 120 }, // optional; PTY size in character cells, only valid with tty=true "permissionProfile": { // optional; defaults to user config - "fileSystem": { "entries": [ + "type": "managed", + "fileSystem": { "type": "restricted", "entries": [ { "path": { "type": "special", "value": { "kind": "root" } }, "access": "read" }, { "path": { "type": "special", "value": { "kind": "current_working_directory" } }, "access": "write" } ] }, diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index 58a8c2fc84..a4c424664c 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -122,7 +122,7 @@ use codex_protocol::ThreadId; use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem; use codex_protocol::dynamic_tools::DynamicToolResponse as CoreDynamicToolResponse; use codex_protocol::items::parse_hook_prompt_message; -use codex_protocol::models::PermissionProfile as CorePermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::plan_tool::UpdatePlanArgs; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::Event; @@ -2719,7 +2719,7 @@ fn request_permissions_response_from_client_result( strict_auto_review: false, }); } - let granted_permissions: CorePermissionProfile = response.permissions.into(); + let granted_permissions: CoreAdditionalPermissionProfile = response.permissions.into(); let permissions = if granted_permissions.is_empty() { CoreRequestPermissionProfile::default() } else { diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index 4d7f3c9a5a..e81ee547f5 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -317,7 +317,6 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::Result as CodexResult; use codex_protocol::items::TurnItem; use codex_protocol::models::ResponseItem; -use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::protocol::AgentStatus; use codex_protocol::protocol::ConversationAudioParams; @@ -2346,24 +2345,8 @@ impl CodexMessageProcessor { file_system_sandbox_policy: &mut FileSystemSandboxPolicy, configured_file_system_sandbox_policy: &FileSystemSandboxPolicy, ) { - if file_system_sandbox_policy.glob_scan_max_depth.is_none() { - file_system_sandbox_policy.glob_scan_max_depth = - configured_file_system_sandbox_policy.glob_scan_max_depth; - } - - for deny_entry in configured_file_system_sandbox_policy - .entries - .iter() - .filter(|entry| entry.access == FileSystemAccessMode::None) - { - if !file_system_sandbox_policy - .entries - .iter() - .any(|entry| entry == deny_entry) - { - file_system_sandbox_policy.entries.push(deny_entry.clone()); - } - } + file_system_sandbox_policy + .preserve_deny_read_restrictions_from(configured_file_system_sandbox_policy); } async fn command_exec_write( @@ -2812,10 +2795,8 @@ impl CodexMessageProcessor { /*has_in_progress_turn*/ false, ); - let permission_profile = thread_response_permission_profile( - &config_snapshot.sandbox_policy, - config_snapshot.permission_profile, - ); + let permission_profile = + thread_response_permission_profile(config_snapshot.permission_profile); let response = ThreadStartResponse { thread: thread.clone(), @@ -4631,7 +4612,6 @@ impl CodexMessageProcessor { /*has_live_in_progress_turn*/ false, ); let permission_profile = thread_response_permission_profile( - &session_configured.sandbox_policy, codex_thread.config_snapshot().await.permission_profile, ); @@ -5302,7 +5282,6 @@ impl CodexMessageProcessor { /*has_in_progress_turn*/ false, ); let permission_profile = thread_response_permission_profile( - &session_configured.sandbox_policy, forked_thread.config_snapshot().await.permission_profile, ); @@ -8827,8 +8806,7 @@ async fn handle_pending_thread_resume_request( .. } = pending.config_snapshot; let instruction_sources = pending.instruction_sources; - let permission_profile = - thread_response_permission_profile(&sandbox_policy, permission_profile); + let permission_profile = thread_response_permission_profile(permission_profile); let response = ThreadResumeResponse { thread, @@ -9958,17 +9936,9 @@ fn with_thread_spawn_agent_metadata( } fn thread_response_permission_profile( - sandbox_policy: &codex_protocol::protocol::SandboxPolicy, permission_profile: codex_protocol::models::PermissionProfile, ) -> Option { - match sandbox_policy { - codex_protocol::protocol::SandboxPolicy::DangerFullAccess - | codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } - | codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { .. } => { - Some(permission_profile.into()) - } - codex_protocol::protocol::SandboxPolicy::ExternalSandbox { .. } => None, - } + Some(permission_profile.into()) } fn requested_permissions_trust_project(overrides: &ConfigOverrides, cwd: &Path) -> bool { @@ -10285,6 +10255,7 @@ mod tests { use codex_model_provider_info::WireApi; use codex_protocol::ThreadId; use codex_protocol::openai_models::ReasoningEffort; + use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::protocol::AskForApproval; @@ -10490,25 +10461,28 @@ mod tests { } #[test] - fn thread_response_permission_profile_omits_external_sandbox() { + fn thread_response_permission_profile_preserves_enforcement() { let cwd = test_path_buf("/tmp").abs(); - let profile = codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( - &SandboxPolicy::DangerFullAccess, - cwd.as_path(), - ); - - assert_eq!( - thread_response_permission_profile( + let full_access_profile = + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + cwd.as_path(), + ); + let external_profile = + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( &SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, }, - profile.clone(), - ), - None + cwd.as_path(), + ); + + assert_eq!( + thread_response_permission_profile(external_profile.clone()), + Some(external_profile.into()) ); assert_eq!( - thread_response_permission_profile(&SandboxPolicy::DangerFullAccess, profile.clone()), - Some(profile.into()) + thread_response_permission_profile(full_access_profile.clone()), + Some(full_access_profile.into()) ); } diff --git a/codex-rs/app-server/tests/suite/v2/command_exec.rs b/codex-rs/app-server/tests/suite/v2/command_exec.rs index c24d2e80db..83718a8dc7 100644 --- a/codex-rs/app-server/tests/suite/v2/command_exec.rs +++ b/codex-rs/app-server/tests/suite/v2/command_exec.rs @@ -256,17 +256,18 @@ async fn command_exec_permission_profile_cwd_uses_command_cwd() -> Result<()> { timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; let mut permission_profile = root_read_only_permission_profile(); - permission_profile - .file_system - .as_mut() - .expect("root read-only helper should include filesystem permissions") - .entries - .push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::CurrentWorkingDirectory, - }, - access: FileSystemAccessMode::Write, - }); + let PermissionProfile::Managed { file_system, .. } = &mut permission_profile else { + panic!("root read-only helper should use managed permissions"); + }; + let PermissionProfileFileSystemPermissions::Restricted { entries, .. } = file_system else { + panic!("root read-only helper should use restricted filesystem permissions"); + }; + entries.push(FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::CurrentWorkingDirectory, + }, + access: FileSystemAccessMode::Write, + }); let command_request_id = mcp .send_command_exec_request(CommandExecParams { @@ -1061,11 +1062,9 @@ fn decode_delta_notification( } fn root_read_only_permission_profile() -> PermissionProfile { - PermissionProfile { - network: Some(PermissionProfileNetworkPermissions { - enabled: Some(false), - }), - file_system: Some(PermissionProfileFileSystemPermissions { + PermissionProfile::Managed { + network: PermissionProfileNetworkPermissions { enabled: false }, + file_system: PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::Root, @@ -1073,7 +1072,7 @@ fn root_read_only_permission_profile() -> PermissionProfile { access: FileSystemAccessMode::Read, }], glob_scan_max_depth: None, - }), + }, } } diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 4a9b2c6a16..04e6ede0c5 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -40,6 +40,7 @@ use codex_app_server_protocol::PatchApplyStatus; use codex_app_server_protocol::PatchChangeKind; use codex_app_server_protocol::PermissionProfile; use codex_app_server_protocol::PermissionProfileFileSystemPermissions; +use codex_app_server_protocol::PermissionProfileNetworkPermissions; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ServerRequest; use codex_app_server_protocol::ServerRequestResolvedNotification; @@ -781,9 +782,9 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> text: "Hello".to_string(), text_elements: Vec::new(), }], - permission_profile: Some(PermissionProfile { - network: None, - file_system: Some(PermissionProfileFileSystemPermissions { + permission_profile: Some(PermissionProfile::Managed { + network: PermissionProfileNetworkPermissions { enabled: false }, + file_system: PermissionProfileFileSystemPermissions::Restricted { entries: vec![FileSystemSandboxEntry { path: FileSystemPath::Path { path: unsupported_write_root, @@ -791,7 +792,7 @@ async fn turn_start_rejects_invalid_permission_profile_before_starting_turn() -> access: FileSystemAccessMode::Write, }], glob_scan_max_depth: None, - }), + }, }), ..Default::default() }) diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index d46d1a316d..9f7778c3c3 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -55,8 +55,6 @@ use codex_model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; use codex_model_provider_info::OLLAMA_OSS_PROVIDER_ID; use codex_model_provider_info::WireApi; use codex_models_manager::bundled_models_response; -use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; @@ -810,20 +808,7 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: async fn permission_profile_override_populates_runtime_permissions() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; - let permission_profile = PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions { - entries: vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Write, - }], - glob_scan_max_depth: None, - }), - }; + let permission_profile = PermissionProfile::Disabled; let config = Config::load_from_base_config_with_overrides( ConfigToml::default(), @@ -848,20 +833,7 @@ async fn permission_profile_override_populates_runtime_permissions() -> std::io: async fn permission_profile_override_preserves_configured_network_proxy() -> std::io::Result<()> { let codex_home = TempDir::new()?; let cwd = TempDir::new()?; - let permission_profile = PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(true), - }), - file_system: Some(FileSystemPermissions { - entries: vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Write, - }], - glob_scan_max_depth: None, - }), - }; + let permission_profile = PermissionProfile::Disabled; let config = Config::load_from_base_config_with_overrides( ConfigToml { diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index cb6f788399..13d4a5d0a1 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -79,6 +79,7 @@ use codex_protocol::config_types::WebSearchConfig; use codex_protocol::config_types::WebSearchMode; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::openai_models::ModelsResponse; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -221,7 +222,8 @@ impl Permissions { /// Effective runtime permissions after config requirements and runtime /// readable-root additions have been applied. pub fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()), &self.file_system_sandbox_policy, self.network_sandbox_policy, ) @@ -1773,9 +1775,9 @@ impl Config { })?; let profile = resolve_permission_profile(permissions, default_permissions)?; - // PermissionProfile only carries the network enabled bit today. Keep the - // configured proxy/allowlist policy so active profiles can round-trip without - // broadening network behavior. + // 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()) } else { NetworkProxyConfig::default() diff --git a/codex-rs/core/src/guardian/approval_request.rs b/codex-rs/core/src/guardian/approval_request.rs index 2afc5e0805..fba227834a 100644 --- a/codex-rs/core/src/guardian/approval_request.rs +++ b/codex-rs/core/src/guardian/approval_request.rs @@ -4,7 +4,7 @@ use codex_analytics::GuardianReviewedAction; use codex_protocol::approvals::GuardianAssessmentAction; use codex_protocol::approvals::GuardianCommandSource; use codex_protocol::approvals::NetworkApprovalProtocol; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::request_permissions::RequestPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use serde::Serialize; @@ -20,7 +20,7 @@ pub(crate) enum GuardianApprovalRequest { command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: crate::sandboxing::SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, justification: Option, }, ExecCommand { @@ -28,7 +28,7 @@ pub(crate) enum GuardianApprovalRequest { command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: crate::sandboxing::SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, justification: Option, tty: bool, }, @@ -39,7 +39,7 @@ pub(crate) enum GuardianApprovalRequest { program: String, argv: Vec, cwd: AbsolutePathBuf, - additional_permissions: Option, + additional_permissions: Option, }, ApplyPatch { id: String, @@ -85,7 +85,7 @@ pub(crate) struct GuardianNetworkAccessTrigger { pub(crate) cwd: AbsolutePathBuf, pub(crate) sandbox_permissions: crate::sandboxing::SandboxPermissions, #[serde(skip_serializing_if = "Option::is_none")] - pub(crate) additional_permissions: Option, + pub(crate) additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub(crate) justification: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -109,7 +109,7 @@ struct CommandApprovalAction<'a> { cwd: &'a Path, sandbox_permissions: crate::sandboxing::SandboxPermissions, #[serde(skip_serializing_if = "Option::is_none")] - additional_permissions: Option<&'a PermissionProfile>, + additional_permissions: Option<&'a AdditionalPermissionProfile>, #[serde(skip_serializing_if = "Option::is_none")] justification: Option<&'a String>, #[serde(skip_serializing_if = "Option::is_none")] @@ -124,7 +124,7 @@ struct ExecveApprovalAction<'a> { argv: &'a [String], cwd: &'a Path, #[serde(skip_serializing_if = "Option::is_none")] - additional_permissions: Option<&'a PermissionProfile>, + additional_permissions: Option<&'a AdditionalPermissionProfile>, } #[derive(Serialize)] @@ -178,7 +178,7 @@ fn serialize_command_guardian_action( command: &[String], cwd: &Path, sandbox_permissions: crate::sandboxing::SandboxPermissions, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, justification: Option<&String>, tty: Option, ) -> serde_json::Result { diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index 0fffe1a29d..ca865300a3 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -90,11 +90,12 @@ use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::items::TurnItem; use codex_protocol::items::UserMessageItem; use codex_protocol::mcp::CallToolResult; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::BaseInstructions; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::models::format_allow_prefixes; use codex_protocol::openai_models::ModelInfo; -use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::FileChange; @@ -1843,7 +1844,7 @@ impl Session { reason: Option, network_approval_context: Option, proposed_execpolicy_amendment: Option, - additional_permissions: Option, + additional_permissions: Option, available_decisions: Option>, ) -> ReviewDecision { // command-level approvals use `call_id`. @@ -2265,7 +2266,8 @@ impl Session { PermissionGrantScope::Turn => { if let Some(turn_state) = originating_turn_state { let mut ts = turn_state.lock().await; - let permissions: PermissionProfile = response.permissions.clone().into(); + let permissions: AdditionalPermissionProfile = + response.permissions.clone().into(); ts.record_granted_permissions(permissions); if response.strict_auto_review { ts.enable_strict_auto_review(); @@ -2283,7 +2285,7 @@ impl Session { clippy::await_holding_invalid_type, reason = "active turn reads must stay consistent with the matching turn state" )] - pub(crate) async fn granted_turn_permissions(&self) -> Option { + pub(crate) async fn granted_turn_permissions(&self) -> Option { let active = self.active_turn.lock().await; let active = active.as_ref()?; let ts = active.turn_state.lock().await; @@ -2303,7 +2305,7 @@ impl Session { ts.strict_auto_review_enabled() } - pub(crate) async fn granted_session_permissions(&self) -> Option { + pub(crate) async fn granted_session_permissions(&self) -> Option { let state = self.state.lock().await; state.granted_permissions() } diff --git a/codex-rs/core/src/session/session.rs b/codex-rs/core/src/session/session.rs index f7a36c1282..af8dec1f86 100644 --- a/codex-rs/core/src/session/session.rs +++ b/codex-rs/core/src/session/session.rs @@ -94,7 +94,8 @@ impl SessionConfiguration { } pub(super) fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(self.sandbox_policy.get()), &self.file_system_sandbox_policy, self.network_sandbox_policy, ) @@ -182,26 +183,8 @@ impl SessionConfiguration { next_configuration.sandbox_policy.set(sandbox_policy)?; let (mut file_system_sandbox_policy, network_sandbox_policy) = permission_profile.to_runtime_permissions(); - if file_system_sandbox_policy.glob_scan_max_depth.is_none() { - file_system_sandbox_policy.glob_scan_max_depth = - self.file_system_sandbox_policy.glob_scan_max_depth; - } - for deny_entry in self - .file_system_sandbox_policy - .entries - .iter() - .filter(|entry| { - entry.access == codex_protocol::permissions::FileSystemAccessMode::None - }) - { - if !file_system_sandbox_policy - .entries - .iter() - .any(|entry| entry == deny_entry) - { - file_system_sandbox_policy.entries.push(deny_entry.clone()); - } - } + file_system_sandbox_policy + .preserve_deny_read_restrictions_from(&self.file_system_sandbox_policy); next_configuration.file_system_sandbox_policy = file_system_sandbox_policy; next_configuration.network_sandbox_policy = network_sandbox_policy; } else if let Some(sandbox_policy) = updates.sandbox_policy.clone() { @@ -825,14 +808,6 @@ impl Session { // Dispatch the SessionConfiguredEvent first and then report any errors. // If resuming, include converted initial messages in the payload so UIs can render them immediately. let initial_messages = initial_history.get_event_msgs(); - let permission_profile = if matches!( - session_configuration.file_system_sandbox_policy.kind, - FileSystemSandboxKind::ExternalSandbox - ) { - None - } else { - Some(session_configuration.permission_profile()) - }; let events = std::iter::once(Event { id: INITIAL_SUBMIT_ID.to_owned(), msg: EventMsg::SessionConfigured(SessionConfiguredEvent { @@ -845,7 +820,7 @@ impl Session { approval_policy: session_configuration.approval_policy.value(), approvals_reviewer: session_configuration.approvals_reviewer, sandbox_policy: session_configuration.sandbox_policy.get().clone(), - permission_profile, + permission_profile: Some(session_configuration.permission_profile()), cwd: session_configuration.cwd.clone(), reasoning_effort: session_configuration.collaboration_mode.reasoning_effort(), history_log_id, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index a36b3a421a..508eadfba0 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -1474,7 +1474,8 @@ async fn record_initial_history_reconstructs_forked_transcript() { } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn session_configured_omits_permission_profile_for_external_sandbox() -> anyhow::Result<()> { +async fn session_configured_reports_permission_profile_for_external_sandbox() -> anyhow::Result<()> +{ let server = start_mock_server().await; let sandbox_policy = SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Restricted, @@ -1492,10 +1493,15 @@ async fn session_configured_omits_permission_profile_for_external_sandbox() -> a test.session_configured.sandbox_policy, expected_sandbox_policy ); + let expected_permission_profile = + codex_protocol::models::PermissionProfile::from_legacy_sandbox_policy( + &expected_sandbox_policy, + test.session_configured.cwd.as_path(), + ); assert_eq!( - test.session_configured.permission_profile, None, - "ExternalSandbox is enforced outside the PermissionProfile model, so SessionConfigured must \ - not expose a lossy root-write profile" + test.session_configured.permission_profile, + Some(expected_permission_profile), + "ExternalSandbox is represented explicitly instead of as a lossy root-write profile" ); Ok(()) } diff --git a/codex-rs/core/src/session/tests/guardian_tests.rs b/codex-rs/core/src/session/tests/guardian_tests.rs index 6423bee28d..f527182fda 100644 --- a/codex-rs/core/src/session/tests/guardian_tests.rs +++ b/codex-rs/core/src/session/tests/guardian_tests.rs @@ -20,9 +20,9 @@ use codex_execpolicy::RuleMatch; use codex_features::Feature; use codex_model_provider::create_model_provider; use codex_protocol::config_types::ApprovalsReviewer; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::ContentItem; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::models::ResponseItem; use codex_protocol::models::function_call_output_content_items_to_text; use codex_protocol::permissions::FileSystemSandboxPolicy; diff --git a/codex-rs/core/src/session/turn_context.rs b/codex-rs/core/src/session/turn_context.rs index 0898bd89af..8c831e6b61 100644 --- a/codex-rs/core/src/session/turn_context.rs +++ b/codex-rs/core/src/session/turn_context.rs @@ -1,8 +1,11 @@ use super::*; use codex_model_provider::SharedModelProvider; use codex_model_provider::create_model_provider; +use codex_protocol::models::AdditionalPermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::protocol::TurnEnvironmentSelection; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; @@ -94,7 +97,8 @@ pub(crate) struct TurnContext { } impl TurnContext { pub(crate) fn permission_profile(&self) -> PermissionProfile { - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), &self.file_system_sandbox_policy, self.network_sandbox_policy, ) @@ -243,12 +247,21 @@ impl TurnContext { pub(crate) fn file_system_sandbox_context( &self, - additional_permissions: Option, + additional_permissions: Option, ) -> FileSystemSandboxContext { - let base_permissions = self.permission_profile(); - let permissions = - merge_permission_profiles(Some(&base_permissions), additional_permissions.as_ref()) - .unwrap_or(base_permissions); + let file_system_sandbox_policy = effective_file_system_sandbox_policy( + &self.file_system_sandbox_policy, + additional_permissions.as_ref(), + ); + let network_sandbox_policy = effective_network_sandbox_policy( + self.network_sandbox_policy, + additional_permissions.as_ref(), + ); + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), + &file_system_sandbox_policy, + network_sandbox_policy, + ); FileSystemSandboxContext { permissions, cwd: Some(self.cwd.clone()), diff --git a/codex-rs/core/src/state/session.rs b/codex-rs/core/src/state/session.rs index e9fd676332..3bd4b8a26e 100644 --- a/codex-rs/core/src/state/session.rs +++ b/codex-rs/core/src/state/session.rs @@ -1,6 +1,6 @@ //! Session-wide mutable state. -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::ResponseItem; use codex_sandboxing::policy_transforms::merge_permission_profiles; use std::collections::HashMap; @@ -32,7 +32,7 @@ pub(crate) struct SessionState { pub(crate) startup_prewarm: Option, pub(crate) active_connector_selection: HashSet, pub(crate) pending_session_start_source: Option, - granted_permissions: Option, + granted_permissions: Option, next_turn_is_first: bool, } @@ -218,12 +218,12 @@ impl SessionState { self.pending_session_start_source.take() } - pub(crate) fn record_granted_permissions(&mut self, permissions: PermissionProfile) { + pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) { self.granted_permissions = merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions)); } - pub(crate) fn granted_permissions(&self) -> Option { + pub(crate) fn granted_permissions(&self) -> Option { self.granted_permissions.clone() } } diff --git a/codex-rs/core/src/state/turn.rs b/codex-rs/core/src/state/turn.rs index 5e1526ad55..48b7a26ccb 100644 --- a/codex-rs/core/src/state/turn.rs +++ b/codex-rs/core/src/state/turn.rs @@ -21,7 +21,7 @@ use tokio::sync::oneshot; use crate::session::turn_context::TurnContext; use crate::tasks::AnySessionTask; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_protocol::protocol::TokenUsage; @@ -105,7 +105,7 @@ pub(crate) struct TurnState { pending_dynamic_tools: HashMap>, pending_input: Vec, mailbox_delivery_phase: MailboxDeliveryPhase, - granted_permissions: Option, + granted_permissions: Option, strict_auto_review_enabled: bool, pub(crate) tool_calls: u64, pub(crate) has_memory_citation: bool, @@ -247,12 +247,12 @@ impl TurnState { self.mailbox_delivery_phase = phase; } - pub(crate) fn record_granted_permissions(&mut self, permissions: PermissionProfile) { + pub(crate) fn record_granted_permissions(&mut self, permissions: AdditionalPermissionProfile) { self.granted_permissions = merge_permission_profiles(self.granted_permissions.as_ref(), Some(&permissions)); } - pub(crate) fn granted_permissions(&self) -> Option { + pub(crate) fn granted_permissions(&self) -> Option { self.granted_permissions.clone() } diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index 91d5543c0f..9d63ad0a8b 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -39,8 +39,8 @@ use codex_apply_patch::Hunk; use codex_apply_patch::parse_patch_streaming; use codex_exec_server::ExecutorFileSystem; use codex_features::Feature; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::FileChange; use codex_protocol::protocol::PatchApplyUpdatedEvent; @@ -215,7 +215,7 @@ fn write_permissions_for_paths( file_paths: &[AbsolutePathBuf], file_system_sandbox_policy: &codex_protocol::permissions::FileSystemSandboxPolicy, cwd: &AbsolutePathBuf, -) -> Option { +) -> Option { let write_paths = file_paths .iter() .map(|path| { @@ -232,7 +232,7 @@ fn write_permissions_for_paths( .collect::, _>>() .ok()?; - let permissions = (!write_paths.is_empty()).then_some(PermissionProfile { + let permissions = (!write_paths.is_empty()).then_some(AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( Some(vec![]), Some(write_paths), diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index af4178f71f..7878c1092c 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -34,7 +34,7 @@ use crate::session::session::Session; pub(crate) use crate::tools::code_mode::CodeModeExecuteHandler; pub(crate) use crate::tools::code_mode::CodeModeWaitHandler; pub use apply_patch::ApplyPatchHandler; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::AskForApproval; pub use dynamic::DynamicToolHandler; pub use js_repl::JsReplHandler; @@ -93,10 +93,10 @@ pub(crate) fn normalize_and_validate_additional_permissions( additional_permissions_allowed: bool, approval_policy: AskForApproval, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, permissions_preapproved: bool, _cwd: &Path, -) -> Result, String> { +) -> Result, String> { let uses_additional_permissions = matches!( sandbox_permissions, SandboxPermissions::WithAdditionalPermissions @@ -146,15 +146,15 @@ pub(crate) fn normalize_and_validate_additional_permissions( pub(super) struct EffectiveAdditionalPermissions { pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub permissions_preapproved: bool, } pub(super) fn implicit_granted_permissions( sandbox_permissions: SandboxPermissions, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, effective_additional_permissions: &EffectiveAdditionalPermissions, -) -> Option { +) -> Option { if !sandbox_permissions.uses_additional_permissions() && !matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) && additional_permissions.is_none() @@ -171,7 +171,7 @@ pub(super) async fn apply_granted_turn_permissions( session: &Session, cwd: &std::path::Path, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, ) -> EffectiveAdditionalPermissions { if matches!(sandbox_permissions, SandboxPermissions::RequireEscalated) { return EffectiveAdditionalPermissions { @@ -213,8 +213,8 @@ pub(super) async fn apply_granted_turn_permissions( } fn permissions_are_preapproved( - effective_permissions: &PermissionProfile, - granted_permissions: PermissionProfile, + effective_permissions: &AdditionalPermissionProfile, + granted_permissions: AdditionalPermissionProfile, cwd: &Path, ) -> bool { let materialized_effective_permissions = intersect_permission_profiles( @@ -233,9 +233,9 @@ mod tests { use super::normalize_and_validate_additional_permissions; use super::permissions_are_preapproved; use crate::sandboxing::SandboxPermissions; + use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; - use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -248,8 +248,8 @@ mod tests { use pretty_assertions::assert_eq; use tempfile::tempdir; - fn network_permissions() -> PermissionProfile { - PermissionProfile { + fn network_permissions() -> AdditionalPermissionProfile { + AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), @@ -257,8 +257,8 @@ mod tests { } } - fn file_system_permissions(path: &std::path::Path) -> PermissionProfile { - PermissionProfile { + fn file_system_permissions(path: &std::path::Path) -> AdditionalPermissionProfile { + AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![ @@ -350,7 +350,7 @@ mod tests { #[test] fn relative_deny_glob_grants_remain_preapproved_after_materialization() { let cwd = tempdir().expect("tempdir"); - let requested_permissions = PermissionProfile { + let requested_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![ FileSystemSandboxEntry { diff --git a/codex-rs/core/src/tools/handlers/shell.rs b/codex-rs/core/src/tools/handlers/shell.rs index f4ab61b524..17daaa7380 100644 --- a/codex-rs/core/src/tools/handlers/shell.rs +++ b/codex-rs/core/src/tools/handlers/shell.rs @@ -36,7 +36,7 @@ use crate::tools::runtimes::shell::ShellRuntime; use crate::tools::runtimes::shell::ShellRuntimeBackend; use crate::tools::sandboxing::ToolCtx; use codex_features::Feature; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ExecCommandSource; use codex_shell_command::is_safe_command::is_known_safe_command; use codex_tools::ShellCommandBackendConfig; @@ -79,7 +79,7 @@ struct RunExecLikeArgs { tool_name: String, exec_params: ExecParams, hook_command: String, - additional_permissions: Option, + additional_permissions: Option, prefix_rule: Option>, session: Arc, turn: Arc, diff --git a/codex-rs/core/src/tools/handlers/unified_exec.rs b/codex-rs/core/src/tools/handlers/unified_exec.rs index 05b54ff884..6a3203a05b 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec.rs @@ -28,7 +28,7 @@ use crate::unified_exec::generate_chunk_id; use codex_features::Feature; use codex_otel::SessionTelemetry; use codex_otel::TOOL_CALL_UNIFIED_EXEC_METRIC; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::TerminalInteractionEvent; use codex_shell_command::is_safe_command::is_known_safe_command; @@ -58,7 +58,7 @@ pub(crate) struct ExecCommandArgs { #[serde(default)] sandbox_permissions: SandboxPermissions, #[serde(default)] - additional_permissions: Option, + additional_permissions: Option, #[serde(default)] justification: Option, #[serde(default)] diff --git a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs index e0a0af5edc..1bdd0b82f9 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec_tests.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec_tests.rs @@ -2,8 +2,8 @@ use super::*; use crate::shell::default_user_shell; use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::resolve_workdir_base_path; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; use codex_tools::UnifiedExecShellMode; use codex_tools::ZshForkConfig; use codex_utils_absolute_path::AbsolutePathBuf; diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index ed325a4e68..ecf3ccdc04 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -23,7 +23,9 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::Event; use codex_protocol::protocol::EventMsg; @@ -33,7 +35,8 @@ use codex_protocol::protocol::FileChange; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use codex_utils_absolute_path::AbsolutePathBuf; use futures::future::BoxFuture; use std::path::PathBuf; @@ -45,7 +48,7 @@ pub struct ApplyPatchRequest { pub file_paths: Vec, pub changes: std::collections::HashMap, pub exec_approval_requirement: ExecApprovalRequirement, - pub additional_permissions: Option, + pub additional_permissions: Option, pub permissions_preapproved: bool, } @@ -77,13 +80,19 @@ impl ApplyPatchRuntime { return None; } - let base_permissions = PermissionProfile::from_runtime_permissions( + let file_system_policy = effective_file_system_sandbox_policy( attempt.file_system_policy, + req.additional_permissions.as_ref(), + ); + let network_policy = effective_network_sandbox_policy( attempt.network_policy, + req.additional_permissions.as_ref(), + ); + let permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(attempt.policy), + &file_system_policy, + network_policy, ); - let permissions = - merge_permission_profiles(Some(&base_permissions), req.additional_permissions.as_ref()) - .unwrap_or(base_permissions); Some(FileSystemSandboxContext { permissions, cwd: Some(attempt.sandbox_cwd.clone()), diff --git a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs index 112a3d3a56..0bc4d2e6f9 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch_tests.rs @@ -1,6 +1,7 @@ use super::*; use crate::tools::sandboxing::SandboxAttempt; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; @@ -9,7 +10,8 @@ use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxManager; use codex_sandboxing::SandboxType; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use core_test_support::PathBufExt; use pretty_assertions::assert_eq; use std::collections::HashMap; @@ -116,7 +118,7 @@ fn file_system_sandbox_context_uses_active_attempt() { let path = std::env::temp_dir() .join("apply-patch-runtime-attempt.txt") .abs(); - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { network: None, file_system: Some(FileSystemPermissions::from_read_write_roots( Some(vec![path.clone()]), @@ -154,15 +156,14 @@ fn file_system_sandbox_context_uses_active_attempt() { let sandbox = ApplyPatchRuntime::file_system_sandbox_context_for_attempt(&req, &attempt) .expect("sandbox context"); - let base_permissions = PermissionProfile::from_runtime_permissions( - &file_system_policy, + let file_system_policy = + effective_file_system_sandbox_policy(&file_system_policy, Some(&additional_permissions)); + let network_policy = effective_network_sandbox_policy( NetworkSandboxPolicy::Restricted, + Some(&additional_permissions), ); - let Some(expected_permissions) = - merge_permission_profiles(Some(&base_permissions), Some(&additional_permissions)) - else { - panic!("merged permissions should not be empty"); - }; + let expected_permissions = + PermissionProfile::from_runtime_permissions(&file_system_policy, network_policy); assert_eq!(sandbox.permissions, expected_permissions); assert_eq!(sandbox.cwd, Some(path.clone())); assert_eq!( diff --git a/codex-rs/core/src/tools/runtimes/mod.rs b/codex-rs/core/src/tools/runtimes/mod.rs index 2246fa18f5..a55f78f8a4 100644 --- a/codex-rs/core/src/tools/runtimes/mod.rs +++ b/codex-rs/core/src/tools/runtimes/mod.rs @@ -14,7 +14,7 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY; use codex_network_proxy::PROXY_ENV_KEYS; #[cfg(target_os = "macos")] use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_sandboxing::SandboxCommand; use codex_utils_absolute_path::AbsolutePathBuf; use std::collections::HashMap; @@ -29,7 +29,7 @@ pub(crate) fn build_sandbox_command( command: &[String], cwd: &AbsolutePathBuf, env: &HashMap, - additional_permissions: Option, + additional_permissions: Option, ) -> Result { let (program, args) = command .split_first() diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index e18d2c269a..edaa2f4721 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -35,7 +35,7 @@ use crate::tools::sandboxing::sandbox_override_for_first_attempt; use crate::tools::sandboxing::with_cached_approval; use codex_network_proxy::NetworkProxy; use codex_protocol::exec_output::ExecToolCallOutput; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxablePreference; use codex_shell_command::powershell::prefix_powershell_script_with_utf8; @@ -53,7 +53,7 @@ pub struct ShellRequest { pub explicit_env_overrides: HashMap, pub network: Option, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, #[cfg(unix)] pub additional_permissions_preapproved: bool, pub justification: Option, @@ -97,7 +97,7 @@ pub(crate) struct ApprovalKey { command: Vec, cwd: AbsolutePathBuf, sandbox_permissions: SandboxPermissions, - additional_permissions: Option, + additional_permissions: Option, } impl ShellRuntime { diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index b29e9c5937..369689f9d0 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -30,7 +30,9 @@ use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::exec_output::StreamOutput; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::PermissionProfile; +use codex_protocol::models::SandboxEnforcement; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; @@ -311,7 +313,7 @@ struct CoreShellActionProvider { network_sandbox_policy: NetworkSandboxPolicy, sandbox_permissions: SandboxPermissions, approval_sandbox_permissions: SandboxPermissions, - prompt_permissions: Option, + prompt_permissions: Option, stopwatch: Stopwatch, } @@ -361,7 +363,7 @@ impl CoreShellActionProvider { sandbox_policy: &SandboxPolicy, file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> EscalationExecution { match sandbox_permissions { SandboxPermissions::UseDefault => EscalationExecution::TurnDefault, @@ -373,10 +375,14 @@ impl CoreShellActionProvider { EscalationExecution::Permissions( EscalationPermissions::ResolvedPermissionProfile( ResolvedPermissionProfile { - permission_profile: PermissionProfile::from_runtime_permissions( - file_system_sandbox_policy, - network_sandbox_policy, - ), + permission_profile: + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy( + sandbox_policy, + ), + file_system_sandbox_policy, + network_sandbox_policy, + ), sandbox_policy: sandbox_policy.clone(), }, ), @@ -392,7 +398,7 @@ impl CoreShellActionProvider { argv: &[String], workdir: &AbsolutePathBuf, stopwatch: &Stopwatch, - additional_permissions: Option, + additional_permissions: Option, ) -> anyhow::Result { let command = join_program_and_argv(program, argv); let workdir = workdir.clone(); @@ -491,7 +497,7 @@ impl CoreShellActionProvider { program: &AbsolutePathBuf, argv: &[String], workdir: &AbsolutePathBuf, - prompt_permissions: Option, + prompt_permissions: Option, escalation_execution: EscalationExecution, decision_source: DecisionSource, ) -> anyhow::Result { @@ -757,7 +763,7 @@ struct PrepareSandboxedExecParams<'a> { sandbox_policy: &'a SandboxPolicy, file_system_sandbox_policy: &'a FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, - additional_permissions: Option, + additional_permissions: Option, } #[async_trait::async_trait] diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index a2b6ca145e..86753a04d7 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -16,6 +16,7 @@ use codex_execpolicy::PolicyParser; use codex_execpolicy::RuleMatch; use codex_hooks::Hooks; use codex_hooks::HooksConfig; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; @@ -257,7 +258,7 @@ fn map_exec_result_preserves_stdout_and_stderr() { #[test] fn shell_request_escalation_execution_is_explicit() { - let requested_permissions = PermissionProfile { + let requested_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![ diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index 96d7abf64c..be185e5fef 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -38,7 +38,7 @@ use crate::unified_exec::UnifiedExecProcessManager; use codex_network_proxy::NetworkProxy; use codex_protocol::error::CodexErr; use codex_protocol::error::SandboxErr; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::ReviewDecision; use codex_sandboxing::SandboxablePreference; use codex_shell_command::powershell::prefix_powershell_script_with_utf8; @@ -61,7 +61,7 @@ pub struct UnifiedExecRequest { pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, #[cfg(unix)] pub additional_permissions_preapproved: bool, pub justification: Option, @@ -76,7 +76,7 @@ pub struct UnifiedExecApprovalKey { pub cwd: AbsolutePathBuf, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, } /// Runtime adapter that keeps policy and sandbox orchestration on the diff --git a/codex-rs/core/src/unified_exec/mod.rs b/codex-rs/core/src/unified_exec/mod.rs index a5a6e69f89..acf361f94b 100644 --- a/codex-rs/core/src/unified_exec/mod.rs +++ b/codex-rs/core/src/unified_exec/mod.rs @@ -28,7 +28,7 @@ use std::sync::Arc; use std::sync::Weak; use codex_network_proxy::NetworkProxy; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use rand::Rng; use rand::rng; @@ -96,7 +96,7 @@ pub(crate) struct ExecCommandRequest { pub network: Option, pub tty: bool, pub sandbox_permissions: SandboxPermissions, - pub additional_permissions: Option, + pub additional_permissions: Option, pub additional_permissions_preapproved: bool, pub justification: Option, pub prefix_rule: Option>, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index bcc938734d..319e3ef8ec 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -5,8 +5,8 @@ use codex_core::config::Constrained; use codex_core::sandboxing::SandboxPermissions; use codex_features::Feature; use codex_protocol::config_types::ApprovalsReviewer; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::EventMsg; use codex_protocol::protocol::ExecApprovalRequestEvent; diff --git a/codex-rs/exec-server/src/file_system.rs b/codex-rs/exec-server/src/file_system.rs index a474b35368..37237f60dd 100644 --- a/codex-rs/exec-server/src/file_system.rs +++ b/codex-rs/exec-server/src/file_system.rs @@ -5,7 +5,6 @@ use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxKind; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; -use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use std::path::Path; @@ -58,10 +57,8 @@ pub struct FileSystemSandboxContext { impl FileSystemSandboxContext { pub fn from_legacy_sandbox_policy(sandbox_policy: SandboxPolicy, cwd: AbsolutePathBuf) -> Self { - let permissions = PermissionProfile::from_runtime_permissions( - &FileSystemSandboxPolicy::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()), - NetworkSandboxPolicy::from(&sandbox_policy), - ); + let permissions = + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy, cwd.as_path()); Self::from_permission_profile_with_cwd(permissions, cwd) } diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index 9836a5b5bd..b9f7456f3c 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -536,11 +536,9 @@ mod tests { }, access: FileSystemAccessMode::Write, }]); - let sandbox_context = - crate::FileSystemSandboxContext::from_permission_profile(PermissionProfile { - network: None, - file_system: Some((&policy).into()), - }); + let sandbox_context = crate::FileSystemSandboxContext::from_permission_profile( + PermissionProfile::from_runtime_permissions(&policy, NetworkSandboxPolicy::Restricted), + ); let err = sandbox_cwd(&sandbox_context).expect_err("missing cwd should be rejected"); diff --git a/codex-rs/exec-server/tests/file_system.rs b/codex-rs/exec-server/tests/file_system.rs index 0a7c000ae1..c42159a6dd 100644 --- a/codex-rs/exec-server/tests/file_system.rs +++ b/codex-rs/exec-server/tests/file_system.rs @@ -21,13 +21,16 @@ use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::LocalFileSystem; use codex_exec_server::ReadDirectoryEntry; use codex_exec_server::RemoveOptions; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; -use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; -use codex_sandboxing::policy_transforms::merge_permission_profiles; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; +use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy; +use codex_sandboxing::policy_transforms::effective_network_sandbox_policy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -102,15 +105,10 @@ fn workspace_write_sandbox(writable_root: std::path::PathBuf) -> FileSystemSandb } fn sandbox_context(entries: Vec) -> FileSystemSandboxContext { - FileSystemSandboxContext::from_permission_profile(PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(false), - }), - file_system: Some(FileSystemPermissions { - entries, - glob_scan_max_depth: None, - }), - }) + FileSystemSandboxContext::from_permission_profile(PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(entries), + NetworkSandboxPolicy::Restricted, + )) } #[test] @@ -604,19 +602,26 @@ async fn file_system_sandboxed_write_allows_additional_write_root(use_remote: bo std::fs::create_dir_all(&writable_dir)?; let mut sandbox = read_only_sandbox(readable_dir); - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { network: None, file_system: Some(FileSystemPermissions::from_read_write_roots( /*read*/ None, Some(vec![absolute_path(writable_dir)]), )), }; - let Some(permissions) = - merge_permission_profiles(Some(&sandbox.permissions), Some(&additional_permissions)) - else { - panic!("merged permissions should not be empty"); - }; - sandbox.permissions = permissions; + let file_system_policy = effective_file_system_sandbox_policy( + &sandbox.permissions.file_system_sandbox_policy(), + Some(&additional_permissions), + ); + let network_policy = effective_network_sandbox_policy( + sandbox.permissions.network_sandbox_policy(), + Some(&additional_permissions), + ); + sandbox.permissions = PermissionProfile::from_runtime_permissions_with_enforcement( + sandbox.permissions.enforcement(), + &file_system_policy, + network_policy, + ); file_system .write_file( diff --git a/codex-rs/protocol/src/approvals.rs b/codex-rs/protocol/src/approvals.rs index 2b9aea2721..6fc5e49b49 100644 --- a/codex-rs/protocol/src/approvals.rs +++ b/codex-rs/protocol/src/approvals.rs @@ -1,4 +1,5 @@ use crate::mcp::RequestId; +use crate::models::AdditionalPermissionProfile; use crate::models::PermissionProfile; use crate::parse_command::ParsedCommand; use crate::protocol::FileChange; @@ -28,7 +29,7 @@ pub struct ResolvedPermissionProfile { #[derive(Debug, Clone, PartialEq, Eq)] pub enum EscalationPermissions { /// Permissions to merge with the active turn permissions. - AdditionalPermissionProfile(PermissionProfile), + AdditionalPermissionProfile(AdditionalPermissionProfile), /// Fully resolved permissions that should replace the active turn permissions. ResolvedPermissionProfile(ResolvedPermissionProfile), } @@ -249,7 +250,7 @@ pub struct ExecApprovalRequestEvent { /// Optional additional filesystem permissions requested for this command. #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, /// Ordered list of decisions the client may present for this prompt. /// /// When absent, clients should derive the legacy default set from the @@ -285,7 +286,7 @@ impl ExecApprovalRequestEvent { network_approval_context: Option<&NetworkApprovalContext>, proposed_execpolicy_amendment: Option<&ExecPolicyAmendment>, proposed_network_policy_amendments: Option<&[NetworkPolicyAmendment]>, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> Vec { if network_approval_context.is_some() { let mut decisions = vec![ReviewDecision::Approved, ReviewDecision::ApprovedForSession]; diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 551723c773..2b02ee88b3 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -266,43 +266,274 @@ impl NetworkPermissions { } } +/// Partial permission overlay used for per-command requests and approved +/// session/turn grants. #[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)] -pub struct PermissionProfile { +pub struct AdditionalPermissionProfile { pub network: Option, pub file_system: Option, } -impl PermissionProfile { +impl AdditionalPermissionProfile { pub fn is_empty(&self) -> bool { self.network.is_none() && self.file_system.is_none() } +} +#[derive( + Debug, Clone, Copy, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS, +)] +#[serde(rename_all = "snake_case")] +pub enum SandboxEnforcement { + /// Codex owns sandbox construction for this profile. + #[default] + Managed, + /// No outer filesystem sandbox should be applied. + Disabled, + /// Filesystem isolation is enforced by an external caller. + External, +} + +impl SandboxEnforcement { + pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self { + match sandbox_policy { + SandboxPolicy::DangerFullAccess => Self::Disabled, + SandboxPolicy::ExternalSandbox { .. } => Self::External, + SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Self::Managed, + } + } +} + +/// Filesystem permissions for profiles where Codex owns sandbox construction. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "snake_case")] +#[ts(tag = "type")] +pub enum ManagedFileSystemPermissions { + /// Apply a managed filesystem sandbox from the listed entries. + #[serde(rename_all = "snake_case")] + #[ts(rename_all = "snake_case")] + Restricted { + entries: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + glob_scan_max_depth: Option, + }, + /// Apply a managed sandbox that allows all filesystem access. + Unrestricted, +} + +impl ManagedFileSystemPermissions { + fn from_sandbox_policy(file_system_sandbox_policy: &FileSystemSandboxPolicy) -> Self { + match file_system_sandbox_policy.kind { + FileSystemSandboxKind::Restricted => Self::Restricted { + entries: file_system_sandbox_policy.entries.clone(), + glob_scan_max_depth: file_system_sandbox_policy + .glob_scan_max_depth + .and_then(NonZeroUsize::new), + }, + FileSystemSandboxKind::Unrestricted => Self::Unrestricted, + FileSystemSandboxKind::ExternalSandbox => unreachable!( + "external filesystem policies are represented by PermissionProfile::External" + ), + } + } + + pub fn to_sandbox_policy(&self) -> FileSystemSandboxPolicy { + match self { + Self::Restricted { + entries, + glob_scan_max_depth, + } => FileSystemSandboxPolicy { + kind: FileSystemSandboxKind::Restricted, + glob_scan_max_depth: glob_scan_max_depth.map(usize::from), + entries: entries.clone(), + }, + Self::Unrestricted => FileSystemSandboxPolicy::unrestricted(), + } + } +} + +/// Canonical active runtime permissions for a conversation, turn, or command. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, JsonSchema, TS)] +#[serde(tag = "type", rename_all = "snake_case")] +#[ts(tag = "type")] +pub enum PermissionProfile { + /// Codex owns sandbox construction for this profile. + #[serde(rename_all = "snake_case")] + #[ts(rename_all = "snake_case")] + Managed { + file_system: ManagedFileSystemPermissions, + network: NetworkSandboxPolicy, + }, + /// Do not apply an outer sandbox. + Disabled, + /// Filesystem isolation is enforced by an external caller. + #[serde(rename_all = "snake_case")] + #[ts(rename_all = "snake_case")] + External { network: NetworkSandboxPolicy }, +} + +impl Default for PermissionProfile { + fn default() -> Self { + Self::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: Vec::new(), + glob_scan_max_depth: None, + }, + network: NetworkSandboxPolicy::Restricted, + } + } +} + +impl PermissionProfile { pub fn from_runtime_permissions( file_system_sandbox_policy: &FileSystemSandboxPolicy, network_sandbox_policy: NetworkSandboxPolicy, ) -> Self { - Self { - network: Some(network_sandbox_policy.into()), - file_system: Some(file_system_sandbox_policy.into()), + let enforcement = match file_system_sandbox_policy.kind { + FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { + SandboxEnforcement::Managed + } + FileSystemSandboxKind::ExternalSandbox => SandboxEnforcement::External, + }; + Self::from_runtime_permissions_with_enforcement( + enforcement, + file_system_sandbox_policy, + network_sandbox_policy, + ) + } + + pub fn from_runtime_permissions_with_enforcement( + enforcement: SandboxEnforcement, + file_system_sandbox_policy: &FileSystemSandboxPolicy, + network_sandbox_policy: NetworkSandboxPolicy, + ) -> Self { + match file_system_sandbox_policy.kind { + FileSystemSandboxKind::ExternalSandbox => Self::External { + network: network_sandbox_policy, + }, + FileSystemSandboxKind::Unrestricted + if enforcement == SandboxEnforcement::Disabled + && network_sandbox_policy.is_enabled() => + { + Self::Disabled + } + FileSystemSandboxKind::Restricted | FileSystemSandboxKind::Unrestricted => { + Self::Managed { + file_system: ManagedFileSystemPermissions::from_sandbox_policy( + file_system_sandbox_policy, + ), + network: network_sandbox_policy, + } + } } } pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self { - Self::from_runtime_permissions( + Self::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy), &FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, cwd), NetworkSandboxPolicy::from(sandbox_policy), ) } + pub fn enforcement(&self) -> SandboxEnforcement { + match self { + Self::Managed { .. } => SandboxEnforcement::Managed, + Self::Disabled => SandboxEnforcement::Disabled, + Self::External { .. } => SandboxEnforcement::External, + } + } + pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy { - self.file_system.as_ref().map_or_else( - || FileSystemSandboxPolicy::restricted(Vec::new()), - FileSystemSandboxPolicy::from, - ) + match self { + Self::Managed { file_system, .. } => file_system.to_sandbox_policy(), + Self::Disabled => FileSystemSandboxPolicy::unrestricted(), + Self::External { .. } => FileSystemSandboxPolicy::external_sandbox(), + } } pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy { - if self + match self { + Self::Managed { network, .. } | Self::External { network } => *network, + Self::Disabled => NetworkSandboxPolicy::Enabled, + } + } + + pub fn to_legacy_sandbox_policy(&self, cwd: &Path) -> io::Result { + match self { + Self::Managed { + file_system, + network, + } => file_system + .to_sandbox_policy() + .to_legacy_sandbox_policy(*network, cwd), + Self::Disabled => Ok(SandboxPolicy::DangerFullAccess), + Self::External { network } => Ok(SandboxPolicy::ExternalSandbox { + network_access: if network.is_enabled() { + crate::protocol::NetworkAccess::Enabled + } else { + crate::protocol::NetworkAccess::Restricted + }, + }), + } + } + + pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) { + ( + self.file_system_sandbox_policy(), + self.network_sandbox_policy(), + ) + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +enum TaggedPermissionProfile { + #[serde(rename_all = "snake_case")] + Managed { + file_system: ManagedFileSystemPermissions, + network: NetworkSandboxPolicy, + }, + Disabled, + #[serde(rename_all = "snake_case")] + External { + network: NetworkSandboxPolicy, + }, +} + +impl From for PermissionProfile { + fn from(value: TaggedPermissionProfile) -> Self { + match value { + TaggedPermissionProfile::Managed { + file_system, + network, + } => Self::Managed { + file_system, + network, + }, + TaggedPermissionProfile::Disabled => Self::Disabled, + TaggedPermissionProfile::External { network } => Self::External { network }, + } + } +} + +/// Pre-tagged shape written to rollout files before `PermissionProfile` +/// represented enforcement explicitly. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(deny_unknown_fields)] +struct LegacyPermissionProfile { + network: Option, + file_system: Option, +} + +impl From for PermissionProfile { + fn from(value: LegacyPermissionProfile) -> Self { + let file_system_sandbox_policy = value.file_system.as_ref().map_or_else( + || FileSystemSandboxPolicy::restricted(Vec::new()), + FileSystemSandboxPolicy::from, + ); + let network_sandbox_policy = if value .network .as_ref() .and_then(|network| network.enabled) @@ -311,19 +542,27 @@ impl PermissionProfile { NetworkSandboxPolicy::Enabled } else { NetworkSandboxPolicy::Restricted - } + }; + Self::from_runtime_permissions(&file_system_sandbox_policy, network_sandbox_policy) } +} - pub fn to_legacy_sandbox_policy(&self, cwd: &Path) -> io::Result { - self.file_system_sandbox_policy() - .to_legacy_sandbox_policy(self.network_sandbox_policy(), cwd) - } +#[derive(Debug, Clone, Deserialize)] +#[serde(untagged)] +enum PermissionProfileDe { + Tagged(TaggedPermissionProfile), + Legacy(LegacyPermissionProfile), +} - pub fn to_runtime_permissions(&self) -> (FileSystemSandboxPolicy, NetworkSandboxPolicy) { - ( - self.file_system_sandbox_policy(), - self.network_sandbox_policy(), - ) +impl<'de> Deserialize<'de> for PermissionProfile { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + Ok(match PermissionProfileDe::deserialize(deserializer)? { + PermissionProfileDe::Tagged(tagged) => tagged.into(), + PermissionProfileDe::Legacy(legacy) => legacy.into(), + }) } } @@ -977,7 +1216,7 @@ pub struct ShellToolCallParams { pub prefix_rule: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -1003,7 +1242,7 @@ pub struct ShellCommandToolCallParams { pub prefix_rule: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] - pub additional_permissions: Option, + pub additional_permissions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub justification: Option, } @@ -1448,13 +1687,13 @@ mod tests { } #[test] - fn permission_profile_is_empty_when_all_fields_are_none() { - assert_eq!(PermissionProfile::default().is_empty(), true); + fn additional_permission_profile_is_empty_when_all_fields_are_none() { + assert_eq!(AdditionalPermissionProfile::default().is_empty(), true); } #[test] - fn permission_profile_is_not_empty_when_field_is_present_but_nested_empty() { - let permission_profile = PermissionProfile { + fn additional_permission_profile_is_not_empty_when_field_is_present_but_nested_empty() { + let permission_profile = AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: None }), file_system: None, }; @@ -1483,6 +1722,146 @@ mod tests { ); } + #[test] + fn permission_profile_deserializes_legacy_rollout_shape() -> Result<()> { + let legacy = serde_json::json!({ + "network": { + "enabled": true, + }, + "file_system": { + "entries": [{ + "path": { + "type": "special", + "value": { + "kind": "root", + }, + }, + "access": "write", + }], + "glob_scan_max_depth": 2, + }, + }); + + let permission_profile: PermissionProfile = serde_json::from_value(legacy)?; + + assert_eq!( + permission_profile, + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Restricted { + entries: vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }], + glob_scan_max_depth: NonZeroUsize::new(2), + }, + network: NetworkSandboxPolicy::Enabled, + } + ); + Ok(()) + } + + #[test] + fn permission_profile_round_trip_preserves_disabled_sandbox() -> Result<()> { + let cwd = tempdir()?; + let permission_profile = PermissionProfile::from_legacy_sandbox_policy( + &SandboxPolicy::DangerFullAccess, + cwd.path(), + ); + + assert_eq!(permission_profile, PermissionProfile::Disabled); + assert_eq!( + permission_profile.to_legacy_sandbox_policy(cwd.path())?, + SandboxPolicy::DangerFullAccess + ); + assert_eq!( + permission_profile.to_runtime_permissions(), + ( + FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Enabled + ) + ); + Ok(()) + } + + #[test] + fn permission_profile_from_runtime_permissions_preserves_external_sandbox() { + let permission_profile = PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + permission_profile, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + } + ); + assert_eq!( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::Managed, + &FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted, + ), + permission_profile, + ); + } + + #[test] + fn permission_profile_from_runtime_permissions_preserves_unrestricted_managed_network() { + let permission_profile = PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::External, + &FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + ); + + assert_eq!( + permission_profile, + PermissionProfile::Managed { + file_system: ManagedFileSystemPermissions::Unrestricted, + network: NetworkSandboxPolicy::Restricted, + }, + "the legacy ExternalSandbox projection must not hide a split unrestricted filesystem policy" + ); + assert_eq!( + permission_profile.to_runtime_permissions(), + ( + FileSystemSandboxPolicy::unrestricted(), + NetworkSandboxPolicy::Restricted, + ) + ); + } + + #[test] + fn permission_profile_round_trip_preserves_external_sandbox() -> Result<()> { + let cwd = tempdir()?; + let sandbox_policy = SandboxPolicy::ExternalSandbox { + network_access: crate::protocol::NetworkAccess::Restricted, + }; + let permission_profile = + PermissionProfile::from_legacy_sandbox_policy(&sandbox_policy, cwd.path()); + + assert_eq!( + permission_profile, + PermissionProfile::External { + network: NetworkSandboxPolicy::Restricted, + } + ); + assert_eq!( + permission_profile.to_legacy_sandbox_policy(cwd.path())?, + sandbox_policy + ); + assert_eq!( + permission_profile.to_runtime_permissions(), + ( + FileSystemSandboxPolicy::external_sandbox(), + NetworkSandboxPolicy::Restricted + ) + ); + Ok(()) + } + #[test] fn file_system_permissions_with_glob_scan_depth_uses_canonical_json() -> Result<()> { let path = AbsolutePathBuf::try_from(PathBuf::from(if cfg!(windows) { diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index ed56567454..f06fc7798c 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -340,6 +340,41 @@ impl FileSystemSandboxPolicy { rebuilt } + /// Preserve explicit read-deny rules from `existing` when a caller + /// replaces the allow side of a policy. + pub fn preserve_deny_read_restrictions_from(&mut self, existing: &Self) { + let has_deny_read_entries = existing + .entries + .iter() + .any(|entry| entry.access == FileSystemAccessMode::None); + if matches!(self.kind, FileSystemSandboxKind::Unrestricted) && has_deny_read_entries { + *self = Self::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }]); + } + + if !matches!(self.kind, FileSystemSandboxKind::Restricted) { + return; + } + + if self.glob_scan_max_depth.is_none() { + self.glob_scan_max_depth = existing.glob_scan_max_depth; + } + + for deny_entry in existing + .entries + .iter() + .filter(|entry| entry.access == FileSystemAccessMode::None) + { + if !self.entries.iter().any(|entry| entry == deny_entry) { + self.entries.push(deny_entry.clone()); + } + } + } + /// Returns true when a restricted policy contains any entry that really /// reduces a broader `:root = write` grant. /// @@ -2297,6 +2332,28 @@ mod tests { ); } + #[test] + fn preserving_deny_entries_keeps_unrestricted_policy_enforceable() { + let deny_entry = unreadable_glob_entry("/tmp/project/**/*.env".to_string()); + let mut existing = FileSystemSandboxPolicy::restricted(vec![deny_entry.clone()]); + existing.glob_scan_max_depth = Some(2); + let mut replacement = FileSystemSandboxPolicy::unrestricted(); + + replacement.preserve_deny_read_restrictions_from(&existing); + + let mut expected = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Write, + }, + deny_entry, + ]); + expected.glob_scan_max_depth = Some(2); + assert_eq!(replacement, expected); + } + fn deny_policy(path: &Path) -> FileSystemSandboxPolicy { FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { path: FileSystemPath::Path { diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 6e0a62fc41..21562f3a9a 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -41,6 +41,7 @@ use crate::models::MessagePhase; use crate::models::PermissionProfile; use crate::models::ResponseInputItem; use crate::models::ResponseItem; +use crate::models::SandboxEnforcement; use crate::models::WebSearchAction; use crate::num_format::format_with_separators; use crate::openai_models::ReasoningEffort as ReasoningEffortConfig; @@ -3062,7 +3063,8 @@ impl TurnContextItem { &self.cwd, ) }); - PermissionProfile::from_runtime_permissions( + PermissionProfile::from_runtime_permissions_with_enforcement( + SandboxEnforcement::from_legacy_sandbox_policy(&self.sandbox_policy), &file_system_sandbox_policy, NetworkSandboxPolicy::from(&self.sandbox_policy), ) diff --git a/codex-rs/protocol/src/request_permissions.rs b/codex-rs/protocol/src/request_permissions.rs index 0649bf2886..6c7b699daf 100644 --- a/codex-rs/protocol/src/request_permissions.rs +++ b/codex-rs/protocol/src/request_permissions.rs @@ -1,6 +1,6 @@ +use crate::models::AdditionalPermissionProfile; use crate::models::FileSystemPermissions; use crate::models::NetworkPermissions; -use crate::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use schemars::JsonSchema; use serde::Deserialize; @@ -28,7 +28,7 @@ impl RequestPermissionProfile { } } -impl From for PermissionProfile { +impl From for AdditionalPermissionProfile { fn from(value: RequestPermissionProfile) -> Self { Self { network: value.network, @@ -37,8 +37,8 @@ impl From for PermissionProfile { } } -impl From for RequestPermissionProfile { - fn from(value: PermissionProfile) -> Self { +impl From for RequestPermissionProfile { + fn from(value: AdditionalPermissionProfile) -> Self { Self { network: value.network, file_system: value.file_system, diff --git a/codex-rs/rollout-trace/src/tool_dispatch.rs b/codex-rs/rollout-trace/src/tool_dispatch.rs index 0389814cba..1c444a87de 100644 --- a/codex-rs/rollout-trace/src/tool_dispatch.rs +++ b/codex-rs/rollout-trace/src/tool_dispatch.rs @@ -7,7 +7,7 @@ use std::fmt::Display; use std::sync::Arc; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::SandboxPermissions; use codex_protocol::models::SearchToolCallParams; @@ -90,7 +90,7 @@ pub enum ToolDispatchPayload { timeout_ms: Option, sandbox_permissions: Option, prefix_rule: Option>, - additional_permissions: Option, + additional_permissions: Option, justification: Option, }, Mcp { diff --git a/codex-rs/sandboxing/src/manager.rs b/codex-rs/sandboxing/src/manager.rs index 0836eb2565..a13f828fdf 100644 --- a/codex-rs/sandboxing/src/manager.rs +++ b/codex-rs/sandboxing/src/manager.rs @@ -11,7 +11,7 @@ use crate::policy_transforms::effective_network_sandbox_policy; use crate::policy_transforms::should_require_platform_sandbox; use codex_network_proxy::NetworkProxy; use codex_protocol::config_types::WindowsSandboxLevel; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::SandboxPolicy; @@ -68,7 +68,7 @@ pub struct SandboxCommand { pub args: Vec, pub cwd: AbsolutePathBuf, pub env: HashMap, - pub additional_permissions: Option, + pub additional_permissions: Option, } #[derive(Debug)] diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index e869736294..a7dca2bf57 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -5,9 +5,9 @@ use super::SandboxType; use super::SandboxablePreference; use super::get_platform_sandbox; use codex_protocol::config_types::WindowsSandboxLevel; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 9b5e444d7c..065d96e8bb 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -1,6 +1,6 @@ +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -27,7 +27,7 @@ pub struct EffectiveSandboxPermissions { impl EffectiveSandboxPermissions { pub fn new( sandbox_policy: &SandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> Self { let Some(additional_permissions) = additional_permissions else { return Self { @@ -42,8 +42,8 @@ impl EffectiveSandboxPermissions { } pub fn normalize_additional_permissions( - additional_permissions: PermissionProfile, -) -> Result { + additional_permissions: AdditionalPermissionProfile, +) -> Result { let network = additional_permissions .network .filter(|network| !network.is_empty()); @@ -87,16 +87,16 @@ pub fn normalize_additional_permissions( } None => None, }; - Ok(PermissionProfile { + Ok(AdditionalPermissionProfile { network, file_system, }) } pub fn merge_permission_profiles( - base: Option<&PermissionProfile>, - permissions: Option<&PermissionProfile>, -) -> Option { + base: Option<&AdditionalPermissionProfile>, + permissions: Option<&AdditionalPermissionProfile>, +) -> Option { let Some(permissions) = permissions else { return base.cloned(); }; @@ -137,7 +137,7 @@ pub fn merge_permission_profiles( (None, None) => None, }; - Some(PermissionProfile { + Some(AdditionalPermissionProfile { network, file_system, }) @@ -148,10 +148,10 @@ pub fn merge_permission_profiles( } pub fn intersect_permission_profiles( - requested: PermissionProfile, - granted: PermissionProfile, + requested: AdditionalPermissionProfile, + granted: AdditionalPermissionProfile, cwd: &Path, -) -> PermissionProfile { +) -> AdditionalPermissionProfile { let file_system = requested .file_system .map(|requested_file_system| { @@ -213,7 +213,7 @@ pub fn intersect_permission_profiles( _ => None, }; - PermissionProfile { + AdditionalPermissionProfile { network, file_system, } @@ -458,7 +458,7 @@ fn dedup_absolute_paths(paths: Vec) -> Vec { } fn additional_permission_roots( - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> (Vec, Vec) { ( dedup_absolute_paths( @@ -516,7 +516,7 @@ fn merge_file_system_policy_with_additional_permissions( pub fn effective_file_system_sandbox_policy( file_system_policy: &FileSystemSandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> FileSystemSandboxPolicy { let Some(additional_permissions) = additional_permissions else { return file_system_policy.clone(); @@ -557,7 +557,7 @@ fn merge_read_only_access_with_additional_reads( fn merge_network_access( base_network_access: bool, - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> bool { base_network_access || additional_permissions @@ -569,7 +569,7 @@ fn merge_network_access( pub fn effective_network_sandbox_policy( network_policy: NetworkSandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> NetworkSandboxPolicy { if additional_permissions .is_some_and(|permissions| merge_network_access(network_policy.is_enabled(), permissions)) @@ -584,7 +584,7 @@ pub fn effective_network_sandbox_policy( fn sandbox_policy_with_additional_permissions( sandbox_policy: &SandboxPolicy, - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> SandboxPolicy { if additional_permissions.is_empty() { return sandbox_policy.clone(); @@ -654,7 +654,7 @@ fn sandbox_policy_with_additional_permissions( fn effective_sandbox_policy( sandbox_policy: &SandboxPolicy, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> SandboxPolicy { additional_permissions.map_or_else( || sandbox_policy.clone(), diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 38b4bd5f02..876cbe9cb2 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -4,9 +4,9 @@ use super::merge_file_system_policy_with_additional_permissions; use super::normalize_additional_permissions; use super::sandbox_policy_with_additional_permissions; use super::should_require_platform_sandbox; +use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; -use codex_protocol::models::PermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; diff --git a/codex-rs/shell-escalation/src/unix/escalate_server.rs b/codex-rs/shell-escalation/src/unix/escalate_server.rs index ce73f262f4..55abf7d734 100644 --- a/codex-rs/shell-escalation/src/unix/escalate_server.rs +++ b/codex-rs/shell-escalation/src/unix/escalate_server.rs @@ -378,8 +378,8 @@ async fn handle_escalate_session_with_policy( mod tests { use super::*; use codex_protocol::approvals::EscalationPermissions; + use codex_protocol::models::AdditionalPermissionProfile as PermissionProfile; use codex_protocol::models::NetworkPermissions; - use codex_protocol::models::PermissionProfile; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 8a90924412..62e074d6cf 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -289,7 +289,7 @@ fn default_exec_approval_decisions( proposed_network_policy_amendments: Option< &[codex_protocol::approvals::NetworkPolicyAmendment], >, - additional_permissions: Option<&codex_protocol::models::PermissionProfile>, + additional_permissions: Option<&codex_protocol::models::AdditionalPermissionProfile>, ) -> Vec { ExecApprovalRequestEvent::default_available_decisions( network_approval_context, diff --git a/codex-rs/tui/src/app/tests.rs b/codex-rs/tui/src/app/tests.rs index b2141977ce..320e0e1c87 100644 --- a/codex-rs/tui/src/app/tests.rs +++ b/codex-rs/tui/src/app/tests.rs @@ -61,6 +61,7 @@ use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::CollaborationModeMask; use codex_protocol::config_types::ModeKind; use codex_protocol::config_types::Settings; +use codex_protocol::models::AdditionalPermissionProfile as CoreAdditionalPermissionProfile; use codex_protocol::models::FileSystemPermissions; use codex_protocol::models::NetworkPermissions; use codex_protocol::models::PermissionProfile; @@ -2476,7 +2477,7 @@ async fn inactive_thread_exec_approval_preserves_context() { ); assert_eq!( additional_permissions, - Some(PermissionProfile { + Some(CoreAdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/tui/src/app/thread_session_state.rs b/codex-rs/tui/src/app/thread_session_state.rs index 302bcb5a63..269a050372 100644 --- a/codex-rs/tui/src/app/thread_session_state.rs +++ b/codex-rs/tui/src/app/thread_session_state.rs @@ -3,7 +3,6 @@ use crate::app_server_session::ThreadSessionState; use crate::read_session_model; use codex_app_server_protocol::Thread; use codex_protocol::ThreadId; -use codex_protocol::protocol::SandboxPolicy; impl App { pub(super) async fn sync_active_thread_permission_settings_to_cached_session(&mut self) { @@ -14,17 +13,12 @@ impl App { let approval_policy = self.config.permissions.approval_policy.value(); let approvals_reviewer = self.config.approvals_reviewer; let sandbox_policy = self.config.permissions.sandbox_policy.get().clone(); - let permission_profile = if matches!(sandbox_policy, SandboxPolicy::ExternalSandbox { .. }) - { - None - } else { - Some( - self.chat_widget - .config_ref() - .permissions - .permission_profile(), - ) - }; + let permission_profile = Some( + self.chat_widget + .config_ref() + .permissions + .permission_profile(), + ); let update_session = |session: &mut ThreadSessionState| { session.approval_policy = approval_policy; session.approvals_reviewer = approvals_reviewer; diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index 262b540c62..5c768c9e9a 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -154,7 +154,7 @@ pub(crate) struct ThreadSessionState { pub(crate) sandbox_policy: SandboxPolicy, /// Canonical active permissions when available. Consumers should prefer /// this over `sandbox_policy`; `None` means the session only has a legacy - /// sandbox projection or represents an external sandbox. + /// sandbox projection. pub(crate) permission_profile: Option, pub(crate) cwd: AbsolutePathBuf, pub(crate) instruction_source_paths: Vec, @@ -1053,15 +1053,12 @@ fn turn_start_permission_overrides( Option, Option, ) { - let is_external_sandbox = matches!(&sandbox_policy, SandboxPolicy::ExternalSandbox { .. }); - match (mode, is_external_sandbox, permission_profile) { - (ThreadParamsMode::Embedded, false, Some(permission_profile)) => { + match (mode, permission_profile) { + (ThreadParamsMode::Embedded, Some(permission_profile)) => { (None, Some(permission_profile.into())) } - (ThreadParamsMode::Embedded, false, None) => (None, None), - (ThreadParamsMode::Embedded, true, _) | (ThreadParamsMode::Remote, _, _) => { - (Some(sandbox_policy.into()), None) - } + (ThreadParamsMode::Embedded, None) => (None, None), + (ThreadParamsMode::Remote, _) => (Some(sandbox_policy.into()), None), } } @@ -1073,14 +1070,7 @@ fn permission_profile_override_from_config( return None; } - if matches!( - config.permissions.sandbox_policy.get(), - SandboxPolicy::ExternalSandbox { .. } - ) { - None - } else { - Some(config.permissions.permission_profile().into()) - } + Some(config.permissions.permission_profile().into()) } fn thread_start_params_from_config( @@ -1594,8 +1584,11 @@ mod tests { &cwd, )), ); - assert_eq!(sandbox, Some(external_sandbox.into())); - assert_eq!(profile, None); + assert_eq!(sandbox, None); + assert_eq!( + profile, + Some(PermissionProfile::from_legacy_sandbox_policy(&external_sandbox, &cwd).into()) + ); } #[tokio::test] diff --git a/codex-rs/tui/src/bottom_pane/approval_overlay.rs b/codex-rs/tui/src/bottom_pane/approval_overlay.rs index 1f7e30c371..57d819e560 100644 --- a/codex-rs/tui/src/bottom_pane/approval_overlay.rs +++ b/codex-rs/tui/src/bottom_pane/approval_overlay.rs @@ -20,7 +20,7 @@ use crate::render::renderable::Renderable; use codex_features::Features; use codex_protocol::ThreadId; use codex_protocol::mcp::RequestId; -use codex_protocol::models::PermissionProfile; +use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::permissions::FileSystemAccessMode; use codex_protocol::permissions::FileSystemPath; use codex_protocol::permissions::FileSystemSandboxEntry; @@ -58,7 +58,7 @@ pub(crate) enum ApprovalRequest { reason: Option, available_decisions: Vec, network_approval_context: Option, - additional_permissions: Option, + additional_permissions: Option, }, Permissions { thread_id: ThreadId, @@ -719,7 +719,7 @@ impl ApprovalOption { fn exec_options( available_decisions: &[ReviewDecision], network_approval_context: Option<&NetworkApprovalContext>, - additional_permissions: Option<&PermissionProfile>, + additional_permissions: Option<&AdditionalPermissionProfile>, ) -> Vec { available_decisions .iter() @@ -808,7 +808,7 @@ fn exec_options( } pub(crate) fn format_additional_permissions_rule( - additional_permissions: &PermissionProfile, + additional_permissions: &AdditionalPermissionProfile, ) -> Option { let mut parts = Vec::new(); if additional_permissions @@ -1341,7 +1341,7 @@ mod tests { #[test] fn additional_permissions_exec_options_hide_execpolicy_amendment() { - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions::from_read_write_roots( Some(vec![absolute_path("/tmp/readme.txt")]), Some(vec![absolute_path("/tmp/out.txt")]), @@ -1383,7 +1383,7 @@ mod tests { #[test] fn additional_permissions_rule_shows_non_path_file_system_entries() { - let additional_permissions = PermissionProfile { + let additional_permissions = AdditionalPermissionProfile { file_system: Some(FileSystemPermissions { entries: vec![ FileSystemSandboxEntry { @@ -1477,7 +1477,7 @@ mod tests { reason: None, available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - additional_permissions: Some(PermissionProfile { + additional_permissions: Some(AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), @@ -1527,7 +1527,7 @@ mod tests { reason: Some("need filesystem access".into()), available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort], network_approval_context: None, - additional_permissions: Some(PermissionProfile { + additional_permissions: Some(AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs index 320dc8a9c5..2e5c51307c 100644 --- a/codex-rs/tui/src/chatwidget/tests/approval_requests.rs +++ b/codex-rs/tui/src/chatwidget/tests/approval_requests.rs @@ -133,7 +133,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() { ); assert_eq!( request.additional_permissions, - Some(PermissionProfile { + Some(codex_protocol::models::AdditionalPermissionProfile { network: Some(NetworkPermissions { enabled: Some(true), }), diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index a23f137b40..c095d372d0 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -92,11 +92,9 @@ async fn submission_includes_configured_permission_profile() { let conversation_id = ThreadId::new(); let rollout_file = NamedTempFile::new().unwrap(); - let expected_permission_profile = PermissionProfile { - network: Some(NetworkPermissions { - enabled: Some(false), - }), - file_system: Some(FileSystemPermissions { + let expected_permission_profile = PermissionProfile::Managed { + network: codex_protocol::permissions::NetworkSandboxPolicy::Restricted, + file_system: codex_protocol::models::ManagedFileSystemPermissions::Restricted { entries: vec![ codex_protocol::permissions::FileSystemSandboxEntry { path: codex_protocol::permissions::FileSystemPath::Special { @@ -112,7 +110,7 @@ async fn submission_includes_configured_permission_profile() { }, ], glob_scan_max_depth: None, - }), + }, }; let configured = codex_protocol::protocol::SessionConfiguredEvent { session_id: conversation_id,