From 789f387982c51e8032766f91d4b026f4c50b0ff8 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 24 Apr 2026 17:16:58 -0700 Subject: [PATCH] permissions: remove legacy read-only access modes (#19449) ## Why `ReadOnlyAccess` was a transitional legacy shape on `SandboxPolicy`: `FullAccess` meant the historical read-only/workspace-write modes could read the full filesystem, while `Restricted` tried to carry partial readable roots. The partial-read model now belongs in `FileSystemSandboxPolicy` and `PermissionProfile`, so keeping it on `SandboxPolicy` makes every legacy projection reintroduce lossy read-root bookkeeping and creates unnecessary noise in the rest of the permissions migration. This PR makes the legacy policy model narrower and explicit: `SandboxPolicy::ReadOnly` and `SandboxPolicy::WorkspaceWrite` represent the old full-read sandbox modes only. Split readable roots, deny-read globs, and platform-default/minimal read behavior stay in the runtime permissions model. ## What changed - Removes `ReadOnlyAccess` from `codex_protocol::protocol::SandboxPolicy`, including the generated `access` and `readOnlyAccess` API fields. - Updates legacy policy/profile conversions so restricted filesystem reads are represented only by `FileSystemSandboxPolicy` / `PermissionProfile` entries. - Keeps app-server v2 compatible with legacy `fullAccess` read-access payloads by accepting and ignoring that no-op shape, while rejecting legacy `restricted` read-access payloads instead of silently widening them to full-read legacy policies. - Carries Windows sandbox platform-default read behavior with an explicit override flag instead of depending on `ReadOnlyAccess::Restricted`. - Refreshes generated app-server schema/types and updates tests/docs for the simplified legacy policy shape. ## Verification - `cargo check -p codex-app-server-protocol --tests` - `cargo check -p codex-windows-sandbox --tests` - `cargo test -p codex-app-server-protocol sandbox_policy_` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19449). * #19395 * #19394 * #19393 * #19392 * #19391 * __->__ #19449 --- .../schema/json/ClientRequest.json | 67 ----- .../codex_app_server_protocol.schemas.json | 67 ----- .../codex_app_server_protocol.v2.schemas.json | 67 ----- .../schema/json/v2/CommandExecParams.json | 67 ----- .../schema/json/v2/ThreadForkResponse.json | 67 ----- .../schema/json/v2/ThreadResumeResponse.json | 67 ----- .../schema/json/v2/ThreadStartResponse.json | 67 ----- .../schema/json/v2/TurnStartParams.json | 67 ----- .../schema/typescript/v2/ReadOnlyAccess.ts | 6 - .../schema/typescript/v2/SandboxPolicy.ts | 3 +- .../schema/typescript/v2/index.ts | 1 - .../app-server-protocol/src/protocol/v2.rs | 263 ++++++++++-------- codex-rs/app-server-test-client/src/lib.rs | 4 - codex-rs/app-server/src/command_exec.rs | 3 - .../app-server/tests/suite/v2/turn_start.rs | 1 - .../tests/suite/v2/turn_start_zsh_fork.rs | 1 - codex-rs/config/src/config_requirements.rs | 2 - codex-rs/config/src/config_toml.rs | 2 - codex-rs/core/README.md | 17 +- codex-rs/core/src/config/config_tests.rs | 15 - codex-rs/core/src/config_loader/tests.rs | 1 - .../context/permissions_instructions_tests.rs | 1 - codex-rs/core/src/exec.rs | 27 +- codex-rs/core/src/exec_tests.rs | 25 +- codex-rs/core/src/memories/phase2.rs | 1 - codex-rs/core/src/safety_tests.rs | 5 - codex-rs/core/src/session/tests.rs | 13 - .../src/tools/handlers/apply_patch_tests.rs | 2 - .../runtimes/shell/unix_escalation_tests.rs | 2 - codex-rs/core/tests/common/zsh_fork.rs | 1 - codex-rs/core/tests/suite/apply_patch_cli.rs | 2 - codex-rs/core/tests/suite/approvals.rs | 4 - codex-rs/core/tests/suite/hooks.rs | 2 - .../core/tests/suite/permissions_messages.rs | 1 - codex-rs/core/tests/suite/prompt_caching.rs | 2 - codex-rs/core/tests/suite/remote_env.rs | 49 ++-- .../core/tests/suite/request_permissions.rs | 1 - .../tests/suite/request_permissions_tool.rs | 1 - .../suite/responses_api_proxy_headers.rs | 1 - codex-rs/exec-server/src/fs_sandbox.rs | 10 - .../src/event_processor_with_human_output.rs | 1 - codex-rs/exec/src/lib_tests.rs | 1 - codex-rs/exec/tests/suite/sandbox.rs | 3 - codex-rs/linux-sandbox/src/bwrap.rs | 52 ++-- .../linux-sandbox/src/linux_run_main_tests.rs | 3 - .../linux-sandbox/tests/suite/landlock.rs | 5 - codex-rs/protocol/src/permissions.rs | 192 +++---------- codex-rs/protocol/src/protocol.rs | 247 +--------------- codex-rs/sandboxing/src/manager_tests.rs | 2 - codex-rs/sandboxing/src/policy_transforms.rs | 38 +-- .../sandboxing/src/policy_transforms_tests.rs | 11 +- ...estricted_read_only_platform_defaults.sbpl | 2 +- codex-rs/sandboxing/src/seatbelt_tests.rs | 46 --- codex-rs/tui/src/chatwidget/tests.rs | 1 - .../tui/src/chatwidget/tests/permissions.rs | 2 - codex-rs/tui/src/status/tests.rs | 2 - .../sandbox-summary/src/sandbox_summary.rs | 3 - codex-rs/windows-sandbox-rs/src/allow.rs | 6 - .../windows-sandbox-rs/src/elevated_impl.rs | 4 +- codex-rs/windows-sandbox-rs/src/identity.rs | 3 + codex-rs/windows-sandbox-rs/src/lib.rs | 1 - .../src/setup_orchestrator.rs | 158 +---------- codex-rs/windows-sandbox-rs/src/spawn_prep.rs | 2 +- 63 files changed, 284 insertions(+), 1506 deletions(-) delete mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ReadOnlyAccess.ts diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index d7631e157..f895d3fe7 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -2126,53 +2126,6 @@ ], "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "RealtimeOutputModality": { "enum": [ "text", @@ -3056,16 +3009,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -3122,16 +3065,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 cdc9da679..0c76232d9 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 @@ -12228,53 +12228,6 @@ "title": "RawResponseItemCompletedNotification", "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/v2/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "RealtimeConversationVersion": { "enum": [ "v1", @@ -13483,16 +13436,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/v2/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -13549,16 +13492,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/v2/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 04c91b5a2..55f33badd 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 @@ -8942,53 +8942,6 @@ "title": "RawResponseItemCompletedNotification", "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "RealtimeConversationVersion": { "enum": [ "v1", @@ -10197,16 +10150,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -10263,16 +10206,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 6ba2fc0db..b85a0e791 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -374,53 +374,6 @@ ], "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "SandboxPolicy": { "oneOf": [ { @@ -441,16 +394,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -507,16 +450,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 281650bb3..a2f2490a0 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1028,53 +1028,6 @@ ], "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -1107,16 +1060,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -1173,16 +1116,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 573cbe92d..516627576 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1028,53 +1028,6 @@ ], "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -1107,16 +1060,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -1173,16 +1116,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 1de06c603..f773c0be6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1028,53 +1028,6 @@ ], "type": "object" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -1107,16 +1060,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -1173,16 +1116,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" 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 245c57886..559698100 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -462,53 +462,6 @@ ], "type": "string" }, - "ReadOnlyAccess": { - "oneOf": [ - { - "properties": { - "includePlatformDefaults": { - "default": true, - "type": "boolean" - }, - "readableRoots": { - "default": [], - "items": { - "$ref": "#/definitions/AbsolutePathBuf" - }, - "type": "array" - }, - "type": { - "enum": [ - "restricted" - ], - "title": "RestrictedReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "RestrictedReadOnlyAccess", - "type": "object" - }, - { - "properties": { - "type": { - "enum": [ - "fullAccess" - ], - "title": "FullAccessReadOnlyAccessType", - "type": "string" - } - }, - "required": [ - "type" - ], - "title": "FullAccessReadOnlyAccess", - "type": "object" - } - ] - }, "ReasoningEffort": { "description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning", "enum": [ @@ -561,16 +514,6 @@ }, { "properties": { - "access": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "networkAccess": { "default": false, "type": "boolean" @@ -627,16 +570,6 @@ "default": false, "type": "boolean" }, - "readOnlyAccess": { - "allOf": [ - { - "$ref": "#/definitions/ReadOnlyAccess" - } - ], - "default": { - "type": "fullAccess" - } - }, "type": { "enum": [ "workspaceWrite" diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ReadOnlyAccess.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ReadOnlyAccess.ts deleted file mode 100644 index 78fa04ff3..000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ReadOnlyAccess.ts +++ /dev/null @@ -1,6 +0,0 @@ -// GENERATED CODE! DO NOT MODIFY BY HAND! - -// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { AbsolutePathBuf } from "../AbsolutePathBuf"; - -export type ReadOnlyAccess = { "type": "restricted", includePlatformDefaults: boolean, readableRoots: Array, } | { "type": "fullAccess" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts index c6780648c..5575701ff 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts @@ -3,6 +3,5 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AbsolutePathBuf } from "../AbsolutePathBuf"; import type { NetworkAccess } from "./NetworkAccess"; -import type { ReadOnlyAccess } from "./ReadOnlyAccess"; -export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", access: ReadOnlyAccess, networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, readOnlyAccess: ReadOnlyAccess, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; +export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", networkAccess: boolean, } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index be747508a..59c4fa673 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -276,7 +276,6 @@ export type { RateLimitReachedType } from "./RateLimitReachedType"; export type { RateLimitSnapshot } from "./RateLimitSnapshot"; export type { RateLimitWindow } from "./RateLimitWindow"; export type { RawResponseItemCompletedNotification } from "./RawResponseItemCompletedNotification"; -export type { ReadOnlyAccess } from "./ReadOnlyAccess"; export type { ReasoningEffortOption } from "./ReasoningEffortOption"; export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification"; export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 5936b3e14..505102e12 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -83,7 +83,6 @@ use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatus; use codex_protocol::protocol::RateLimitReachedType as CoreRateLimitReachedType; use codex_protocol::protocol::RateLimitSnapshot as CoreRateLimitSnapshot; use codex_protocol::protocol::RateLimitWindow as CoreRateLimitWindow; -use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; use codex_protocol::protocol::RealtimeAudioFrame as CoreRealtimeAudioFrame; use codex_protocol::protocol::RealtimeConversationVersion; use codex_protocol::protocol::RealtimeOutputModality; @@ -808,10 +807,6 @@ const fn default_enabled() -> bool { true } -const fn default_include_platform_defaults() -> bool { - true -} - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS, ExperimentalApi)] #[serde(rename_all = "snake_case")] #[ts(export_to = "v2/")] @@ -1719,54 +1714,7 @@ pub enum NetworkAccess { Enabled, } -#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type")] -#[ts(export_to = "v2/")] -pub enum ReadOnlyAccess { - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - Restricted { - #[serde(default = "default_include_platform_defaults")] - include_platform_defaults: bool, - #[serde(default)] - readable_roots: Vec, - }, - #[default] - FullAccess, -} - -impl ReadOnlyAccess { - pub fn to_core(&self) -> CoreReadOnlyAccess { - match self { - ReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots, - } => CoreReadOnlyAccess::Restricted { - include_platform_defaults: *include_platform_defaults, - readable_roots: readable_roots.clone(), - }, - ReadOnlyAccess::FullAccess => CoreReadOnlyAccess::FullAccess, - } - } -} - -impl From for ReadOnlyAccess { - fn from(value: CoreReadOnlyAccess) -> Self { - match value { - CoreReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots, - } => ReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots, - }, - CoreReadOnlyAccess::FullAccess => ReadOnlyAccess::FullAccess, - } - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[derive(Serialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] #[ts(export_to = "v2/")] @@ -1775,8 +1723,6 @@ pub enum SandboxPolicy { #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] ReadOnly { - #[serde(default)] - access: ReadOnlyAccess, #[serde(default)] network_access: bool, }, @@ -1792,7 +1738,36 @@ pub enum SandboxPolicy { #[serde(default)] writable_roots: Vec, #[serde(default)] - read_only_access: ReadOnlyAccess, + network_access: bool, + #[serde(default)] + exclude_tmpdir_env_var: bool, + #[serde(default)] + exclude_slash_tmp: bool, + }, +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum SandboxPolicyDeserialize { + DangerFullAccess, + #[serde(rename_all = "camelCase")] + ReadOnly { + #[serde(default)] + network_access: bool, + #[serde(default)] + access: Option, + }, + #[serde(rename_all = "camelCase")] + ExternalSandbox { + #[serde(default)] + network_access: NetworkAccess, + }, + #[serde(rename_all = "camelCase")] + WorkspaceWrite { + #[serde(default)] + writable_roots: Vec, + #[serde(default)] + read_only_access: Option, #[serde(default)] network_access: bool, #[serde(default)] @@ -1802,19 +1777,68 @@ pub enum SandboxPolicy { }, } +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +enum LegacyReadOnlyAccess { + FullAccess, + Restricted, +} + +impl<'de> Deserialize<'de> for SandboxPolicy { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + match SandboxPolicyDeserialize::deserialize(deserializer)? { + SandboxPolicyDeserialize::DangerFullAccess => Ok(SandboxPolicy::DangerFullAccess), + SandboxPolicyDeserialize::ReadOnly { + network_access, + access, + } => { + if matches!(access, Some(LegacyReadOnlyAccess::Restricted)) { + return Err(serde::de::Error::custom( + "readOnly.access is no longer supported; use permissionProfile for restricted reads", + )); + } + Ok(SandboxPolicy::ReadOnly { network_access }) + } + SandboxPolicyDeserialize::ExternalSandbox { network_access } => { + Ok(SandboxPolicy::ExternalSandbox { network_access }) + } + SandboxPolicyDeserialize::WorkspaceWrite { + writable_roots, + read_only_access, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + } => { + if matches!(read_only_access, Some(LegacyReadOnlyAccess::Restricted)) { + return Err(serde::de::Error::custom( + "workspaceWrite.readOnlyAccess is no longer supported; use permissionProfile for restricted reads", + )); + } + Ok(SandboxPolicy::WorkspaceWrite { + writable_roots, + network_access, + exclude_tmpdir_env_var, + exclude_slash_tmp, + }) + } + } + } +} + impl SandboxPolicy { pub fn to_core(&self) -> codex_protocol::protocol::SandboxPolicy { match self { SandboxPolicy::DangerFullAccess => { codex_protocol::protocol::SandboxPolicy::DangerFullAccess } - SandboxPolicy::ReadOnly { - access, - network_access, - } => codex_protocol::protocol::SandboxPolicy::ReadOnly { - access: access.to_core(), - network_access: *network_access, - }, + SandboxPolicy::ReadOnly { network_access } => { + codex_protocol::protocol::SandboxPolicy::ReadOnly { + network_access: *network_access, + } + } SandboxPolicy::ExternalSandbox { network_access } => { codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access: match network_access { @@ -1825,13 +1849,11 @@ impl SandboxPolicy { } SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), - read_only_access: read_only_access.to_core(), network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, @@ -1846,13 +1868,9 @@ impl From for SandboxPolicy { codex_protocol::protocol::SandboxPolicy::DangerFullAccess => { SandboxPolicy::DangerFullAccess } - codex_protocol::protocol::SandboxPolicy::ReadOnly { - access, - network_access, - } => SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::from(access), - network_access, - }, + codex_protocol::protocol::SandboxPolicy::ReadOnly { network_access } => { + SandboxPolicy::ReadOnly { network_access } + } codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => { SandboxPolicy::ExternalSandbox { network_access: match network_access { @@ -1863,13 +1881,11 @@ impl From for SandboxPolicy { } codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, } => SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access: ReadOnlyAccess::from(read_only_access), network_access, exclude_tmpdir_env_var, exclude_slash_tmp, @@ -7578,7 +7594,6 @@ mod tests { use codex_protocol::items::WebSearchItem; use codex_protocol::models::WebSearchAction as CoreWebSearchAction; use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; - use codex_protocol::protocol::ReadOnlyAccess as CoreReadOnlyAccess; use codex_protocol::user_input::UserInput as CoreUserInput; use codex_utils_absolute_path::test_support::PathBufExt; use codex_utils_absolute_path::test_support::test_path_buf; @@ -8784,13 +8799,8 @@ mod tests { } #[test] - fn sandbox_policy_round_trips_read_only_access() { - let readable_root = test_absolute_path(); + fn sandbox_policy_round_trips_read_only_network_access() { let v2_policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![readable_root.clone()], - }, network_access: true, }; @@ -8798,10 +8808,6 @@ mod tests { assert_eq!( core_policy, codex_protocol::protocol::SandboxPolicy::ReadOnly { - access: CoreReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![readable_root], - }, network_access: true, } ); @@ -9425,14 +9431,9 @@ mod tests { } #[test] - fn sandbox_policy_round_trips_workspace_write_read_only_access() { - let readable_root = test_absolute_path(); + fn sandbox_policy_round_trips_workspace_write_access() { let v2_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![readable_root.clone()], - }, network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -9443,10 +9444,6 @@ mod tests { core_policy, codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: CoreReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![readable_root], - }, network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -9458,40 +9455,78 @@ mod tests { } #[test] - fn sandbox_policy_deserializes_legacy_read_only_without_access_field() { - let policy: SandboxPolicy = serde_json::from_value(json!({ - "type": "readOnly" + fn sandbox_policy_deserializes_legacy_read_only_full_access_field() { + let policy = serde_json::from_value::(json!({ + "type": "readOnly", + "access": { + "type": "fullAccess" + }, + "networkAccess": true })) - .expect("read-only policy should deserialize"); + .expect("read-only policy should ignore legacy fullAccess field"); assert_eq!( policy, SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, - network_access: false, + network_access: true } ); } #[test] - fn sandbox_policy_deserializes_legacy_workspace_write_without_read_only_access_field() { - let policy: SandboxPolicy = serde_json::from_value(json!({ + fn sandbox_policy_deserializes_legacy_workspace_write_full_access_field() { + let writable_root = absolute_path("/workspace"); + let policy = serde_json::from_value::(json!({ + "type": "workspaceWrite", + "writableRoots": [writable_root], + "readOnlyAccess": { + "type": "fullAccess" + }, + "networkAccess": true, + "excludeTmpdirEnvVar": true, + "excludeSlashTmp": true + })) + .expect("workspace-write policy should ignore legacy fullAccess field"); + assert_eq!( + policy, + SandboxPolicy::WorkspaceWrite { + writable_roots: vec![absolute_path("/workspace")], + network_access: true, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + } + ); + } + + #[test] + fn sandbox_policy_rejects_legacy_read_only_restricted_access_field() { + let err = serde_json::from_value::(json!({ + "type": "readOnly", + "access": { + "type": "restricted", + "includePlatformDefaults": false, + "readableRoots": [] + } + })) + .expect_err("read-only policy should reject removed restricted access field"); + assert!(err.to_string().contains("readOnly.access")); + } + + #[test] + fn sandbox_policy_rejects_legacy_workspace_write_restricted_read_access_field() { + let err = serde_json::from_value::(json!({ "type": "workspaceWrite", "writableRoots": [], + "readOnlyAccess": { + "type": "restricted", + "includePlatformDefaults": false, + "readableRoots": [] + }, "networkAccess": false, "excludeTmpdirEnvVar": false, "excludeSlashTmp": false })) - .expect("workspace-write policy should deserialize"); - assert_eq!( - policy, - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::FullAccess, - network_access: false, - exclude_tmpdir_env_var: false, - exclude_slash_tmp: false, - } - ); + .expect_err("workspace-write policy should reject removed restricted readOnlyAccess field"); + assert!(err.to_string().contains("workspaceWrite.readOnlyAccess")); } #[test] diff --git a/codex-rs/app-server-test-client/src/lib.rs b/codex-rs/app-server-test-client/src/lib.rs index cf28cb151..2a3cea273 100644 --- a/codex-rs/app-server-test-client/src/lib.rs +++ b/codex-rs/app-server-test-client/src/lib.rs @@ -48,7 +48,6 @@ use codex_app_server_protocol::JSONRPCResponse; use codex_app_server_protocol::LoginAccountResponse; use codex_app_server_protocol::ModelListParams; use codex_app_server_protocol::ModelListResponse; -use codex_app_server_protocol::ReadOnlyAccess; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::SandboxPolicy; use codex_app_server_protocol::ServerNotification; @@ -743,7 +742,6 @@ async fn trigger_zsh_fork_multi_cmd_approval( }; turn_params.approval_policy = Some(AskForApproval::OnRequest); turn_params.sandbox_policy = Some(SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }); @@ -885,7 +883,6 @@ async fn trigger_cmd_approval( experimental_api: true, approval_policy: Some(AskForApproval::OnRequest), sandbox_policy: Some(SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }), dynamic_tools, @@ -912,7 +909,6 @@ async fn trigger_patch_approval( experimental_api: true, approval_policy: Some(AskForApproval::OnRequest), sandbox_policy: Some(SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }), dynamic_tools, diff --git a/codex-rs/app-server/src/command_exec.rs b/codex-rs/app-server/src/command_exec.rs index b72c84e90..8004e282e 100644 --- a/codex-rs/app-server/src/command_exec.rs +++ b/codex-rs/app-server/src/command_exec.rs @@ -712,7 +712,6 @@ mod tests { use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::NetworkSandboxPolicy; - use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -731,7 +730,6 @@ mod tests { fn windows_sandbox_exec_request() -> ExecRequest { let sandbox_policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }; ExecRequest::new( @@ -837,7 +835,6 @@ mod tests { request_id: codex_app_server_protocol::RequestId::Integer(100), }; let sandbox_policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }; 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 04e6ede0c..d41ca2610 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1895,7 +1895,6 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { approvals_reviewer: None, sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![first_cwd.try_into()?], - read_only_access: codex_app_server_protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs index eda24358c..31247418e 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start_zsh_fork.rs @@ -536,7 +536,6 @@ async fn turn_start_shell_zsh_fork_subcommand_decline_marks_parent_declined_v2() approval_policy: Some(codex_app_server_protocol::AskForApproval::UnlessTrusted), sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![workspace.clone().try_into()?], - read_only_access: codex_app_server_protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index 56ff26f90..ef0602ae2 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -1961,7 +1961,6 @@ allowed_approvals_reviewers = ["user"] .sandbox_policy .can_set(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -2070,7 +2069,6 @@ allowed_approvals_reviewers = ["user"] .sandbox_policy .can_set(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::from_absolute_path(root)?], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/config/src/config_toml.rs b/codex-rs/config/src/config_toml.rs index f0de00192..67d68fa04 100644 --- a/codex-rs/config/src/config_toml.rs +++ b/codex-rs/config/src/config_toml.rs @@ -49,7 +49,6 @@ use codex_protocol::config_types::WebSearchToolConfig; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::openai_models::ReasoningEffort; use codex_protocol::protocol::AskForApproval; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_path::normalize_for_path_comparison; @@ -685,7 +684,6 @@ impl ConfigToml { exclude_slash_tmp, }) => SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), - read_only_access: ReadOnlyAccess::FullAccess, network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, diff --git a/codex-rs/core/README.md b/codex-rs/core/README.md index 2e311790d..be222a167 100644 --- a/codex-rs/core/README.md +++ b/codex-rs/core/README.md @@ -51,23 +51,18 @@ sandboxed shell commands that would enter the bubblewrap path before invoking ### Windows Legacy `SandboxPolicy` / `sandbox_mode` configs are still supported on -Windows. - -The elevated setup/runner backend supports legacy `ReadOnlyAccess::Restricted` -for `read-only` and `workspace-write` policies. Restricted read access honors -explicit readable roots plus the command `cwd`, and keeps writable roots -readable when `workspace-write` is used. - -When `include_platform_defaults = true`, the elevated Windows backend adds -backend-managed system read roots required for basic execution, such as -`C:\Windows`, `C:\Program Files`, `C:\Program Files (x86)`, and -`C:\ProgramData`. When it is `false`, those extra system roots are omitted. +Windows. Legacy `read-only` and `workspace-write` policies imply full +filesystem read access; exact readable roots are represented by split +filesystem policies instead. The elevated Windows sandbox also supports: - legacy `ReadOnly` and `WorkspaceWrite` behavior - split filesystem policies that need exact readable roots, exact writable roots, or extra read-only carveouts under writable roots +- backend-managed system read roots required for basic execution, such as + `C:\Windows`, `C:\Program Files`, `C:\Program Files (x86)`, and + `C:\ProgramData`, when a split filesystem policy requests platform defaults The unelevated restricted-token backend still supports the legacy full-read Windows model for legacy `ReadOnly` and `WorkspaceWrite` behavior. It also diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index 268617320..7ea9f2719 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -63,7 +63,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::RealtimeVoice; use codex_protocol::protocol::SandboxPolicy; use serde::Deserialize; @@ -789,10 +788,6 @@ async fn default_permissions_profile_populates_runtime_sandbox_policy() -> std:: config.permissions.sandbox_policy.get(), &SandboxPolicy::WorkspaceWrite { writable_roots: vec![memories_root], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![cwd.path().join("docs").abs(),], - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1150,10 +1145,6 @@ async fn permissions_profiles_allow_unknown_special_paths() -> std::io::Result<( assert_eq!( config.permissions.sandbox_policy.get(), &SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: Vec::new(), - }, network_access: false, } ); @@ -1219,10 +1210,6 @@ async fn permissions_profiles_allow_missing_filesystem_with_warning() -> std::io assert_eq!( config.permissions.sandbox_policy.get(), &SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: Vec::new(), - }, network_access: false, } ); @@ -1485,7 +1472,6 @@ trust_level = "trusted" resolution, SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root.clone()], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1526,7 +1512,6 @@ exclude_slash_tmp = true resolution, SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 7f6195269..82d621a5f 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -591,7 +591,6 @@ allowed_sandbox_modes = ["read-only"] .sandbox_policy .can_set(&SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/context/permissions_instructions_tests.rs b/codex-rs/core/src/context/permissions_instructions_tests.rs index 866c68b4a..c8d4607ba 100644 --- a/codex-rs/core/src/context/permissions_instructions_tests.rs +++ b/codex-rs/core/src/context/permissions_instructions_tests.rs @@ -51,7 +51,6 @@ fn builds_permissions_with_network_access_override() { fn builds_permissions_from_policy() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index 0c841693d..ec5292d36 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -99,11 +99,13 @@ pub struct ExecParams { /// The unelevated restricted-token backend only consumes extra deny-write /// carveouts on top of the legacy `WorkspaceWrite` allow set. The elevated /// backend can also consume explicit read and write roots during setup/refresh. -/// Read-root overrides are layered on top of the baseline helper/platform roots -/// that the elevated setup path needs to launch the sandboxed command. +/// Read-root overrides are layered on top of the baseline helper roots that the +/// elevated setup path needs to launch the sandboxed command. Split policies +/// that opt into platform defaults carry that explicitly with the override. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct WindowsSandboxFilesystemOverrides { pub(crate) read_roots_override: Option>, + pub(crate) read_roots_include_platform_defaults: bool, pub(crate) write_roots_override: Option>, pub(crate) additional_deny_write_paths: Vec, } @@ -546,6 +548,8 @@ async fn exec_windows_sandbox( .unwrap_or_default(); let elevated_read_roots_override = windows_sandbox_filesystem_overrides .and_then(|overrides| overrides.read_roots_override.clone()); + let elevated_read_roots_include_platform_defaults = windows_sandbox_filesystem_overrides + .is_some_and(|overrides| overrides.read_roots_include_platform_defaults); let elevated_write_roots_override = windows_sandbox_filesystem_overrides .and_then(|overrides| overrides.write_roots_override.clone()); let elevated_deny_write_paths = windows_sandbox_filesystem_overrides @@ -571,6 +575,8 @@ async fn exec_windows_sandbox( use_private_desktop: windows_sandbox_private_desktop, proxy_enforced, read_roots_override: elevated_read_roots_override.as_deref(), + read_roots_include_platform_defaults: + elevated_read_roots_include_platform_defaults, write_roots_override: elevated_write_roots_override.as_deref(), deny_write_paths_override: &elevated_deny_write_paths, }, @@ -1064,6 +1070,7 @@ pub(crate) fn resolve_windows_restricted_token_filesystem_overrides( Ok(Some(WindowsSandboxFilesystemOverrides { read_roots_override: None, + read_roots_include_platform_defaults: false, write_roots_override: None, additional_deny_write_paths: additional_deny_write_paths .into_iter() @@ -1127,12 +1134,6 @@ pub(crate) fn resolve_windows_elevated_filesystem_overrides( .needs_direct_runtime_enforcement(network_sandbox_policy, sandbox_policy_cwd); let normalize_path = |path: PathBuf| dunce::canonicalize(&path).unwrap_or(path); let legacy_writable_roots = sandbox_policy.get_writable_roots_with_cwd(sandbox_policy_cwd); - let legacy_readable_root_set: BTreeSet = sandbox_policy - .get_readable_roots_with_cwd(sandbox_policy_cwd) - .into_iter() - .map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf) - .map(&normalize_path) - .collect(); let legacy_root_paths: BTreeSet = legacy_writable_roots .iter() .map(|root| normalize_path(root.root.to_path_buf())) @@ -1143,19 +1144,13 @@ pub(crate) fn resolve_windows_elevated_filesystem_overrides( .map(codex_utils_absolute_path::AbsolutePathBuf::into_path_buf) .map(&normalize_path) .collect(); - let split_readable_root_set: BTreeSet = split_readable_roots.iter().cloned().collect(); let split_root_paths: Vec = split_writable_roots .iter() .map(|root| normalize_path(root.root.to_path_buf())) .collect(); let split_root_path_set: BTreeSet = split_root_paths.iter().cloned().collect(); - let matches_legacy_read_access = file_system_sandbox_policy.has_full_disk_read_access() - == sandbox_policy.has_full_disk_read_access(); - let read_roots_override = if matches_legacy_read_access - && (file_system_sandbox_policy.has_full_disk_read_access() - || split_readable_root_set == legacy_readable_root_set) - { + let read_roots_override = if file_system_sandbox_policy.has_full_disk_read_access() { None } else { Some(split_readable_roots) @@ -1209,6 +1204,8 @@ pub(crate) fn resolve_windows_elevated_filesystem_overrides( } Ok(Some(WindowsSandboxFilesystemOverrides { + read_roots_include_platform_defaults: read_roots_override.is_some() + && file_system_sandbox_policy.include_platform_defaults(), read_roots_override, write_roots_override, additional_deny_write_paths, diff --git a/codex-rs/core/src/exec_tests.rs b/codex-rs/core/src/exec_tests.rs index 1cfa87ff3..c09d4b48d 100644 --- a/codex-rs/core/src/exec_tests.rs +++ b/codex-rs/core/src/exec_tests.rs @@ -470,7 +470,6 @@ fn windows_restricted_token_allows_legacy_restricted_policies() { fn windows_restricted_token_allows_legacy_workspace_write_policies() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -492,7 +491,7 @@ fn windows_restricted_token_allows_legacy_workspace_write_policies() { } #[test] -fn windows_elevated_allows_legacy_restricted_read_policies() { +fn windows_elevated_allows_split_restricted_read_policies() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); let docs = codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path( temp_dir.path().join("docs"), @@ -500,13 +499,14 @@ fn windows_elevated_allows_legacy_restricted_read_policies() { .expect("absolute docs"); std::fs::create_dir_all(docs.as_path()).expect("create docs"); let policy = SandboxPolicy::ReadOnly { - access: codex_protocol::protocol::ReadOnlyAccess::Restricted { - readable_roots: vec![docs], - include_platform_defaults: false, - }, network_access: false, }; - let file_system_policy = FileSystemSandboxPolicy::from(&policy); + let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ + codex_protocol::permissions::FileSystemSandboxEntry { + path: codex_protocol::permissions::FileSystemPath::Path { path: docs }, + access: codex_protocol::permissions::FileSystemAccessMode::Read, + }, + ]); assert_eq!( unsupported_windows_restricted_token_sandbox_reason( @@ -528,7 +528,6 @@ fn windows_restricted_token_rejects_split_only_filesystem_policies() { std::fs::create_dir_all(&docs).expect("create docs"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -572,7 +571,6 @@ fn windows_restricted_token_rejects_root_write_read_only_carveouts() { std::fs::create_dir_all(&docs).expect("create docs"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -619,7 +617,6 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { std::fs::create_dir_all(docs.as_path()).expect("create docs"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -658,6 +655,7 @@ fn windows_restricted_token_supports_full_read_split_write_read_carveouts() { ), Ok(Some(WindowsSandboxFilesystemOverrides { read_roots_override: None, + read_roots_include_platform_defaults: false, write_roots_override: None, additional_deny_write_paths: expected_deny_write_paths, })) @@ -671,7 +669,6 @@ fn windows_elevated_supports_split_restricted_read_roots() { std::fs::create_dir_all(&docs).expect("create docs"); let expected_docs = dunce::canonicalize(&docs).expect("canonical docs"); let policy = SandboxPolicy::ReadOnly { - access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, }; let file_system_policy = FileSystemSandboxPolicy::restricted(vec![ @@ -695,6 +692,7 @@ fn windows_elevated_supports_split_restricted_read_roots() { ), Ok(Some(WindowsSandboxFilesystemOverrides { read_roots_override: Some(vec![expected_docs]), + read_roots_include_platform_defaults: false, write_roots_override: None, additional_deny_write_paths: vec![], })) @@ -709,7 +707,6 @@ fn windows_elevated_supports_split_write_read_carveouts() { let expected_docs = dunce::canonicalize(&docs).expect("canonical docs"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -747,6 +744,7 @@ fn windows_elevated_supports_split_write_read_carveouts() { ), Ok(Some(WindowsSandboxFilesystemOverrides { read_roots_override: None, + read_roots_include_platform_defaults: false, write_roots_override: None, additional_deny_write_paths: vec![ codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(expected_docs) @@ -763,7 +761,6 @@ fn windows_elevated_rejects_unreadable_split_carveouts() { std::fs::create_dir_all(&blocked).expect("create blocked"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -811,7 +808,6 @@ fn windows_elevated_rejects_unreadable_globs() { let temp_dir = tempfile::TempDir::new().expect("tempdir"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -861,7 +857,6 @@ fn windows_elevated_rejects_reopened_writable_descendants() { std::fs::create_dir_all(&nested).expect("create nested"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_protocol::protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index ac1d0285d..f780c0dc8 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -323,7 +323,6 @@ mod agent { // The consolidation agent only needs local memory-root write access and no network. let consolidation_sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/safety_tests.rs b/codex-rs/core/src/safety_tests.rs index a5892b292..774673f88 100644 --- a/codex-rs/core/src/safety_tests.rs +++ b/codex-rs/core/src/safety_tests.rs @@ -28,7 +28,6 @@ fn test_writable_roots_constraint() { // only `cwd` is writable by default. let policy_workspace_only = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -50,7 +49,6 @@ fn test_writable_roots_constraint() { // outside write should be permitted. let policy_with_parent = SandboxPolicy::WorkspaceWrite { writable_roots: vec![parent], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -98,7 +96,6 @@ fn granular_with_all_flags_true_matches_on_request_for_out_of_root_patch() { let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string()); let policy_workspace_only = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -143,7 +140,6 @@ fn granular_sandbox_approval_false_rejects_out_of_root_patch() { let add_outside = ApplyPatchAction::new_add_for_test(&outside_path, "".to_string()); let policy_workspace_only = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -294,7 +290,6 @@ fn missing_project_dot_codex_config_requires_approval() { let action = ApplyPatchAction::new_add_for_test(&config_path, "".to_string()); let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 3208f97dc..f29f6c888 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -43,7 +43,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::protocol::NonSteerableTurnKind; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_protocol::request_permissions::PermissionGrantScope; use codex_protocol::request_permissions::RequestPermissionProfile; @@ -2836,10 +2835,6 @@ async fn session_configuration_apply_preserves_split_file_system_policy_on_cwd_o session_configuration.sandbox_policy = codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![docs_dir.clone()], - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -3010,18 +3005,10 @@ async fn session_configuration_apply_rederives_legacy_file_system_policy_on_cwd_ let workspace = tempfile::tempdir().expect("create temp dir"); let project_root = workspace.path().join("project"); let original_cwd = project_root.join("subdir"); - let docs_dir = original_cwd.join("docs"); - std::fs::create_dir_all(&docs_dir).expect("create docs dir"); - let docs_dir = docs_dir.abs(); - session_configuration.cwd = original_cwd.abs(); session_configuration.sandbox_policy = codex_config::Constrained::allow_any(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![docs_dir], - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/tools/handlers/apply_patch_tests.rs b/codex-rs/core/src/tools/handlers/apply_patch_tests.rs index f3d9bdd7d..230e65622 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch_tests.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch_tests.rs @@ -239,7 +239,6 @@ fn write_permissions_for_paths_skip_dirs_already_writable_under_workspace_root() .expect("nested file path should be absolute"); let sandbox_policy = FileSystemSandboxPolicy::from(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, @@ -262,7 +261,6 @@ fn write_permissions_for_paths_keep_dirs_outside_workspace_root() { let cwd_abs = cwd.abs(); let sandbox_policy = FileSystemSandboxPolicy::from(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, 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 86753a04d..927d1b1ce 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 @@ -28,7 +28,6 @@ use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::AskForApproval; use codex_protocol::protocol::GranularApprovalConfig; use codex_protocol::protocol::GuardianCommandSource; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxType; use codex_shell_escalation::EscalationExecution; @@ -269,7 +268,6 @@ fn shell_request_escalation_execution_is_explicit() { }; let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::from_absolute_path("/tmp/original/output").unwrap()], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/tests/common/zsh_fork.rs b/codex-rs/core/tests/common/zsh_fork.rs index e61d3ea95..bc87c9ea9 100644 --- a/codex-rs/core/tests/common/zsh_fork.rs +++ b/codex-rs/core/tests/common/zsh_fork.rs @@ -43,7 +43,6 @@ impl ZshForkRuntime { pub fn restrictive_workspace_write_policy() -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 588dd98d5..4bb3be663 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -643,7 +643,6 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace( let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -699,7 +698,6 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace( let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index f915ec45a..c16cc86fd 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -768,7 +768,6 @@ fn scenarios() -> Vec { let workspace_write = |network_access| SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -1799,7 +1798,6 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() let approval_policy = AskForApproval::OnRequest; let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -2529,7 +2527,6 @@ allow_local_binding = true let approval_policy = AskForApproval::OnFailure; let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -2831,7 +2828,6 @@ allow_local_binding = true let approval_policy = AskForApproval::OnFailure; let turn_sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/tests/suite/hooks.rs b/codex-rs/core/tests/suite/hooks.rs index 3a0669c00..c683d353a 100644 --- a/codex-rs/core/tests/suite/hooks.rs +++ b/codex-rs/core/tests/suite/hooks.rs @@ -1417,7 +1417,6 @@ async fn permission_request_hook_allows_apply_patch_with_write_alias() -> Result AskForApproval::OnRequest, SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1566,7 +1565,6 @@ allow_local_binding = true let approval_policy = AskForApproval::OnFailure; let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 1bf2ee121..fea228375 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -542,7 +542,6 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let writable_root = AbsolutePathBuf::try_from(writable.path())?; let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index ca0b3e281..2e168bd72 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -433,7 +433,6 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an let writable = TempDir::new().unwrap(); let new_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable.path().try_into().unwrap()], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -712,7 +711,6 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res let writable = TempDir::new().unwrap(); let new_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable.abs()], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/tests/suite/remote_env.rs b/codex-rs/core/tests/suite/remote_env.rs index 1e781a669..36cc2e681 100644 --- a/codex-rs/core/tests/suite/remote_env.rs +++ b/codex-rs/core/tests/suite/remote_env.rs @@ -4,8 +4,12 @@ use codex_exec_server::CopyOptions; use codex_exec_server::CreateDirectoryOptions; use codex_exec_server::FileSystemSandboxContext; use codex_exec_server::RemoveOptions; -use codex_protocol::protocol::ReadOnlyAccess; -use codex_protocol::protocol::SandboxPolicy; +use codex_protocol::models::PermissionProfile; +use codex_protocol::permissions::FileSystemAccessMode; +use codex_protocol::permissions::FileSystemPath; +use codex_protocol::permissions::FileSystemSandboxEntry; +use codex_protocol::permissions::FileSystemSandboxPolicy; +use codex_protocol::permissions::NetworkSandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::PathBufExt; use core_test_support::get_remote_test_env; @@ -60,37 +64,28 @@ fn absolute_path(path: PathBuf) -> AbsolutePathBuf { fn read_only_sandbox(readable_root: PathBuf) -> FileSystemSandboxContext { let readable_root = absolute_path(readable_root); - // The policy is evaluated in the remote container, so use a container path - // for cwd instead of capturing the local test runner cwd. - FileSystemSandboxContext::from_legacy_sandbox_policy( - SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![readable_root.clone()], + FileSystemSandboxContext::from_permission_profile(PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: readable_root, }, - network_access: false, - }, - readable_root, - ) + access: FileSystemAccessMode::Read, + }]), + NetworkSandboxPolicy::Restricted, + )) } fn workspace_write_sandbox(writable_root: PathBuf) -> FileSystemSandboxContext { let writable_root = absolute_path(writable_root); - // The policy is evaluated in the remote container, so use a container path - // for cwd instead of capturing the local test runner cwd. - FileSystemSandboxContext::from_legacy_sandbox_policy( - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![writable_root.clone()], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![], + FileSystemSandboxContext::from_permission_profile(PermissionProfile::from_runtime_permissions( + &FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: writable_root, }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }, - writable_root, - ) + access: FileSystemAccessMode::Write, + }]), + NetworkSandboxPolicy::Restricted, + )) } fn assert_normalized_path_rejected(error: &std::io::Error) { diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index 319e3ef8e..8719bba9f 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -286,7 +286,6 @@ async fn expect_request_permissions_event( fn workspace_write_excluding_tmp() -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/tests/suite/request_permissions_tool.rs b/codex-rs/core/tests/suite/request_permissions_tool.rs index 4df6602cd..8bd83f58b 100644 --- a/codex-rs/core/tests/suite/request_permissions_tool.rs +++ b/codex-rs/core/tests/suite/request_permissions_tool.rs @@ -73,7 +73,6 @@ fn build_add_file_patch(patch_path: &Path, content: &str) -> String { fn workspace_write_excluding_tmp() -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs index 5df7e516a..cd9a73696 100644 --- a/codex-rs/core/tests/suite/responses_api_proxy_headers.rs +++ b/codex-rs/core/tests/suite/responses_api_proxy_headers.rs @@ -138,7 +138,6 @@ async fn submit_turn_with_timeout(test: &TestCodex, prompt: &str) -> Result<()> approvals_reviewer: None, sandbox_policy: SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/exec-server/src/fs_sandbox.rs b/codex-rs/exec-server/src/fs_sandbox.rs index b9f7456f3..a1c77fb88 100644 --- a/codex-rs/exec-server/src/fs_sandbox.rs +++ b/codex-rs/exec-server/src/fs_sandbox.rs @@ -7,7 +7,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_sandboxing::SandboxCommand; use codex_sandboxing::SandboxExecRequest; @@ -194,14 +193,6 @@ fn compatibility_workspace_write_policy( file_system_policy: &FileSystemSandboxPolicy, cwd: &std::path::Path, ) -> SandboxPolicy { - let read_only_access = if file_system_policy.has_full_disk_read_access() { - ReadOnlyAccess::FullAccess - } else { - ReadOnlyAccess::Restricted { - include_platform_defaults: file_system_policy.include_platform_defaults(), - readable_roots: file_system_policy.get_readable_roots_with_cwd(cwd), - } - }; let cwd_abs = AbsolutePathBuf::from_absolute_path(cwd).ok(); let writable_roots = file_system_policy .get_writable_roots_with_cwd(cwd) @@ -212,7 +203,6 @@ fn compatibility_workspace_write_policy( SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/exec/src/event_processor_with_human_output.rs b/codex-rs/exec/src/event_processor_with_human_output.rs index 4060d07da..4dab20449 100644 --- a/codex-rs/exec/src/event_processor_with_human_output.rs +++ b/codex-rs/exec/src/event_processor_with_human_output.rs @@ -484,7 +484,6 @@ fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { network_access, exclude_tmpdir_env_var, exclude_slash_tmp, - read_only_access: _, } => { let mut summary = "workspace-write".to_string(); let mut writable_entries = vec!["workdir".to_string()]; diff --git a/codex-rs/exec/src/lib_tests.rs b/codex-rs/exec/src/lib_tests.rs index 0ec1fbc59..bcb17fb87 100644 --- a/codex-rs/exec/src/lib_tests.rs +++ b/codex-rs/exec/src/lib_tests.rs @@ -422,7 +422,6 @@ fn session_configured_from_thread_response_uses_review_policy_from_response() { approvals_reviewer: codex_app_server_protocol::ApprovalsReviewer::AutoReview, sandbox: codex_app_server_protocol::SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: codex_app_server_protocol::ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index cd5459d1f..aa41464ec 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -182,7 +182,6 @@ async fn python_multiprocessing_lock_works_under_sandbox() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -297,7 +296,6 @@ async fn sandbox_distinguishes_command_and_policy_cwds() { // is under a writable root. let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -379,7 +377,6 @@ async fn sandbox_blocks_first_time_dot_codex_creation() { let config_toml = dot_codex.join("config.toml"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 64d1342be..08ecda6c5 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -32,7 +32,7 @@ use globset::GlobSet; use globset::GlobSetBuilder; /// Linux "platform defaults" that keep common system binaries and dynamic -/// libraries readable when `ReadOnlyAccess::Restricted` requests them. +/// libraries readable when a split filesystem policy requests `:minimal`. /// /// These are intentionally system-level paths only (plus Nix store roots) so /// `include_platform_defaults` does not silently widen access to user data. @@ -1002,7 +1002,6 @@ mod tests { use codex_protocol::protocol::FileSystemSandboxEntry; use codex_protocol::protocol::FileSystemSandboxPolicy; use codex_protocol::protocol::FileSystemSpecialPath; - use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -1371,7 +1370,6 @@ mod tests { AbsolutePathBuf::try_from(existing_root.as_path()).expect("absolute existing root"), AbsolutePathBuf::try_from(missing_root.as_path()).expect("absolute missing root"), ], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1402,7 +1400,6 @@ mod tests { fn mounts_dev_before_writable_dev_binds() { let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(Path::new("/dev")).expect("/dev path")], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1449,23 +1446,17 @@ mod tests { let readable_root = temp_dir.path().join("readable"); std::fs::create_dir(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![ - AbsolutePathBuf::try_from(readable_root.as_path()) - .expect("absolute readable root"), - ], + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Path { + path: AbsolutePathBuf::try_from(readable_root.as_path()) + .expect("absolute readable root"), }, - network_access: false, - }; + access: FileSystemAccessMode::Read, + }]); - let args = create_filesystem_args( - &FileSystemSandboxPolicy::from(&policy), - temp_dir.path(), - NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH, - ) - .expect("filesystem args"); + let args = + create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH) + .expect("filesystem args"); assert_eq!(args.args[0..4], ["--tmpfs", "/", "--dev", "/dev"]); @@ -1483,23 +1474,16 @@ mod tests { #[test] fn restricted_read_only_with_platform_defaults_includes_usr_when_present() { let temp_dir = TempDir::new().expect("temp dir"); - let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: Vec::new(), + let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Minimal, }, - network_access: false, - }; + access: FileSystemAccessMode::Read, + }]); - // `ReadOnlyAccess::Restricted` always includes `cwd` as a readable - // root. Using `"/"` here would intentionally collapse to broad read - // access, so use a non-root cwd to exercise the restricted path. - let args = create_filesystem_args( - &FileSystemSandboxPolicy::from(&policy), - temp_dir.path(), - NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH, - ) - .expect("filesystem args"); + let args = + create_filesystem_args(&policy, temp_dir.path(), NO_UNREADABLE_GLOB_SCAN_MAX_DEPTH) + .expect("filesystem args"); assert!( args.args diff --git a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs index 0ed10717f..0eef35842 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main_tests.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main_tests.rs @@ -5,8 +5,6 @@ use codex_protocol::protocol::FileSystemSandboxPolicy; #[cfg(test)] use codex_protocol::protocol::NetworkSandboxPolicy; #[cfg(test)] -use codex_protocol::protocol::ReadOnlyAccess; -#[cfg(test)] use codex_protocol::protocol::SandboxPolicy; #[cfg(test)] use codex_utils_absolute_path::AbsolutePathBuf; @@ -456,7 +454,6 @@ fn resolve_sandbox_policies_accepts_semantically_equivalent_workspace_write_inpu let workspace = AbsolutePathBuf::from_absolute_path(&workspace).expect("absolute workspace"); let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![workspace], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 3795719f0..17ee7dd8a 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -16,7 +16,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -87,7 +86,6 @@ async fn run_cmd_result_with_writable_roots( .iter() .map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap()) .collect(), - read_only_access: Default::default(), network_access, // Exclude tmp-related folders from writable roots because we need a // folder that is writable by tests but that we intentionally disallow @@ -561,7 +559,6 @@ async fn sandbox_blocks_explicit_split_policy_carveouts_under_bwrap() { let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir")], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -634,7 +631,6 @@ async fn sandbox_reenables_writable_subpaths_under_unreadable_parents() { let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(tmpdir.path()).expect("absolute tempdir")], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -709,7 +705,6 @@ async fn sandbox_blocks_root_read_carveouts_under_bwrap() { std::fs::write(&blocked_target, "secret").expect("seed blocked file"); let sandbox_policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: true, }; let file_system_sandbox_policy = FileSystemSandboxPolicy::restricted(vec![ diff --git a/codex-rs/protocol/src/permissions.rs b/codex-rs/protocol/src/permissions.rs index c1580a90f..450fb3997 100644 --- a/codex-rs/protocol/src/permissions.rs +++ b/codex-rs/protocol/src/permissions.rs @@ -16,7 +16,6 @@ use tracing::error; use ts_rs::TS; use crate::protocol::NetworkAccess; -use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; use crate::protocol::WritableRoot; @@ -834,12 +833,9 @@ impl FileSystemSandboxPolicy { } FileSystemSandboxKind::Restricted => { let cwd_absolute = AbsolutePathBuf::from_absolute_path(cwd).ok(); - let mut include_platform_defaults = false; - let has_full_disk_read_access = self.has_full_disk_read_access(); let has_full_disk_write_access = self.has_full_disk_write_access(); let mut workspace_root_writable = false; let mut writable_roots = Vec::new(); - let mut readable_roots = Vec::new(); let mut tmpdir_writable = false; let mut slash_tmp_writable = false; let mut unbridgeable_root_write = false; @@ -854,39 +850,20 @@ impl FileSystemSandboxPolicy { } else { writable_roots.push(path.clone()); } - } else if entry.access.can_read() { - readable_roots.push(path.clone()); } } FileSystemPath::Special { value } => match value { FileSystemSpecialPath::Root => match entry.access { FileSystemAccessMode::None => {} - FileSystemAccessMode::Read => { - if !has_full_disk_read_access - && let Some(cwd) = cwd_absolute.as_ref() - { - readable_roots.push(absolute_root_path_for_cwd(cwd)); - } - } + FileSystemAccessMode::Read => {} FileSystemAccessMode::Write => { unbridgeable_root_write = true; } }, - FileSystemSpecialPath::Minimal => { - if entry.access.can_read() { - include_platform_defaults = true; - } - } + FileSystemSpecialPath::Minimal => {} FileSystemSpecialPath::CurrentWorkingDirectory => { if entry.access.can_write() { workspace_root_writable = true; - } else if entry.access.can_read() - && let Some(path) = resolve_file_system_special_path( - value, - cwd_absolute.as_ref(), - ) - { - readable_roots.push(path); } } FileSystemSpecialPath::ProjectRoots { subpath } => { @@ -894,36 +871,19 @@ impl FileSystemSandboxPolicy { workspace_root_writable = true; } else if let Some(path) = resolve_file_system_special_path(value, cwd_absolute.as_ref()) + && entry.access.can_write() { - if entry.access.can_write() { - writable_roots.push(path); - } else if entry.access.can_read() { - readable_roots.push(path); - } + writable_roots.push(path); } } FileSystemSpecialPath::Tmpdir => { if entry.access.can_write() { tmpdir_writable = true; - } else if entry.access.can_read() - && let Some(path) = resolve_file_system_special_path( - value, - cwd_absolute.as_ref(), - ) - { - readable_roots.push(path); } } FileSystemSpecialPath::SlashTmp => { if entry.access.can_write() { slash_tmp_writable = true; - } else if entry.access.can_read() - && let Some(path) = resolve_file_system_special_path( - value, - cwd_absolute.as_ref(), - ) - { - readable_roots.push(path); } } FileSystemSpecialPath::Unknown { .. } => {} @@ -941,25 +901,12 @@ impl FileSystemSandboxPolicy { }); } - let read_only_access = if has_full_disk_read_access { - ReadOnlyAccess::FullAccess - } else { - ReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots: dedup_absolute_paths( - readable_roots, - /*normalize_effective_paths*/ false, - ), - } - }; - if workspace_root_writable { SandboxPolicy::WorkspaceWrite { writable_roots: dedup_absolute_paths( writable_roots, /*normalize_effective_paths*/ false, ), - read_only_access, network_access: network_policy.is_enabled(), exclude_tmpdir_env_var: !tmpdir_writable, exclude_slash_tmp: !slash_tmp_writable, @@ -975,7 +922,6 @@ impl FileSystemSandboxPolicy { )); } else { SandboxPolicy::ReadOnly { - access: read_only_access, network_access: network_policy.is_enabled(), } } @@ -1026,78 +972,26 @@ impl From<&SandboxPolicy> for FileSystemSandboxPolicy { match value { SandboxPolicy::DangerFullAccess => FileSystemSandboxPolicy::unrestricted(), SandboxPolicy::ExternalSandbox { .. } => FileSystemSandboxPolicy::external_sandbox(), - SandboxPolicy::ReadOnly { access, .. } => { - let mut entries = Vec::new(); - match access { - ReadOnlyAccess::FullAccess => entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Read, - }), - ReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots, - } => { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::CurrentWorkingDirectory, - }, - access: FileSystemAccessMode::Read, - }); - if *include_platform_defaults { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Minimal, - }, - access: FileSystemAccessMode::Read, - }); - } - entries.extend(readable_roots.iter().cloned().map(|path| { - FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Read, - } - })); - } - } - FileSystemSandboxPolicy::restricted(entries) + SandboxPolicy::ReadOnly { .. } => { + FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }]) } SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access, exclude_tmpdir_env_var, exclude_slash_tmp, .. } => { - let mut entries = Vec::new(); - match read_only_access { - ReadOnlyAccess::FullAccess => entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Root, - }, - access: FileSystemAccessMode::Read, - }), - ReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots, - } => { - if *include_platform_defaults { - entries.push(FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::Minimal, - }, - access: FileSystemAccessMode::Read, - }); - } - entries.extend(readable_roots.iter().cloned().map(|path| { - FileSystemSandboxEntry { - path: FileSystemPath::Path { path }, - access: FileSystemAccessMode::Read, - } - })); - } - } + let mut entries = vec![FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }]; entries.push(FileSystemSandboxEntry { path: FileSystemPath::Special { @@ -1592,15 +1486,23 @@ mod tests { #[test] fn unknown_special_paths_are_ignored_by_legacy_bridge() -> std::io::Result<()> { - let policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { - path: FileSystemPath::Special { - value: FileSystemSpecialPath::unknown( - ":future_special_path", - /*subpath*/ None, - ), + let policy = FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, }, - access: FileSystemAccessMode::Write, - }]); + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::unknown( + ":future_special_path", + /*subpath*/ None, + ), + }, + access: FileSystemAccessMode::Write, + }, + ]); let sandbox_policy = policy.to_legacy_sandbox_policy( NetworkSandboxPolicy::Restricted, @@ -1610,10 +1512,6 @@ mod tests { assert_eq!( sandbox_policy, SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: Vec::new(), - }, network_access: false, } ); @@ -1651,10 +1549,6 @@ mod tests { fn legacy_workspace_write_projection_preserves_symbolic_cwd() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: Vec::new(), - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1663,6 +1557,12 @@ mod tests { assert_eq!( FileSystemSandboxPolicy::from_legacy_sandbox_policy(&policy), FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::CurrentWorkingDirectory, @@ -1741,10 +1641,6 @@ mod tests { let dot_codex_config = cwd.path().join(".codex").join("config.toml"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![], - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1768,10 +1664,6 @@ mod tests { .expect("absolute dot codex"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![], - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1783,6 +1675,12 @@ mod tests { assert_eq!( file_system_policy, FileSystemSandboxPolicy::restricted(vec![ + FileSystemSandboxEntry { + path: FileSystemPath::Special { + value: FileSystemSpecialPath::Root, + }, + access: FileSystemAccessMode::Read, + }, FileSystemSandboxEntry { path: FileSystemPath::Special { value: FileSystemSpecialPath::CurrentWorkingDirectory, diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index a79aa1f01..63e8ab0e5 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1025,76 +1025,6 @@ impl NetworkAccess { matches!(self, NetworkAccess::Enabled) } } -fn default_include_platform_defaults() -> bool { - true -} - -/// Determines how read-only file access is granted inside a restricted -/// sandbox. -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS)] -#[strum(serialize_all = "kebab-case")] -#[serde(tag = "type", rename_all = "kebab-case")] -#[ts(tag = "type")] -pub enum ReadOnlyAccess { - /// Restrict reads to an explicit set of roots. - /// - /// When `include_platform_defaults` is `true`, platform defaults required - /// for basic execution are included in addition to `readable_roots`. - Restricted { - /// Include built-in platform read roots required for basic process - /// execution. - #[serde(default = "default_include_platform_defaults")] - include_platform_defaults: bool, - /// Additional absolute roots that should be readable. - #[serde(default, skip_serializing_if = "Vec::is_empty")] - readable_roots: Vec, - }, - - /// Allow unrestricted file reads. - #[default] - FullAccess, -} - -impl ReadOnlyAccess { - pub fn has_full_disk_read_access(&self) -> bool { - matches!(self, ReadOnlyAccess::FullAccess) - } - - /// Returns true if platform defaults should be included for restricted read access. - pub fn include_platform_defaults(&self) -> bool { - matches!( - self, - ReadOnlyAccess::Restricted { - include_platform_defaults: true, - .. - } - ) - } - - /// Returns the readable roots for restricted read access. - /// - /// For [`ReadOnlyAccess::FullAccess`], returns an empty list because - /// callers should grant blanket read access instead. - pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec { - let mut roots: Vec = match self { - ReadOnlyAccess::FullAccess => return Vec::new(), - ReadOnlyAccess::Restricted { readable_roots, .. } => { - let mut roots = readable_roots.clone(); - match AbsolutePathBuf::from_absolute_path(cwd) { - Ok(cwd_root) => roots.push(cwd_root), - Err(err) => { - error!("Ignoring invalid cwd {cwd:?} for sandbox readable root: {err}"); - } - } - roots - } - }; - - let mut seen = HashSet::new(); - roots.retain(|root| seen.insert(root.to_path_buf())); - roots - } -} /// Determines execution restrictions for model shell commands. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)] @@ -1108,13 +1038,6 @@ pub enum SandboxPolicy { /// Read-only access configuration. #[serde(rename = "read-only")] ReadOnly { - /// Read access granted while running under this policy. - #[serde( - default, - skip_serializing_if = "ReadOnlyAccess::has_full_disk_read_access" - )] - access: ReadOnlyAccess, - /// When set to `true`, outbound network access is allowed. `false` by /// default. #[serde(default, skip_serializing_if = "std::ops::Not::not")] @@ -1139,13 +1062,6 @@ pub enum SandboxPolicy { #[serde(default, skip_serializing_if = "Vec::is_empty")] writable_roots: Vec, - /// Read access granted while running under this policy. - #[serde( - default, - skip_serializing_if = "ReadOnlyAccess::has_full_disk_read_access" - )] - read_only_access: ReadOnlyAccess, - /// When set to `true`, outbound network access is allowed. `false` by /// default. #[serde(default)] @@ -1223,7 +1139,6 @@ impl SandboxPolicy { /// Returns a policy with read-only disk access and no network. pub fn new_read_only_policy() -> Self { SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, } } @@ -1234,7 +1149,6 @@ impl SandboxPolicy { pub fn new_workspace_write_policy() -> Self { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -1242,14 +1156,7 @@ impl SandboxPolicy { } pub fn has_full_disk_read_access(&self) -> bool { - match self { - SandboxPolicy::DangerFullAccess => true, - SandboxPolicy::ExternalSandbox { .. } => true, - SandboxPolicy::ReadOnly { access, .. } => access.has_full_disk_read_access(), - SandboxPolicy::WorkspaceWrite { - read_only_access, .. - } => read_only_access.has_full_disk_read_access(), - } + true } pub fn has_full_disk_write_access(&self) -> bool { @@ -1270,46 +1177,6 @@ impl SandboxPolicy { } } - /// Returns true if platform defaults should be included for restricted read access. - pub fn include_platform_defaults(&self) -> bool { - if self.has_full_disk_read_access() { - return false; - } - match self { - SandboxPolicy::ReadOnly { access, .. } => access.include_platform_defaults(), - SandboxPolicy::WorkspaceWrite { - read_only_access, .. - } => read_only_access.include_platform_defaults(), - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => false, - } - } - - /// Returns the list of readable roots (tailored to the current working - /// directory) when read access is restricted. - /// - /// For policies with full read access, this returns an empty list because - /// callers should grant blanket reads. - pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec { - let mut roots = match self { - SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => Vec::new(), - SandboxPolicy::ReadOnly { access, .. } => access.get_readable_roots_with_cwd(cwd), - SandboxPolicy::WorkspaceWrite { - read_only_access, .. - } => { - let mut roots = read_only_access.get_readable_roots_with_cwd(cwd); - roots.extend( - self.get_writable_roots_with_cwd(cwd) - .into_iter() - .map(|root| root.root), - ); - roots - } - }; - let mut seen = HashSet::new(); - roots.retain(|root| seen.insert(root.to_path_buf())); - roots - } - /// Returns the list of writable roots (tailored to the current working /// directory) together with subpaths that should remain read‑only under /// each writable root. @@ -1320,7 +1187,6 @@ impl SandboxPolicy { SandboxPolicy::ReadOnly { .. } => Vec::new(), SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access: _, exclude_tmpdir_env_var, exclude_slash_tmp, network_access: _, @@ -4077,19 +3943,8 @@ mod tests { sorted_roots } - fn sandbox_policy_allows_read(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool { - if policy.has_full_disk_read_access() { - return true; - } - - policy - .get_readable_roots_with_cwd(cwd) - .iter() - .any(|root| path.starts_with(root.as_path())) - || policy - .get_writable_roots_with_cwd(cwd) - .iter() - .any(|root| path.starts_with(root.root.as_path())) + fn sandbox_policy_allows_read(policy: &SandboxPolicy, _path: &Path, _cwd: &Path) -> bool { + policy.has_full_disk_read_access() } fn sandbox_policy_allows_write(policy: &SandboxPolicy, path: &Path, cwd: &Path) -> bool { @@ -4217,12 +4072,6 @@ mod tests { fn sandbox_policy_probe_paths(policy: &SandboxPolicy, cwd: &Path) -> Vec { let mut paths = vec![cwd.to_path_buf()]; - paths.extend( - policy - .get_readable_roots_with_cwd(cwd) - .into_iter() - .map(|path| path.to_path_buf()), - ); for root in policy.get_writable_roots_with_cwd(cwd) { paths.push(root.root.to_path_buf()); paths.extend( @@ -4253,10 +4102,6 @@ mod tests { actual.has_full_network_access(), expected.has_full_network_access() ); - assert_eq!( - actual.include_platform_defaults(), - expected.include_platform_defaults() - ); let mut probe_paths = sandbox_policy_probe_paths(expected, cwd); probe_paths.extend(sandbox_policy_probe_paths(actual, cwd)); probe_paths.sort(); @@ -4299,7 +4144,6 @@ mod tests { assert!(!restricted.has_full_network_access()); let enabled = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: true, }; assert!(enabled.has_full_network_access()); @@ -4398,38 +4242,6 @@ mod tests { ); } - #[test] - fn workspace_write_restricted_read_access_includes_effective_writable_roots() { - let cwd = if cfg!(windows) { - Path::new(r"C:\workspace") - } else { - Path::new("/tmp/workspace") - }; - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![], - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: false, - }; - - let readable_roots = policy.get_readable_roots_with_cwd(cwd); - let writable_roots = policy.get_writable_roots_with_cwd(cwd); - - for writable_root in writable_roots { - assert!( - readable_roots - .iter() - .any(|root| root.as_path() == writable_root.root.as_path()), - "expected writable root {} to also be readable", - writable_root.root.as_path().display() - ); - } - } - #[test] fn restricted_file_system_policy_reports_full_access_from_root_entries() { let read_only = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry { @@ -4623,34 +4435,6 @@ mod tests { ); } - #[test] - fn legacy_workspace_write_nested_readable_root_stays_writable() { - let cwd = TempDir::new().expect("tempdir"); - let docs = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()); - let canonical_cwd = codex_utils_absolute_path::canonicalize_preserving_symlinks(cwd.path()) - .expect("canonicalize cwd"); - let expected_dot_codex = AbsolutePathBuf::from_absolute_path(canonical_cwd.join(".codex")) - .expect("canonical .codex"); - let policy = SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![docs], - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }; - - assert_eq!( - sorted_writable_roots( - FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(&policy, cwd.path()) - .get_writable_roots_with_cwd(cwd.path()) - ), - vec![(canonical_cwd, vec![expected_dot_codex.to_path_buf()])] - ); - } - #[test] fn file_system_policy_rejects_legacy_bridge_for_non_workspace_writes() { let cwd = if cfg!(windows) { @@ -4684,9 +4468,7 @@ mod tests { #[test] fn legacy_sandbox_policy_semantics_survive_split_bridge() { let cwd = TempDir::new().expect("tempdir"); - let readable_root = AbsolutePathBuf::resolve_path_against_base("readable", cwd.path()); let writable_root = AbsolutePathBuf::resolve_path_against_base("writable", cwd.path()); - let nested_readable_root = AbsolutePathBuf::resolve_path_against_base("docs", cwd.path()); let policies = [ SandboxPolicy::DangerFullAccess, SandboxPolicy::ExternalSandbox { @@ -4696,43 +4478,20 @@ mod tests { network_access: NetworkAccess::Enabled, }, SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }, - SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![readable_root.clone()], - }, - network_access: true, - }, SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, }, SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![readable_root], - }, network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: true, }, - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![nested_readable_root], - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }, ]; for expected in policies { diff --git a/codex-rs/sandboxing/src/manager_tests.rs b/codex-rs/sandboxing/src/manager_tests.rs index a7dca2bf5..d9c3e194f 100644 --- a/codex-rs/sandboxing/src/manager_tests.rs +++ b/codex-rs/sandboxing/src/manager_tests.rs @@ -15,7 +15,6 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; @@ -191,7 +190,6 @@ fn transform_additional_permissions_preserves_denied_entries() { }), }, policy: &SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }, file_system_policy: &FileSystemSandboxPolicy::restricted(vec![ diff --git a/codex-rs/sandboxing/src/policy_transforms.rs b/codex-rs/sandboxing/src/policy_transforms.rs index 065d96e8b..20a026d00 100644 --- a/codex-rs/sandboxing/src/policy_transforms.rs +++ b/codex-rs/sandboxing/src/policy_transforms.rs @@ -10,7 +10,6 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::permissions::ReadDenyMatcher; use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use codex_utils_absolute_path::canonicalize_preserving_symlinks; @@ -535,26 +534,6 @@ pub fn effective_file_system_sandbox_policy( } } -fn merge_read_only_access_with_additional_reads( - read_only_access: &ReadOnlyAccess, - extra_reads: Vec, -) -> ReadOnlyAccess { - match read_only_access { - ReadOnlyAccess::FullAccess => ReadOnlyAccess::FullAccess, - ReadOnlyAccess::Restricted { - include_platform_defaults, - readable_roots, - } => { - let mut merged = readable_roots.clone(); - merged.extend(extra_reads); - ReadOnlyAccess::Restricted { - include_platform_defaults: *include_platform_defaults, - readable_roots: dedup_absolute_paths(merged), - } - } - } -} - fn merge_network_access( base_network_access: bool, additional_permissions: &AdditionalPermissionProfile, @@ -590,7 +569,7 @@ fn sandbox_policy_with_additional_permissions( return sandbox_policy.clone(); } - let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions); + let (_extra_reads, extra_writes) = additional_permission_roots(additional_permissions); match sandbox_policy { SandboxPolicy::DangerFullAccess => SandboxPolicy::DangerFullAccess, @@ -606,7 +585,6 @@ fn sandbox_policy_with_additional_permissions( }, SandboxPolicy::WorkspaceWrite { writable_roots, - read_only_access, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, @@ -615,22 +593,14 @@ fn sandbox_policy_with_additional_permissions( merged_writes.extend(extra_writes); SandboxPolicy::WorkspaceWrite { writable_roots: dedup_absolute_paths(merged_writes), - read_only_access: merge_read_only_access_with_additional_reads( - read_only_access, - extra_reads, - ), network_access: merge_network_access(*network_access, additional_permissions), exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, } } - SandboxPolicy::ReadOnly { - access, - network_access, - } => { + SandboxPolicy::ReadOnly { network_access } => { if extra_writes.is_empty() { SandboxPolicy::ReadOnly { - access: merge_read_only_access_with_additional_reads(access, extra_reads), network_access: merge_network_access(*network_access, additional_permissions), } } else { @@ -639,10 +609,6 @@ fn sandbox_policy_with_additional_permissions( // UnderDevelopment, it's a useful approximation of the desired behavior. SandboxPolicy::WorkspaceWrite { writable_roots: dedup_absolute_paths(extra_writes), - read_only_access: merge_read_only_access_with_additional_reads( - access, - extra_reads, - ), network_access: merge_network_access(*network_access, additional_permissions), exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/sandboxing/src/policy_transforms_tests.rs b/codex-rs/sandboxing/src/policy_transforms_tests.rs index 876cbe9cb..2894b29bb 100644 --- a/codex-rs/sandboxing/src/policy_transforms_tests.rs +++ b/codex-rs/sandboxing/src/policy_transforms_tests.rs @@ -14,7 +14,6 @@ use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use codex_protocol::protocol::NetworkAccess; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use dunce::canonicalize; @@ -767,10 +766,6 @@ fn read_only_additional_permissions_can_enable_network_without_writes() { .expect("absolute temp dir"); let policy = sandbox_policy_with_additional_permissions( &SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![path.clone()], - }, network_access: false, }, &PermissionProfile { @@ -778,7 +773,7 @@ fn read_only_additional_permissions_can_enable_network_without_writes() { enabled: Some(true), }), file_system: Some(FileSystemPermissions::from_read_write_roots( - Some(vec![path.clone()]), + Some(vec![path]), Some(Vec::new()), )), }, @@ -787,10 +782,6 @@ fn read_only_additional_permissions_can_enable_network_without_writes() { assert_eq!( policy, SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![path], - }, network_access: true, } ); diff --git a/codex-rs/sandboxing/src/restricted_read_only_platform_defaults.sbpl b/codex-rs/sandboxing/src/restricted_read_only_platform_defaults.sbpl index 0e3a7bb2f..d3015bc58 100644 --- a/codex-rs/sandboxing/src/restricted_read_only_platform_defaults.sbpl +++ b/codex-rs/sandboxing/src/restricted_read_only_platform_defaults.sbpl @@ -1,4 +1,4 @@ -; macOS platform defaults included via `ReadOnlyAccess::Restricted::include_platform_defaults` +; macOS platform defaults included when a split filesystem policy requests `:minimal`. ; Read access to standard system paths (allow file-read* file-test-existence diff --git a/codex-rs/sandboxing/src/seatbelt_tests.rs b/codex-rs/sandboxing/src/seatbelt_tests.rs index a07e02dfc..b69148574 100644 --- a/codex-rs/sandboxing/src/seatbelt_tests.rs +++ b/codex-rs/sandboxing/src/seatbelt_tests.rs @@ -26,7 +26,6 @@ use codex_protocol::permissions::FileSystemSandboxEntry; use codex_protocol::permissions::FileSystemSandboxPolicy; use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; -use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; @@ -350,43 +349,6 @@ fn seatbelt_args_without_extension_profile_keep_legacy_preferences_read_access() assert!(!policy.contains("(allow user-preference-write)")); } -#[test] -fn seatbelt_legacy_workspace_write_nested_readable_root_stays_writable() { - let tmp = TempDir::new().expect("tempdir"); - let cwd = tmp.path().join("workspace"); - fs::create_dir_all(cwd.join("docs")).expect("create docs"); - let docs = AbsolutePathBuf::from_absolute_path(cwd.join("docs")).expect("absolute docs"); - let args = create_seatbelt_command_args_for_legacy_policy( - vec!["/bin/true".to_string()], - &SandboxPolicy::WorkspaceWrite { - writable_roots: vec![], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: vec![docs.clone()], - }, - network_access: false, - exclude_tmpdir_env_var: true, - exclude_slash_tmp: true, - }, - cwd.as_path(), - /*enforce_managed_network*/ false, - /*network*/ None, - ); - - assert!( - !args - .iter() - .any(|arg| arg.ends_with(&format!("={}", docs.as_path().display()))), - "legacy workspace-write readable roots under cwd should not become seatbelt carveouts:\n{args:#?}", - ); - assert!( - args.iter() - .any(|arg| arg.starts_with("-DWRITABLE_ROOT_0_EXCLUDED_") - && arg.ends_with("/workspace/.codex")), - "expected proactive .codex carveout for cwd root: {args:#?}", - ); -} - #[test] fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() { let policy = dynamic_network_policy( @@ -427,7 +389,6 @@ fn dynamic_network_policy_preserves_restricted_policy_when_proxy_config_without_ let policy = dynamic_network_policy( &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -464,7 +425,6 @@ fn dynamic_network_policy_blocks_dns_when_local_binding_has_no_proxy_ports() { let policy = dynamic_network_policy( &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -493,7 +453,6 @@ fn dynamic_network_policy_preserves_restricted_policy_for_managed_network_withou let policy = dynamic_network_policy( &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -784,7 +743,6 @@ fn create_seatbelt_args_full_network_with_proxy_is_still_proxy_only() { let policy = dynamic_network_policy( &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -835,7 +793,6 @@ fn create_seatbelt_args_with_read_only_git_and_codex_subpaths() { .into_iter() .map(|p| p.try_into().unwrap()) .collect(), - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1054,7 +1011,6 @@ fn create_seatbelt_args_block_first_time_dot_codex_creation_with_exact_and_desce let config_toml = dot_codex.join("config.toml"); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![repo_root.as_path().try_into().expect("absolute repo root")], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1110,7 +1066,6 @@ fn create_seatbelt_args_with_read_only_git_pointer_file() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![worktree_root.try_into().expect("worktree_root is absolute")], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1206,7 +1161,6 @@ fn create_seatbelt_args_for_cwd_as_git_repo() { // `.codex` checks are done properly for cwd. let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f4f7dede2..6ae18ec8a 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -182,7 +182,6 @@ pub(super) use codex_protocol::protocol::PatchApplyStatus as CorePatchApplyStatu pub(super) use codex_protocol::protocol::RateLimitReachedType; pub(super) use codex_protocol::protocol::RateLimitSnapshot; pub(super) use codex_protocol::protocol::RateLimitWindow; -pub(super) use codex_protocol::protocol::ReadOnlyAccess; pub(super) use codex_protocol::protocol::RealtimeConversationClosedEvent; pub(super) use codex_protocol::protocol::RealtimeConversationRealtimeEvent; pub(super) use codex_protocol::protocol::RealtimeEvent; diff --git a/codex-rs/tui/src/chatwidget/tests/permissions.rs b/codex-rs/tui/src/chatwidget/tests/permissions.rs index 29f953452..73263c687 100644 --- a/codex-rs/tui/src/chatwidget/tests/permissions.rs +++ b/codex-rs/tui/src/chatwidget/tests/permissions.rs @@ -54,7 +54,6 @@ async fn preset_matching_accepts_workspace_write_with_extra_roots() { let extra_root = test_path_buf("/tmp/extra").abs(); let current_sandbox = SandboxPolicy::WorkspaceWrite { writable_roots: vec![extra_root], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -537,7 +536,6 @@ async fn permissions_selection_marks_auto_review_current_with_custom_workspace_w approvals_reviewer: ApprovalsReviewer::AutoReview, sandbox_policy: SandboxPolicy::WorkspaceWrite { writable_roots: vec![extra_root], - read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index 3fe3e5a8a..44611deee 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -102,7 +102,6 @@ async fn status_snapshot_includes_reasoning_details() { .sandbox_policy .set(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -188,7 +187,6 @@ async fn status_permissions_non_default_workspace_write_is_custom() { .sandbox_policy .set(SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs b/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs index a65d6b2ce..f9a4f5daf 100644 --- a/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs +++ b/codex-rs/utils/sandbox-summary/src/sandbox_summary.rs @@ -23,7 +23,6 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { network_access, exclude_tmpdir_env_var, exclude_slash_tmp, - read_only_access: _, } => { let mut summary = "workspace-write".to_string(); @@ -75,7 +74,6 @@ mod tests { #[test] fn summarizes_read_only_with_enabled_network() { let summary = summarize_sandbox_policy(&SandboxPolicy::ReadOnly { - access: Default::default(), network_access: true, }); assert_eq!(summary, "read-only (network access enabled)"); @@ -87,7 +85,6 @@ mod tests { let writable_root = AbsolutePathBuf::try_from(root).unwrap(); let summary = summarize_sandbox_policy(&SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root.clone()], - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/windows-sandbox-rs/src/allow.rs b/codex-rs/windows-sandbox-rs/src/allow.rs index b40532cda..273dc8c4f 100644 --- a/codex-rs/windows-sandbox-rs/src/allow.rs +++ b/codex-rs/windows-sandbox-rs/src/allow.rs @@ -110,7 +110,6 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(extra_root.as_path()).unwrap()], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -137,7 +136,6 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, @@ -165,7 +163,6 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, @@ -193,7 +190,6 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, @@ -222,7 +218,6 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, @@ -251,7 +246,6 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], - read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index 77e4b3a89..b6e3ace1c 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -13,6 +13,7 @@ pub struct ElevatedSandboxCaptureRequest<'a> { pub use_private_desktop: bool, pub proxy_enforced: bool, pub read_roots_override: Option<&'a [PathBuf]>, + pub read_roots_include_platform_defaults: bool, pub write_roots_override: Option<&'a [PathBuf]>, pub deny_write_paths_override: &'a [PathBuf], } @@ -121,6 +122,7 @@ mod windows_impl { use_private_desktop, proxy_enforced, read_roots_override, + read_roots_include_platform_defaults, write_roots_override, deny_write_paths_override, } = request; @@ -142,6 +144,7 @@ mod windows_impl { &env_map, codex_home, read_roots_override, + read_roots_include_platform_defaults, write_roots_override, deny_write_paths_override, proxy_enforced, @@ -250,7 +253,6 @@ mod windows_impl { fn workspace_policy(network_access: bool) -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/windows-sandbox-rs/src/identity.rs b/codex-rs/windows-sandbox-rs/src/identity.rs index 12b021054..84e72341e 100644 --- a/codex-rs/windows-sandbox-rs/src/identity.rs +++ b/codex-rs/windows-sandbox-rs/src/identity.rs @@ -137,6 +137,7 @@ pub fn require_logon_sandbox_creds( env_map: &HashMap, codex_home: &Path, read_roots_override: Option<&[PathBuf]>, + read_roots_include_platform_defaults: bool, write_roots_override: Option<&[PathBuf]>, deny_write_paths_override: &[PathBuf], proxy_enforced: bool, @@ -198,6 +199,7 @@ pub fn require_logon_sandbox_creds( }, crate::setup::SetupRootOverrides { read_roots: Some(needed_read.clone()), + read_roots_include_platform_defaults, write_roots: Some(needed_write.clone()), deny_write_paths: Some(deny_write_paths_override.to_vec()), }, @@ -216,6 +218,7 @@ pub fn require_logon_sandbox_creds( }, crate::setup::SetupRootOverrides { read_roots: Some(needed_read), + read_roots_include_platform_defaults, write_roots: Some(needed_write), deny_write_paths: Some(deny_write_paths_override.to_vec()), }, diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index b807ded58..8110c3237 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -642,7 +642,6 @@ mod windows_impl { fn workspace_policy(network_access: bool) -> SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs index 8c334000e..94dd3574b 100644 --- a/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs +++ b/codex-rs/windows-sandbox-rs/src/setup_orchestrator.rs @@ -94,6 +94,7 @@ pub struct SandboxSetupRequest<'a> { #[derive(Default)] pub struct SetupRootOverrides { pub read_roots: Option>, + pub read_roots_include_platform_defaults: bool, pub write_roots: Option>, pub deny_write_paths: Option>, } @@ -148,6 +149,7 @@ pub fn run_setup_refresh_with_extra_read_roots( }, SetupRootOverrides { read_roots: Some(read_roots), + read_roots_include_platform_defaults: false, write_roots: Some(Vec::new()), deny_write_paths: None, }, @@ -373,38 +375,12 @@ fn gather_legacy_full_read_roots( canonical_existing(&roots) } -fn gather_restricted_read_roots( - command_cwd: &Path, - policy: &SandboxPolicy, - codex_home: &Path, -) -> Vec { - let mut roots = gather_helper_read_roots(codex_home); - if policy.include_platform_defaults() { - roots.extend( - WINDOWS_PLATFORM_DEFAULT_READ_ROOTS - .iter() - .map(PathBuf::from), - ); - } - roots.extend( - policy - .get_readable_roots_with_cwd(command_cwd) - .into_iter() - .map(|path| path.to_path_buf()), - ); - canonical_existing(&roots) -} - pub(crate) fn gather_read_roots( command_cwd: &Path, policy: &SandboxPolicy, codex_home: &Path, ) -> Vec { - if policy.has_full_disk_read_access() { - gather_legacy_full_read_roots(command_cwd, policy, codex_home) - } else { - gather_restricted_read_roots(command_cwd, policy, codex_home) - } + gather_legacy_full_read_roots(command_cwd, policy, codex_home) } pub(crate) fn gather_write_roots( @@ -792,7 +768,7 @@ fn build_payload_roots( // An explicit override is the split policy's complete readable set. Keep only the // helper/platform roots the elevated setup needs; do not re-add legacy cwd/full-read roots. let mut read_roots = gather_helper_read_roots(request.codex_home); - if request.policy.include_platform_defaults() { + if overrides.read_roots_include_platform_defaults { read_roots.extend( WINDOWS_PLATFORM_DEFAULT_READ_ROOTS .iter() @@ -973,7 +949,6 @@ mod tests { use super::proxy_ports_from_env; use crate::helper_materialization::helper_bin_dir; use crate::policy::SandboxPolicy; - use codex_protocol::protocol::ReadOnlyAccess; use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use std::collections::HashMap; @@ -1298,66 +1273,7 @@ mod tests { } #[test] - fn restricted_read_roots_skip_platform_defaults_when_disabled() { - let tmp = TempDir::new().expect("tempdir"); - let codex_home = tmp.path().join("codex-home"); - let command_cwd = tmp.path().join("workspace"); - let readable_root = tmp.path().join("docs"); - fs::create_dir_all(&command_cwd).expect("create workspace"); - fs::create_dir_all(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![ - AbsolutePathBuf::from_absolute_path(&readable_root) - .expect("absolute readable root"), - ], - }, - network_access: false, - }; - - let roots = gather_read_roots(&command_cwd, &policy, &codex_home); - let expected_helper = - dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); - let expected_cwd = dunce::canonicalize(&command_cwd).expect("canonical workspace"); - let expected_readable = - dunce::canonicalize(&readable_root).expect("canonical readable root"); - - assert!(roots.contains(&expected_helper)); - assert!(roots.contains(&expected_cwd)); - assert!(roots.contains(&expected_readable)); - assert!( - canonical_windows_platform_default_roots() - .into_iter() - .all(|path| !roots.contains(&path)) - ); - } - - #[test] - fn restricted_read_roots_include_platform_defaults_when_enabled() { - let tmp = TempDir::new().expect("tempdir"); - let codex_home = tmp.path().join("codex-home"); - let command_cwd = tmp.path().join("workspace"); - fs::create_dir_all(&command_cwd).expect("create workspace"); - let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: Vec::new(), - }, - network_access: false, - }; - - let roots = gather_read_roots(&command_cwd, &policy, &codex_home); - - assert!( - canonical_windows_platform_default_roots() - .into_iter() - .all(|path| roots.contains(&path)) - ); - } - - #[test] - fn restricted_workspace_write_roots_remain_readable() { + fn workspace_write_roots_remain_readable() { let tmp = TempDir::new().expect("tempdir"); let codex_home = tmp.path().join("codex-home"); let command_cwd = tmp.path().join("workspace"); @@ -1369,10 +1285,6 @@ mod tests { AbsolutePathBuf::from_absolute_path(&writable_root) .expect("absolute writable root"), ], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: Vec::new(), - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1385,55 +1297,6 @@ mod tests { assert!(roots.contains(&expected_writable)); } - #[test] - fn build_payload_roots_preserves_restricted_read_policy_when_no_override_is_needed() { - let tmp = TempDir::new().expect("tempdir"); - let codex_home = tmp.path().join("codex-home"); - let policy_cwd = tmp.path().join("policy-cwd"); - let command_cwd = tmp.path().join("workspace"); - let readable_root = tmp.path().join("docs"); - fs::create_dir_all(&policy_cwd).expect("create policy cwd"); - fs::create_dir_all(&command_cwd).expect("create workspace"); - fs::create_dir_all(&readable_root).expect("create readable root"); - let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: vec![ - AbsolutePathBuf::from_absolute_path(&readable_root) - .expect("absolute readable root"), - ], - }, - network_access: false, - }; - - let (read_roots, write_roots) = build_payload_roots( - &super::SandboxSetupRequest { - policy: &policy, - policy_cwd: &policy_cwd, - command_cwd: &command_cwd, - env_map: &HashMap::new(), - codex_home: &codex_home, - proxy_enforced: false, - }, - &super::SetupRootOverrides::default(), - ); - let expected_helper = - dunce::canonicalize(helper_bin_dir(&codex_home)).expect("canonical helper dir"); - let expected_cwd = dunce::canonicalize(&command_cwd).expect("canonical workspace"); - let expected_readable = - dunce::canonicalize(&readable_root).expect("canonical readable root"); - - assert_eq!(write_roots, Vec::::new()); - assert!(read_roots.contains(&expected_helper)); - assert!(read_roots.contains(&expected_cwd)); - assert!(read_roots.contains(&expected_readable)); - assert!( - canonical_windows_platform_default_roots() - .into_iter() - .all(|path| !read_roots.contains(&path)) - ); - } - #[test] fn build_payload_roots_preserves_helper_roots_when_read_override_is_provided() { let tmp = TempDir::new().expect("tempdir"); @@ -1445,10 +1308,6 @@ mod tests { fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::Restricted { - include_platform_defaults: true, - readable_roots: Vec::new(), - }, network_access: false, }; @@ -1463,6 +1322,7 @@ mod tests { }, &super::SetupRootOverrides { read_roots: Some(vec![readable_root.clone()]), + read_roots_include_platform_defaults: true, write_roots: None, deny_write_paths: None, }, @@ -1495,7 +1355,6 @@ mod tests { fs::create_dir_all(&command_cwd).expect("create workspace"); fs::create_dir_all(&readable_root).expect("create readable root"); let policy = SandboxPolicy::ReadOnly { - access: ReadOnlyAccess::FullAccess, network_access: false, }; @@ -1510,6 +1369,7 @@ mod tests { }, &super::SetupRootOverrides { read_roots: Some(vec![readable_root.clone()]), + read_roots_include_platform_defaults: false, write_roots: None, deny_write_paths: None, }, @@ -1547,10 +1407,6 @@ mod tests { AbsolutePathBuf::from_absolute_path(&extra_write_root) .expect("absolute writable root"), ], - read_only_access: ReadOnlyAccess::Restricted { - include_platform_defaults: false, - readable_roots: Vec::new(), - }, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs index aab2b5446..56d04925b 100644 --- a/codex-rs/windows-sandbox-rs/src/spawn_prep.rs +++ b/codex-rs/windows-sandbox-rs/src/spawn_prep.rs @@ -294,6 +294,7 @@ pub(crate) fn prepare_elevated_spawn_context( env_map, codex_home, /*read_roots_override*/ None, + /*read_roots_include_platform_defaults*/ false, write_roots_override, &deny_write_paths, /*proxy_enforced*/ false, @@ -349,7 +350,6 @@ mod tests { assert!(!should_apply_network_block( &SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), - read_only_access: Default::default(), network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false,