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 | 23 +- .../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, 2091 insertions(+), 890 deletions(-) diff --git a/codex-rs/analytics/src/events.rs b/codex-rs/analytics/src/events.rs index 73f2886f2..98d0e6ff6 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 db2065f75..d7631e157 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 78e75d7c4..76d265c59 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 84bf52473..50510adf9 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 ccce74bc0..cdc9da679 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 8560fb6c8..04c91b5a2 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 4def45c04..6ba2fc0db 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 a603aff1e..d120fc8b5 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 47677da41..281650bb3 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 19ccad14c..40ff83aeb 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 38b0eb0d3..573cbe92d 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 ae599d3e6..5a59e280e 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 879fa5c68..1de06c603 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 062771029..245c57886 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 65836c119..5120ec313 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 c38bde54b..7642c2765 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 204a42764..29aeceb43 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 9aa130412..0b25a769a 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 5dc6b82a3..b69f1da01 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 d76ad5a58..5ceec7f3f 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 5a83011ab..61d268afe 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 9fa6d98e7..40855a095 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 a30306b34..5936b3e14 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 a28021bb9..a03849344 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 58a8c2fc8..a4c424664 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 4d7f3c9a5..e81ee547f 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 c24d2e80d..83718a8dc 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 4a9b2c6a1..04e6ede0c 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 d46d1a316..9f7778c3c 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 cb6f78839..13d4a5d0a 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 2afc5e080..fba227834 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 0fffe1a29..ca865300a 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 f7a36c128..af8dec1f8 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 a36b3a421..508eadfba 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 6423bee28..f527182fd 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 0898bd89a..8c831e6b6 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 e9fd67633..3bd4b8a26 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 5e1526ad5..48b7a26cc 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 91d5543c0..9d63ad0a8 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 af4178f71..7878c1092 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 f4ab61b52..17daaa738 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 05b54ff88..6a3203a05 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 e0a0af5ed..1bdd0b82f 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 ed325a4e6..ecf3ccdc0 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, - attempt.network_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 112a3d3a5..0bc4d2e6f 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 2246fa18f..a55f78f8a 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 e18d2c269..edaa2f472 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 b29e9c593..369689f9d 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 a2b6ca145..86753a04d 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 96d7abf64..be185e5fe 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 a5a6e69f8..acf361f94 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 bcc938734..319e3ef8e 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 a474b3536..37237f60d 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 9836a5b5b..b9f7456f3 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 0a7c000ae..c42159a6d 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 2b9aea272..6fc5e49b4 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 551723c77..2b02ee88b 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 ed5656745..f06fc7798 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 6e0a62fc4..21562f3a9 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 0649bf288..6c7b699da 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 0389814cb..1c444a87d 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 0836eb256..a13f828fd 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 e86973629..a7dca2bf5 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 9b5e444d7..065d96e8b 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 38b4bd5f0..876cbe9cb 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 ce73f262f..55abf7d73 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 8a9092441..62e074d6c 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 b2141977c..320e0e1c8 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 302bcb5a6..269a05037 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 262b540c6..5c768c9e9 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 1f7e30c37..57d819e56 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 320dc8a9c..2e5c51307 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 a23f137b4..c095d372d 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,