diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index de746e7a78..4a29e8d145 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1243,6 +1243,57 @@ ], "type": "string" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -2018,8 +2069,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -2078,6 +2137,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 5991401b84..650bd16295 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -3518,6 +3518,57 @@ ], "type": "object" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -4355,8 +4406,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -4415,6 +4474,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 69c6c2cb9b..eff42fa4db 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -4544,6 +4544,57 @@ ], "type": "object" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -5461,8 +5512,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -5521,6 +5580,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" 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 88fb5ddbdb..c7c0a46874 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 @@ -6603,6 +6603,57 @@ ], "type": "object" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -7616,8 +7667,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -7676,6 +7735,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/v1/ExecOneOffCommandParams.json b/codex-rs/app-server-protocol/schema/json/v1/ExecOneOffCommandParams.json index a325704be4..ec5b789352 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ExecOneOffCommandParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ExecOneOffCommandParams.json @@ -13,6 +13,57 @@ ], "type": "string" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "title": "FullAccessReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullAccessReadOnlyAccess", + "type": "object" + } + ] + }, "SandboxPolicy": { "description": "Determines execution restrictions for model shell commands.", "oneOf": [ @@ -34,8 +85,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -94,6 +153,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json index 90949d56f2..28ffb82cc4 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -3518,6 +3518,57 @@ ], "type": "object" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -4355,8 +4406,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -4415,6 +4474,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json index b25d2dbed3..5f21d65ec5 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -3518,6 +3518,57 @@ ], "type": "object" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -4355,8 +4406,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -4415,6 +4474,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json index d56ae933bd..b2fc248663 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json @@ -142,6 +142,57 @@ ], "type": "string" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -195,8 +246,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -255,6 +314,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json index fa9e144c48..8f2e441f86 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -3518,6 +3518,57 @@ ], "type": "object" }, + "ReadOnlyAccess": { + "description": "Determines how read-only file access is granted inside a restricted sandbox.", + "oneOf": [ + { + "description": "Restrict reads to an explicit set of roots.\n\nWhen `include_platform_defaults` is `true`, platform defaults required for basic execution are included in addition to `readable_roots`.", + "properties": { + "include_platform_defaults": { + "default": true, + "description": "Include built-in platform read roots required for basic process execution.", + "type": "boolean" + }, + "readable_roots": { + "description": "Additional absolute roots that should be readable.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted" + ], + "title": "RestrictedReadOnlyAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadOnlyAccess", + "type": "object" + }, + { + "description": "Allow unrestricted file reads.", + "properties": { + "type": { + "enum": [ + "full-access" + ], + "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": [ @@ -4355,8 +4406,16 @@ "type": "object" }, { - "description": "Read-only access to the entire file-system.", + "description": "Read-only access configuration.", "properties": { + "access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "read-only" @@ -4415,6 +4474,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_only_access": { + "allOf": [ + { + "$ref": "#/definitions/ReadOnlyAccess" + } + ], + "description": "Read access granted while running under this policy." + }, "type": { "enum": [ "workspace-write" diff --git a/codex-rs/app-server-protocol/schema/typescript/ReadOnlyAccess.ts b/codex-rs/app-server-protocol/schema/typescript/ReadOnlyAccess.ts new file mode 100644 index 0000000000..c01bdd37c6 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/ReadOnlyAccess.ts @@ -0,0 +1,19 @@ +// 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"; + +/** + * Determines how read-only file access is granted inside a restricted + * sandbox. + */ +export type ReadOnlyAccess = { "type": "restricted", +/** + * Include built-in platform read roots required for basic process + * execution. + */ +include_platform_defaults: boolean, +/** + * Additional absolute roots that should be readable. + */ +readable_roots?: Array, } | { "type": "full-access" }; diff --git a/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts index 103a6863f4..743ad22229 100644 --- a/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts @@ -3,11 +3,16 @@ // 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"; /** * Determines execution restrictions for model shell commands. */ -export type SandboxPolicy = { "type": "danger-full-access" } | { "type": "read-only" } | { "type": "external-sandbox", +export type SandboxPolicy = { "type": "danger-full-access" } | { "type": "read-only", +/** + * Read access granted while running under this policy. + */ +access?: ReadOnlyAccess, } | { "type": "external-sandbox", /** * Whether the external sandbox permits outbound network traffic. */ @@ -17,6 +22,10 @@ network_access: NetworkAccess, } | { "type": "workspace-write", * writable from within the sandbox. */ writable_roots?: Array, +/** + * Read access granted while running under this policy. + */ +read_only_access?: ReadOnlyAccess, /** * When set to `true`, outbound network access is allowed. `false` by * default. diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index 895936de2b..03675b3e10 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -137,6 +137,7 @@ export type { Profile } from "./Profile"; export type { RateLimitSnapshot } from "./RateLimitSnapshot"; export type { RateLimitWindow } from "./RateLimitWindow"; export type { RawResponseItemEvent } from "./RawResponseItemEvent"; +export type { ReadOnlyAccess } from "./ReadOnlyAccess"; export type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent"; export type { ReasoningEffort } from "./ReasoningEffort"; export type { ReasoningItem } from "./ReasoningItem"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 679dc4ab23..37004e8081 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -32,6 +32,7 @@ use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; 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::SessionSource as CoreSessionSource; use codex_protocol::protocol::SkillDependencies as CoreSkillDependencies; use codex_protocol::protocol::SkillErrorInfo as CoreSkillErrorInfo; @@ -680,7 +681,9 @@ impl SandboxPolicy { SandboxPolicy::DangerFullAccess => { codex_protocol::protocol::SandboxPolicy::DangerFullAccess } - SandboxPolicy::ReadOnly => codex_protocol::protocol::SandboxPolicy::ReadOnly, + SandboxPolicy::ReadOnly => codex_protocol::protocol::SandboxPolicy::ReadOnly { + access: CoreReadOnlyAccess::FullAccess, + }, SandboxPolicy::ExternalSandbox { network_access } => { codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access: match network_access { @@ -696,6 +699,7 @@ impl SandboxPolicy { exclude_slash_tmp, } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), + read_only_access: CoreReadOnlyAccess::FullAccess, network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, @@ -710,7 +714,7 @@ impl From for SandboxPolicy { codex_protocol::protocol::SandboxPolicy::DangerFullAccess => { SandboxPolicy::DangerFullAccess } - codex_protocol::protocol::SandboxPolicy::ReadOnly => SandboxPolicy::ReadOnly, + codex_protocol::protocol::SandboxPolicy::ReadOnly { .. } => SandboxPolicy::ReadOnly, codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access } => { SandboxPolicy::ExternalSandbox { network_access: match network_access { @@ -721,6 +725,7 @@ impl From for SandboxPolicy { } codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots, + read_only_access: _, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, diff --git a/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs index 2debbda653..863a7bfc4f 100644 --- a/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs +++ b/codex-rs/app-server/tests/suite/codex_message_processor_flow.rs @@ -444,6 +444,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<( approval_policy: AskForApproval::Never, sandbox_policy: SandboxPolicy::WorkspaceWrite { writable_roots: vec![first_cwd.try_into()?], + read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/common/src/approval_presets.rs b/codex-rs/common/src/approval_presets.rs index cec67d258d..9e66e39e08 100644 --- a/codex-rs/common/src/approval_presets.rs +++ b/codex-rs/common/src/approval_presets.rs @@ -26,7 +26,7 @@ pub fn builtin_approval_presets() -> Vec { label: "Read Only", description: "Codex can read files in the current workspace. Approval is required to edit files or access the internet.", approval: AskForApproval::OnRequest, - sandbox: SandboxPolicy::ReadOnly, + sandbox: SandboxPolicy::new_read_only_policy(), }, ApprovalPreset { id: "auto", diff --git a/codex-rs/common/src/sandbox_summary.rs b/codex-rs/common/src/sandbox_summary.rs index 45520b11a0..53851d86b7 100644 --- a/codex-rs/common/src/sandbox_summary.rs +++ b/codex-rs/common/src/sandbox_summary.rs @@ -4,7 +4,7 @@ use codex_core::protocol::SandboxPolicy; pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { match sandbox_policy { SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), - SandboxPolicy::ReadOnly => "read-only".to_string(), + SandboxPolicy::ReadOnly { .. } => "read-only".to_string(), SandboxPolicy::ExternalSandbox { network_access } => { let mut summary = "external-sandbox".to_string(); if matches!(network_access, NetworkAccess::Enabled) { @@ -17,6 +17,7 @@ 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(); @@ -71,6 +72,7 @@ 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/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index a923b0ef78..1407436d25 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -43,6 +43,7 @@ use crate::model_provider_info::built_in_model_providers; use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME; use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME; use crate::protocol::AskForApproval; +use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; use crate::windows_sandbox::WindowsSandboxLevelExt; use codex_app_server_protocol::Tools; @@ -1183,6 +1184,7 @@ 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, @@ -2105,7 +2107,7 @@ network_access = true # This should be ignored. assert_eq!( resolution, SandboxPolicyResolution { - policy: SandboxPolicy::ReadOnly, + policy: SandboxPolicy::new_read_only_policy(), forced_auto_mode_downgraded_on_windows: false, } ); @@ -2139,7 +2141,7 @@ exclude_slash_tmp = true assert_eq!( resolution, SandboxPolicyResolution { - policy: SandboxPolicy::ReadOnly, + policy: SandboxPolicy::new_read_only_policy(), forced_auto_mode_downgraded_on_windows: true, } ); @@ -2149,6 +2151,7 @@ exclude_slash_tmp = true SandboxPolicyResolution { policy: 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, @@ -2189,7 +2192,7 @@ trust_level = "trusted" assert_eq!( resolution, SandboxPolicyResolution { - policy: SandboxPolicy::ReadOnly, + policy: SandboxPolicy::new_read_only_policy(), forced_auto_mode_downgraded_on_windows: true, } ); @@ -2199,6 +2202,7 @@ trust_level = "trusted" SandboxPolicyResolution { policy: SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], + read_only_access: ReadOnlyAccess::FullAccess, network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -2371,7 +2375,7 @@ trust_level = "trusted" "expected workspace-write request to be downgraded on Windows" ); match config.sandbox_policy.get() { - &SandboxPolicy::ReadOnly => {} + SandboxPolicy::ReadOnly { .. } => {} other => panic!("expected read-only policy on Windows, got {other:?}"), } } else { @@ -2517,7 +2521,10 @@ trust_level = "trusted" #[test] fn web_search_mode_for_turn_uses_preference_for_read_only() { let web_search_mode = Constrained::allow_any(WebSearchMode::Cached); - let mode = resolve_web_search_mode_for_turn(&web_search_mode, &SandboxPolicy::ReadOnly); + let mode = resolve_web_search_mode_for_turn( + &web_search_mode, + &SandboxPolicy::new_read_only_policy(), + ); assert_eq!(mode, WebSearchMode::Cached); } @@ -2700,7 +2707,7 @@ profile = "project" if cfg!(target_os = "windows") { assert!(matches!( config.sandbox_policy.get(), - SandboxPolicy::ReadOnly + SandboxPolicy::ReadOnly { .. } )); assert!(config.forced_auto_mode_downgraded_on_windows); } else { @@ -4672,7 +4679,7 @@ trust_level = "untrusted" // Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade) if cfg!(target_os = "windows") { assert!( - matches!(resolution.policy, SandboxPolicy::ReadOnly), + matches!(resolution.policy, SandboxPolicy::ReadOnly { .. }), "Expected ReadOnly on Windows, got {:?}", resolution.policy ); @@ -4768,7 +4775,7 @@ trust_level = "untrusted" assert_eq!( resolution, SandboxPolicyResolution { - policy: SandboxPolicy::ReadOnly, + policy: SandboxPolicy::new_read_only_policy(), forced_auto_mode_downgraded_on_windows: true, } ); @@ -4924,7 +4931,7 @@ mcp_oauth_callback_port = 5678 // Verify that untrusted projects still get WorkspaceWrite sandbox (or ReadOnly on Windows) if cfg!(target_os = "windows") { assert!( - matches!(config.sandbox_policy.get(), SandboxPolicy::ReadOnly), + matches!(config.sandbox_policy.get(), SandboxPolicy::ReadOnly { .. }), "Expected ReadOnly on Windows" ); } else { @@ -4958,7 +4965,10 @@ mcp_oauth_callback_port = 5678 .build() .await?; - assert_eq!(*config.sandbox_policy.get(), SandboxPolicy::ReadOnly); + assert_eq!( + *config.sandbox_policy.get(), + SandboxPolicy::new_read_only_policy() + ); Ok(()) } @@ -4992,7 +5002,10 @@ mcp_oauth_callback_port = 5678 )) .build() .await?; - assert_eq!(*config.sandbox_policy.get(), SandboxPolicy::ReadOnly); + assert_eq!( + *config.sandbox_policy.get(), + SandboxPolicy::new_read_only_policy() + ); Ok(()) } diff --git a/codex-rs/core/src/config_loader/config_requirements.rs b/codex-rs/core/src/config_loader/config_requirements.rs index 6ba68afebe..7e5ca96d00 100644 --- a/codex-rs/core/src/config_loader/config_requirements.rs +++ b/codex-rs/core/src/config_loader/config_requirements.rs @@ -94,7 +94,7 @@ impl Default for ConfigRequirements { None, ), sandbox_policy: ConstrainedWithSource::new( - Constrained::allow_any(SandboxPolicy::ReadOnly), + Constrained::allow_any(SandboxPolicy::new_read_only_policy()), None, ), web_search_mode: ConstrainedWithSource::new( @@ -421,7 +421,7 @@ impl TryFrom for ConfigRequirements { // the other variants (WorkspaceWrite, ExternalSandbox) require // additional parameters. Ultimately, we should expand the config // format to allow specifying those parameters. - let default_sandbox_policy = SandboxPolicy::ReadOnly; + let default_sandbox_policy = SandboxPolicy::new_read_only_policy(); let sandbox_policy = match allowed_sandbox_modes { Some(Sourced { value: modes, @@ -439,7 +439,7 @@ impl TryFrom for ConfigRequirements { let requirement_source_for_error = requirement_source.clone(); let constrained = Constrained::new(default_sandbox_policy, move |candidate| { let mode = match candidate { - SandboxPolicy::ReadOnly => SandboxModeRequirement::ReadOnly, + SandboxPolicy::ReadOnly { .. } => SandboxModeRequirement::ReadOnly, SandboxPolicy::WorkspaceWrite { .. } => { SandboxModeRequirement::WorkspaceWrite } @@ -873,7 +873,7 @@ mod tests { assert!( requirements .sandbox_policy - .can_set(&SandboxPolicy::ReadOnly) + .can_set(&SandboxPolicy::new_read_only_policy()) .is_ok() ); @@ -892,7 +892,7 @@ mod tests { assert!( requirements .sandbox_policy - .can_set(&SandboxPolicy::ReadOnly) + .can_set(&SandboxPolicy::new_read_only_policy()) .is_ok() ); assert!( @@ -900,6 +900,7 @@ mod tests { .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/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index 0a6f67d85b..a47546dac7 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -397,7 +397,7 @@ allowed_sandbox_modes = ["read-only"] ); assert_eq!( *state.requirements().sandbox_policy.get(), - SandboxPolicy::ReadOnly + SandboxPolicy::new_read_only_policy() ); assert!( state @@ -412,6 +412,7 @@ 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/connectors.rs b/codex-rs/core/src/connectors.rs index b451d21b57..1769446b66 100644 --- a/codex-rs/core/src/connectors.rs +++ b/codex-rs/core/src/connectors.rs @@ -80,7 +80,7 @@ pub async fn list_accessible_connectors_from_mcp_tools_with_options( let cancel_token = CancellationToken::new(); let sandbox_state = SandboxState { - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index e627ebec06..398a767ce0 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -1094,7 +1094,13 @@ mod tests { arg0: None, }; - let output = exec(params, SandboxType::None, &SandboxPolicy::ReadOnly, None).await?; + let output = exec( + params, + SandboxType::None, + &SandboxPolicy::new_read_only_policy(), + None, + ) + .await?; assert!(output.timed_out); let stdout = output.stdout.from_utf8_lossy().text; diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 514b4f0af5..77c4d5e7bb 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -290,7 +290,7 @@ pub fn render_decision_for_unmatched_command( // On Windows, ReadOnly sandbox is not a real sandbox, so special-case it // here. let runtime_sandbox_provides_safety = - cfg!(windows) && matches!(sandbox_policy, SandboxPolicy::ReadOnly); + cfg!(windows) && matches!(sandbox_policy, SandboxPolicy::ReadOnly { .. }); // If the command is flagged as dangerous or we have no sandbox protection, // we should never allow it to run without user approval. @@ -325,7 +325,7 @@ pub fn render_decision_for_unmatched_command( // command has not been flagged as dangerous. Decision::Allow } - SandboxPolicy::ReadOnly | SandboxPolicy::WorkspaceWrite { .. } => { + SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => { // In restricted sandboxes (ReadOnly/WorkspaceWrite), do not prompt for // non‑escalated, non‑dangerous commands — let the sandbox enforce // restrictions (e.g., block network/write) without a user prompt. @@ -832,7 +832,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -859,7 +859,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -887,7 +887,7 @@ prefix_rule(pattern=["rm"], decision="forbidden") .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: Some(requested_prefix.clone()), }) @@ -1008,7 +1008,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -1038,7 +1038,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::RequireEscalated, prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]), }) @@ -1149,7 +1149,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -1206,7 +1206,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -1244,7 +1244,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::UnlessTrusted, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -1267,7 +1267,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -1297,7 +1297,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: SandboxPermissions::UseDefault, prefix_rule: None, }) @@ -1386,7 +1386,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &sneaky_command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: permissions, prefix_rule: None, }) @@ -1409,7 +1409,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &dangerous_command, approval_policy: AskForApproval::OnRequest, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: permissions, prefix_rule: None, }) @@ -1428,7 +1428,7 @@ prefix_rule( .create_exec_approval_requirement_for_command(ExecApprovalRequest { command: &dangerous_command, approval_policy: AskForApproval::Never, - sandbox_policy: &SandboxPolicy::ReadOnly, + sandbox_policy: &SandboxPolicy::new_read_only_policy(), sandbox_permissions: permissions, prefix_rule: None, }) diff --git a/codex-rs/core/src/landlock.rs b/codex-rs/core/src/landlock.rs index 51a4661938..65b2a68073 100644 --- a/codex-rs/core/src/landlock.rs +++ b/codex-rs/core/src/landlock.rs @@ -112,7 +112,7 @@ mod tests { fn bwrap_flags_are_feature_gated() { let command = vec!["/bin/true".to_string()]; let cwd = Path::new("/tmp"); - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); let with_bwrap = create_linux_sandbox_command_args(command.clone(), &policy, cwd, true, false); @@ -132,7 +132,7 @@ mod tests { fn proxy_flag_is_included_when_requested() { let command = vec!["/bin/true".to_string()]; let cwd = Path::new("/tmp"); - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); let args = create_linux_sandbox_command_args(command, &policy, cwd, true, true); assert_eq!( diff --git a/codex-rs/core/src/mcp/mod.rs b/codex-rs/core/src/mcp/mod.rs index 8034ed13a2..0ed0d221d2 100644 --- a/codex-rs/core/src/mcp/mod.rs +++ b/codex-rs/core/src/mcp/mod.rs @@ -165,7 +165,7 @@ pub async fn collect_mcp_snapshot(config: &Config) -> McpListToolsResponseEvent // Use ReadOnly sandbox policy for MCP snapshot collection (safest default) let sandbox_state = SandboxState { - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: config.codex_linux_sandbox_exe.clone(), sandbox_cwd: env::current_dir().unwrap_or_else(|_| PathBuf::from("/")), use_linux_sandbox_bwrap: config.features.enabled(Feature::UseLinuxSandboxBwrap), diff --git a/codex-rs/core/src/rollout/metadata.rs b/codex-rs/core/src/rollout/metadata.rs index 5bfdc363ae..7298877751 100644 --- a/codex-rs/core/src/rollout/metadata.rs +++ b/codex-rs/core/src/rollout/metadata.rs @@ -49,7 +49,7 @@ pub(crate) fn builder_from_session_meta( builder.model_provider = session_meta.meta.model_provider.clone(); builder.cwd = session_meta.meta.cwd.clone(); builder.cli_version = Some(session_meta.meta.cli_version.clone()); - builder.sandbox_policy = SandboxPolicy::ReadOnly; + builder.sandbox_policy = SandboxPolicy::new_read_only_policy(); builder.approval_mode = AskForApproval::OnRequest; if let Some(git) = session_meta.git.as_ref() { builder.git_sha = git.commit_hash.clone(); diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 47a12e029e..b0d359b7e1 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -108,7 +108,7 @@ fn is_write_patch_constrained_to_writable_paths( ) -> bool { // Early‑exit if there are no declared writable roots. let writable_roots = match sandbox_policy { - SandboxPolicy::ReadOnly => { + SandboxPolicy::ReadOnly { .. } => { return false; } SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { @@ -195,6 +195,7 @@ mod tests { // 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, @@ -216,6 +217,7 @@ mod tests { // outside write should be permitted. let policy_with_parent = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(parent).unwrap()], + read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index e2061b52dc..acd4638b0d 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -237,10 +237,40 @@ pub(crate) fn create_seatbelt_command_args( } }; - let file_read_policy = if sandbox_policy.has_full_disk_read_access() { - "; allow read-only file operations\n(allow file-read*)" + let (file_read_policy, file_read_dir_params) = if sandbox_policy.has_full_disk_read_access() { + ( + "; allow read-only file operations\n(allow file-read*)".to_string(), + Vec::new(), + ) } else { - "" + let mut readable_roots_policies: Vec = Vec::new(); + let mut file_read_params = Vec::new(); + for (index, root) in sandbox_policy + .get_readable_roots_with_cwd(sandbox_policy_cwd) + .into_iter() + .enumerate() + { + // Canonicalize to avoid mismatches like /var vs /private/var on macOS. + let canonical_root = root + .as_path() + .canonicalize() + .unwrap_or_else(|_| root.to_path_buf()); + let root_param = format!("READABLE_ROOT_{index}"); + file_read_params.push((root_param.clone(), canonical_root)); + readable_roots_policies.push(format!("(subpath (param \"{root_param}\"))")); + } + + if readable_roots_policies.is_empty() { + ("".to_string(), Vec::new()) + } else { + ( + format!( + "; allow read-only file operations\n(allow file-read*\n{}\n)", + readable_roots_policies.join(" ") + ), + file_read_params, + ) + } }; let proxy = proxy_policy_inputs(network); @@ -250,7 +280,12 @@ pub(crate) fn create_seatbelt_command_args( "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" ); - let dir_params = [file_write_dir_params, macos_dir_params()].concat(); + let dir_params = [ + file_read_dir_params, + file_write_dir_params, + macos_dir_params(), + ] + .concat(); let mut seatbelt_args: Vec = vec!["-p".to_string(), full_policy]; let definition_args = dir_params @@ -317,7 +352,7 @@ mod tests { #[test] fn create_seatbelt_args_routes_network_through_proxy_ports() { let policy = dynamic_network_policy( - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), false, &ProxyPolicyInputs { ports: vec![43128, 48081], @@ -351,7 +386,7 @@ mod tests { #[test] fn create_seatbelt_args_allows_local_binding_when_explicitly_enabled() { let policy = dynamic_network_policy( - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), false, &ProxyPolicyInputs { ports: vec![43128], @@ -383,6 +418,7 @@ mod tests { 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, @@ -410,6 +446,7 @@ mod tests { 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, @@ -430,6 +467,7 @@ mod tests { 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, @@ -479,6 +517,7 @@ mod tests { .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, @@ -664,6 +703,7 @@ mod tests { 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, @@ -748,6 +788,7 @@ mod tests { // `.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/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 3dca006d35..12fcc7e844 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -252,7 +252,7 @@ fn unsupported_tool_call_message(payload: &ToolPayload, tool_name: &str) -> Stri fn sandbox_policy_tag(policy: &SandboxPolicy) -> &'static str { match policy { - SandboxPolicy::ReadOnly => "read-only", + SandboxPolicy::ReadOnly { .. } => "read-only", SandboxPolicy::WorkspaceWrite { .. } => "workspace-write", SandboxPolicy::DangerFullAccess => "danger-full-access", SandboxPolicy::ExternalSandbox { .. } => "external-sandbox", diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 56e54e62f1..3671760844 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -326,7 +326,10 @@ mod tests { #[test] fn restricted_sandbox_requires_exec_approval_on_request() { assert_eq!( - default_exec_approval_requirement(AskForApproval::OnRequest, &SandboxPolicy::ReadOnly), + default_exec_approval_requirement( + AskForApproval::OnRequest, + &SandboxPolicy::new_read_only_policy() + ), ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None, diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index ccfa9fe654..112f242bc0 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -578,6 +578,7 @@ 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, @@ -634,6 +635,7 @@ 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 ccf8760c8d..3b5e115ef8 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -628,6 +628,7 @@ 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, @@ -841,7 +842,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_request_requires_approval", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_on_request.txt"), content: "read-only-approval", @@ -861,7 +862,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_request_requires_approval_gpt_5_1_no_exit", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_on_request_5_1.txt"), content: "read-only-approval", @@ -881,7 +882,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "trusted_command_on_request_read_only_runs_without_prompt", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::RunCommand { command: "echo trusted-read-only", }, @@ -896,7 +897,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "trusted_command_on_request_read_only_runs_without_prompt_gpt_5_1_no_exit", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::RunCommand { command: "echo trusted-read-only", }, @@ -911,7 +912,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_request_blocks_network", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::FetchUrl { endpoint: "/ro/network-blocked", response_body: "should-not-see", @@ -925,7 +926,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_request_denied_blocks_execution", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_on_request_denied.txt"), content: "should-not-write", @@ -946,7 +947,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_failure_escalates_after_sandbox_error", approval_policy: OnFailure, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_on_failure.txt"), content: "read-only-on-failure", @@ -967,7 +968,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_failure_escalates_after_sandbox_error_gpt_5_1_no_exit", approval_policy: OnFailure, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_on_failure_5_1.txt"), content: "read-only-on-failure", @@ -987,7 +988,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_request_network_escalates_when_approved", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::FetchUrl { endpoint: "/ro/network-approved", response_body: "read-only-network-ok", @@ -1006,7 +1007,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_on_request_network_escalates_when_approved_gpt_5_1_no_exit", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::FetchUrl { endpoint: "/ro/network-approved", response_body: "read-only-network-ok", @@ -1178,7 +1179,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_unless_trusted_requires_approval", approval_policy: UnlessTrusted, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_unless_trusted.txt"), content: "read-only-unless-trusted", @@ -1198,7 +1199,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_unless_trusted_requires_approval_gpt_5_1_no_exit", approval_policy: UnlessTrusted, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_unless_trusted_5_1.txt"), content: "read-only-unless-trusted", @@ -1218,7 +1219,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "read_only_never_reports_sandbox_failure", approval_policy: Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::WriteFile { target: TargetPath::Workspace("ro_never.txt"), content: "read-only-never", @@ -1242,7 +1243,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "trusted_command_never_runs_without_prompt", approval_policy: Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::RunCommand { command: "echo trusted-never", }, @@ -1407,7 +1408,7 @@ fn scenarios() -> Vec { ScenarioSpec { name: "unified exec on request escalated requires approval", approval_policy: OnRequest, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), action: ActionKind::RunUnifiedExecCommand { command: "python3 -c 'print('\"'\"'escalated unified exec'\"'\"')'", justification: Some(DEFAULT_UNIFIED_EXEC_JUSTIFICATION), @@ -1574,6 +1575,7 @@ 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, @@ -1687,7 +1689,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() async fn approving_execpolicy_amendment_persists_policy_and_skips_future_prompts() -> Result<()> { let server = start_mock_server().await; let approval_policy = AskForApproval::UnlessTrusted; - let sandbox_policy = SandboxPolicy::ReadOnly; + let sandbox_policy = SandboxPolicy::new_read_only_policy(); let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_config(move |config| { config.approval_policy = Constrained::allow_any(approval_policy); diff --git a/codex-rs/core/tests/suite/codex_delegate.rs b/codex-rs/core/tests/suite/codex_delegate.rs index ea759ba5f4..36fa8b4c41 100644 --- a/codex-rs/core/tests/suite/codex_delegate.rs +++ b/codex-rs/core/tests/suite/codex_delegate.rs @@ -63,7 +63,7 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() { // routes ExecApprovalRequest via the parent. let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); - config.sandbox_policy = Constrained::allow_any(SandboxPolicy::ReadOnly); + config.sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); }); let test = builder.build(&server).await.expect("build test codex"); @@ -144,7 +144,7 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() { let mut builder = test_codex().with_model("gpt-5.1").with_config(|config| { config.approval_policy = Constrained::allow_any(AskForApproval::OnRequest); // Use a restricted sandbox so patch approval is required - config.sandbox_policy = Constrained::allow_any(SandboxPolicy::ReadOnly); + config.sandbox_policy = Constrained::allow_any(SandboxPolicy::new_read_only_policy()); config.include_apply_patch_tool = true; }); let test = builder.build(&server).await.expect("build test codex"); diff --git a/codex-rs/core/tests/suite/model_switching.rs b/codex-rs/core/tests/suite/model_switching.rs index b76dcb0467..3f9ec19688 100644 --- a/codex-rs/core/tests/suite/model_switching.rs +++ b/codex-rs/core/tests/suite/model_switching.rs @@ -53,7 +53,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -86,7 +86,7 @@ async fn model_change_appends_model_instructions_developer_message() -> Result<( final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: next_model.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -141,7 +141,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -174,7 +174,7 @@ async fn model_and_personality_change_only_appends_model_instructions() -> Resul final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: next_model.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -288,7 +288,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: image_model_slug.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -307,7 +307,7 @@ async fn model_change_from_image_to_text_strips_prior_image_content() -> Result< final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: text_model_slug.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index fc1f0fa0b6..5bb92114f6 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -457,6 +457,7 @@ 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/personality.rs b/codex-rs/core/tests/suite/personality.rs index 4fdedd0798..97fccabc69 100644 --- a/codex-rs/core/tests/suite/personality.rs +++ b/codex-rs/core/tests/suite/personality.rs @@ -94,7 +94,7 @@ async fn user_turn_personality_none_does_not_add_update_message() -> anyhow::Res final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -141,7 +141,7 @@ async fn config_personality_some_sets_instructions_template() -> anyhow::Result< final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -195,7 +195,7 @@ async fn config_personality_none_sends_no_personality() -> anyhow::Result<()> { final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -255,7 +255,7 @@ async fn default_personality_is_pragmatic_without_config_toml() -> anyhow::Resul final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -303,7 +303,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -337,7 +337,7 @@ async fn user_turn_personality_some_adds_update_message() -> anyhow::Result<()> final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -400,7 +400,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -434,7 +434,7 @@ async fn user_turn_personality_same_value_does_not_add_update_message() -> anyho final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -506,7 +506,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -540,7 +540,7 @@ async fn user_turn_personality_skips_if_feature_disabled() -> anyhow::Result<()> final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: test.config.approval_policy.value(), - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: test.session_configured.model.clone(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -651,7 +651,7 @@ async fn ignores_remote_personality_if_remote_models_disabled() -> anyhow::Resul final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -767,7 +767,7 @@ async fn remote_model_friendly_personality_instructions_with_feature() -> anyhow final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -881,7 +881,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, @@ -915,7 +915,7 @@ async fn user_turn_personality_remote_model_template_includes_update_message() - final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: remote_slug.to_string(), effort: test.config.model_reasoning_effort, summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index e1ef097e20..64a9b55f8b 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -375,6 +375,7 @@ 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, @@ -618,6 +619,7 @@ 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![AbsolutePathBuf::try_from(writable.path()).unwrap()], + 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/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index 82d4d13a6e..a8ba430ab9 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -126,7 +126,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, @@ -293,7 +293,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, @@ -490,7 +490,7 @@ async fn stdio_image_responses_are_sanitized_for_text_only_model() -> anyhow::Re final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: text_only_model_slug.to_string(), effort: None, summary: ReasoningSummary::Auto, @@ -602,7 +602,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, @@ -761,7 +761,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, @@ -952,7 +952,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/seatbelt.rs b/codex-rs/core/tests/suite/seatbelt.rs index 614e87367a..89674494a9 100644 --- a/codex-rs/core/tests/suite/seatbelt.rs +++ b/codex-rs/core/tests/suite/seatbelt.rs @@ -77,6 +77,7 @@ async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() { let test_scenario = create_test_scenario(&tmp); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![test_scenario.repo_parent.as_path().try_into().unwrap()], + read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -103,6 +104,7 @@ async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() { let test_scenario = create_test_scenario(&tmp); let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![test_scenario.repo_root.as_path().try_into().unwrap()], + read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -145,7 +147,7 @@ async fn danger_full_access_allows_all_writes() { async fn read_only_forbids_all_writes() { let tmp = TempDir::new().expect("should be able to create temp dir"); let test_scenario = create_test_scenario(&tmp); - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); test_scenario .run_test( @@ -171,7 +173,7 @@ async fn openpty_works_under_seatbelt() { return; } - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); let command_cwd = std::env::current_dir().expect("getcwd"); let sandbox_cwd = command_cwd.clone(); @@ -229,7 +231,7 @@ async fn java_home_finds_runtime_under_seatbelt() { return; } - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); let command_cwd = std::env::current_dir().expect("getcwd"); let sandbox_cwd = command_cwd.clone(); diff --git a/codex-rs/core/tests/suite/tools.rs b/codex-rs/core/tests/suite/tools.rs index 7efa8bb28e..a828071ea5 100644 --- a/codex-rs/core/tests/suite/tools.rs +++ b/codex-rs/core/tests/suite/tools.rs @@ -230,7 +230,7 @@ async fn sandbox_denied_shell_returns_original_output() -> Result<()> { fixture .submit_turn_with_policy( "run a command that should be denied by the read-only sandbox", - SandboxPolicy::ReadOnly, + SandboxPolicy::new_read_only_policy(), ) .await?; diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index b36fef9eac..9475a9c510 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -384,7 +384,7 @@ async fn mcp_tool_call_output_exceeds_limit_truncated_for_model() -> Result<()> fixture .submit_turn_with_policy( "call the rmcp echo tool with a very large message", - SandboxPolicy::ReadOnly, + SandboxPolicy::new_read_only_policy(), ) .await?; @@ -485,7 +485,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, @@ -742,7 +742,7 @@ async fn mcp_tool_call_output_not_truncated_with_custom_limit() -> Result<()> { fixture .submit_turn_with_policy( "call the rmcp echo tool with a very large message", - SandboxPolicy::ReadOnly, + SandboxPolicy::new_read_only_policy(), ) .await?; diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index c7b21413c5..34c5bd9617 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -2540,7 +2540,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, // Important! - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, @@ -2644,7 +2644,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { final_output_json_schema: None, cwd: cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), model: session_model, effort: None, summary: ReasoningSummary::Auto, diff --git a/codex-rs/core/tests/suite/web_search.rs b/codex-rs/core/tests/suite/web_search.rs index 65e8aedbed..8909e01cf4 100644 --- a/codex-rs/core/tests/suite/web_search.rs +++ b/codex-rs/core/tests/suite/web_search.rs @@ -44,9 +44,12 @@ async fn web_search_mode_cached_sets_external_web_access_false() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy("hello cached web search", SandboxPolicy::ReadOnly) - .await - .expect("submit turn"); + test.submit_turn_with_policy( + "hello cached web search", + SandboxPolicy::new_read_only_policy(), + ) + .await + .expect("submit turn"); let body = resp_mock.single_request().body_json(); let tool = find_web_search_tool(&body); @@ -82,9 +85,12 @@ async fn web_search_mode_takes_precedence_over_legacy_flags() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy("hello cached+live flags", SandboxPolicy::ReadOnly) - .await - .expect("submit turn"); + test.submit_turn_with_policy( + "hello cached+live flags", + SandboxPolicy::new_read_only_policy(), + ) + .await + .expect("submit turn"); let body = resp_mock.single_request().body_json(); let tool = find_web_search_tool(&body); @@ -121,9 +127,12 @@ async fn web_search_mode_defaults_to_cached_when_features_disabled() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy("hello default cached web search", SandboxPolicy::ReadOnly) - .await - .expect("submit turn"); + test.submit_turn_with_policy( + "hello default cached web search", + SandboxPolicy::new_read_only_policy(), + ) + .await + .expect("submit turn"); let body = resp_mock.single_request().body_json(); let tool = find_web_search_tool(&body); @@ -169,7 +178,7 @@ async fn web_search_mode_updates_between_turns_with_sandbox_policy() { .await .expect("create test Codex conversation"); - test.submit_turn_with_policy("hello cached", SandboxPolicy::ReadOnly) + test.submit_turn_with_policy("hello cached", SandboxPolicy::new_read_only_policy()) .await .expect("submit first turn"); test.submit_turn_with_policy("hello live", SandboxPolicy::DangerFullAccess) diff --git a/codex-rs/exec-server/src/posix/mcp.rs b/codex-rs/exec-server/src/posix/mcp.rs index 4cfce9ff89..7e8d0ccdff 100644 --- a/codex-rs/exec-server/src/posix/mcp.rs +++ b/codex-rs/exec-server/src/posix/mcp.rs @@ -123,7 +123,7 @@ impl ExecTool { .await .clone() .unwrap_or_else(|| SandboxState { - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe: None, sandbox_cwd: PathBuf::from(¶ms.workdir), use_linux_sandbox_bwrap: false, diff --git a/codex-rs/exec-server/tests/common/lib.rs b/codex-rs/exec-server/tests/common/lib.rs index a51a589adf..430eb89953 100644 --- a/codex-rs/exec-server/tests/common/lib.rs +++ b/codex-rs/exec-server/tests/common/lib.rs @@ -88,7 +88,7 @@ where S: Service + ClientHandler, { let sandbox_state = SandboxState { - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), codex_linux_sandbox_exe, sandbox_cwd: sandbox_cwd.as_ref().to_path_buf(), use_linux_sandbox_bwrap: false, @@ -110,6 +110,7 @@ where // Note that sandbox_cwd will already be included as a writable root // when the sandbox policy is expanded. writable_roots: vec![], + read_only_access: Default::default(), network_access: false, // Disable writes to temp dir because this is a test, so // writable_folder is likely also under /tmp and we want to be diff --git a/codex-rs/exec/tests/event_processor_with_json_output.rs b/codex-rs/exec/tests/event_processor_with_json_output.rs index f349e4835f..d5f7126952 100644 --- a/codex-rs/exec/tests/event_processor_with_json_output.rs +++ b/codex-rs/exec/tests/event_processor_with_json_output.rs @@ -92,7 +92,7 @@ fn session_configured_produces_thread_started_event() { model: "codex-mini-latest".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index 45de9c2236..ce2e79eac2 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -69,7 +69,7 @@ async fn spawn_command_under_sandbox( async fn linux_sandbox_test_env() -> Option> { let command_cwd = std::env::current_dir().ok()?; let sandbox_cwd = command_cwd.clone(); - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); if can_apply_linux_sandbox_policy(&policy, &command_cwd, sandbox_cwd.as_path(), HashMap::new()) .await @@ -134,6 +134,7 @@ 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, @@ -194,7 +195,7 @@ async fn python_getpwuid_works_under_sandbox() { return; } - let policy = SandboxPolicy::ReadOnly; + let policy = SandboxPolicy::new_read_only_policy(); let command_cwd = std::env::current_dir().expect("should be able to get current dir"); let sandbox_cwd = command_cwd.clone(); @@ -247,6 +248,7 @@ 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, @@ -387,7 +389,7 @@ fn unix_sock_body() { async fn allow_unix_socketpair_recvfrom() { run_code_under_sandbox( "allow_unix_socketpair_recvfrom", - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), || async { unix_sock_body() }, ) .await diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 1a835fd5e0..7ff7acf5ef 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -141,6 +141,13 @@ fn create_bwrap_flags( /// 4. `--dev-bind /dev/null /dev/null` preserves the common sink even under a /// read-only root. fn create_filesystem_args(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Result> { + if !sandbox_policy.has_full_disk_read_access() { + return Err(CodexErr::UnsupportedOperation( + "Restricted read-only access is not yet supported by the Linux bubblewrap backend." + .to_string(), + )); + } + let writable_roots = sandbox_policy.get_writable_roots_with_cwd(cwd); ensure_mount_targets_exist(&writable_roots)?; diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index d86fafd297..8d568dba0a 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -62,6 +62,13 @@ pub(crate) fn apply_sandbox_policy_to_current_thread( } if apply_landlock_fs && !sandbox_policy.has_full_disk_write_access() { + if !sandbox_policy.has_full_disk_read_access() { + return Err(CodexErr::UnsupportedOperation( + "Restricted read-only access is not supported by the legacy Linux Landlock filesystem backend." + .to_string(), + )); + } + let writable_roots = sandbox_policy .get_writable_roots_with_cwd(cwd) .into_iter() @@ -70,9 +77,6 @@ pub(crate) fn apply_sandbox_policy_to_current_thread( install_filesystem_landlock_rules_on_current_thread(writable_roots)?; } - // TODO(ragona): Add appropriate restrictions if - // `sandbox_policy.has_full_disk_read_access()` is `false`. - Ok(()) } @@ -222,11 +226,11 @@ mod tests { #[test] fn restricted_network_policy_always_installs_seccomp() { assert!(should_install_network_seccomp( - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), false )); assert!(should_install_network_seccomp( - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), true )); } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index 2978efae2a..1df9a54438 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -391,7 +391,7 @@ mod tests { fn inserts_bwrap_argv0_before_command_separator() { let argv = build_bwrap_argv( vec!["/bin/true".to_string()], - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), Path::new("/"), BwrapOptions { mount_proc: true, @@ -425,7 +425,7 @@ mod tests { fn inserts_unshare_net_when_network_isolation_requested() { let argv = build_bwrap_argv( vec!["/bin/true".to_string()], - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), Path::new("/"), BwrapOptions { mount_proc: true, @@ -439,7 +439,7 @@ mod tests { fn inserts_unshare_net_when_proxy_only_network_mode_requested() { let argv = build_bwrap_argv( vec!["/bin/true".to_string()], - &SandboxPolicy::ReadOnly, + &SandboxPolicy::new_read_only_policy(), Path::new("/"), BwrapOptions { mount_proc: true, diff --git a/codex-rs/mcp-server/src/outgoing_message.rs b/codex-rs/mcp-server/src/outgoing_message.rs index 1bf98534b0..3f83e1990e 100644 --- a/codex-rs/mcp-server/src/outgoing_message.rs +++ b/codex-rs/mcp-server/src/outgoing_message.rs @@ -301,7 +301,7 @@ mod tests { model: "gpt-4o".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -343,7 +343,7 @@ mod tests { model: "gpt-4o".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, @@ -409,7 +409,7 @@ mod tests { model: "gpt-4o".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffort::default()), history_log_id: 1, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 7f33199702..b5ce0f2b2c 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -312,7 +312,7 @@ impl DeveloperInstructions { let (sandbox_mode, writable_roots) = match sandbox_policy { SandboxPolicy::DangerFullAccess => (SandboxMode::DangerFullAccess, None), - SandboxPolicy::ReadOnly => (SandboxMode::ReadOnly, None), + SandboxPolicy::ReadOnly { .. } => (SandboxMode::ReadOnly, None), SandboxPolicy::ExternalSandbox { .. } => (SandboxMode::DangerFullAccess, None), SandboxPolicy::WorkspaceWrite { .. } => { let roots = sandbox_policy.get_writable_roots_with_cwd(cwd); @@ -1223,6 +1223,7 @@ mod tests { 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/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2368a39ba3..16f896435b 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -4,6 +4,7 @@ //! between user and agent. use std::collections::HashMap; +use std::collections::HashSet; use std::ffi::OsStr; use std::fmt; use std::path::Path; @@ -382,6 +383,107 @@ impl NetworkAccess { } } +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 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 { + include_platform_defaults, + readable_roots, + } => { + let mut roots = readable_roots.clone(); + if *include_platform_defaults { + #[cfg(target_os = "macos")] + for platform_path in [ + "/bin", "/dev", "/etc", "/Library", "/private", "/sbin", "/System", "/tmp", + "/usr", + ] { + #[allow(clippy::expect_used)] + roots.push( + AbsolutePathBuf::from_absolute_path(platform_path) + .expect("platform defaults should be absolute"), + ); + } + + #[cfg(target_os = "linux")] + for platform_path in ["/bin", "/dev", "/etc", "/lib", "/lib64", "/tmp", "/usr"] + { + #[allow(clippy::expect_used)] + roots.push( + AbsolutePathBuf::from_absolute_path(platform_path) + .expect("platform defaults should be absolute"), + ); + } + + #[cfg(target_os = "windows")] + for platform_path in [ + r"C:\Windows", + r"C:\Program Files", + r"C:\Program Files (x86)", + r"C:\ProgramData", + ] { + #[allow(clippy::expect_used)] + roots.push( + AbsolutePathBuf::from_absolute_path(platform_path) + .expect("platform defaults should be absolute"), + ); + } + + 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)] #[strum(serialize_all = "kebab-case")] @@ -391,9 +493,16 @@ pub enum SandboxPolicy { #[serde(rename = "danger-full-access")] DangerFullAccess, - /// Read-only access to the entire file-system. + /// Read-only access configuration. #[serde(rename = "read-only")] - ReadOnly, + ReadOnly { + /// Read access granted while running under this policy. + #[serde( + default, + skip_serializing_if = "ReadOnlyAccess::has_full_disk_read_access" + )] + access: ReadOnlyAccess, + }, /// Indicates the process is already in an external sandbox. Allows full /// disk access while honoring the provided network setting. @@ -413,6 +522,13 @@ 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)] @@ -473,7 +589,9 @@ impl FromStr for SandboxPolicy { impl SandboxPolicy { /// Returns a policy with read-only disk access and no network. pub fn new_read_only_policy() -> Self { - SandboxPolicy::ReadOnly + SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::FullAccess, + } } /// Returns a policy that can read the entire disk, but can only write to @@ -482,22 +600,29 @@ 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, } } - /// Always returns `true`; restricting read access is not supported. pub fn has_full_disk_read_access(&self) -> bool { - true + 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(), + } } pub fn has_full_disk_write_access(&self) -> bool { match self { SandboxPolicy::DangerFullAccess => true, SandboxPolicy::ExternalSandbox { .. } => true, - SandboxPolicy::ReadOnly => false, + SandboxPolicy::ReadOnly { .. } => false, SandboxPolicy::WorkspaceWrite { .. } => false, } } @@ -506,11 +631,35 @@ impl SandboxPolicy { match self { SandboxPolicy::DangerFullAccess => true, SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(), - SandboxPolicy::ReadOnly => false, + SandboxPolicy::ReadOnly { .. } => false, SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access, } } + /// 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 { + writable_roots, + read_only_access, + .. + } => { + let mut roots = read_only_access.get_readable_roots_with_cwd(cwd); + roots.extend(writable_roots.clone()); + 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. @@ -518,9 +667,10 @@ impl SandboxPolicy { match self { SandboxPolicy::DangerFullAccess => Vec::new(), SandboxPolicy::ExternalSandbox { .. } => Vec::new(), - SandboxPolicy::ReadOnly => Vec::new(), + SandboxPolicy::ReadOnly { .. } => Vec::new(), SandboxPolicy::WorkspaceWrite { writable_roots, + read_only_access: _, exclude_tmpdir_env_var, exclude_slash_tmp, network_access: _, @@ -2705,7 +2855,7 @@ mod tests { model: "codex-mini-latest".to_string(), model_provider_id: "openai".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, diff --git a/codex-rs/state/src/model/thread_metadata.rs b/codex-rs/state/src/model/thread_metadata.rs index 2577ead502..d29bf07606 100644 --- a/codex-rs/state/src/model/thread_metadata.rs +++ b/codex-rs/state/src/model/thread_metadata.rs @@ -138,7 +138,7 @@ impl ThreadMetadataBuilder { model_provider: None, cwd: PathBuf::new(), cli_version: None, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), approval_mode: AskForApproval::OnRequest, archived_at: None, git_sha: None, diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 541e02ad38..7110f29282 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -1997,7 +1997,7 @@ WHERE kind = 'memory_stage1' cwd, cli_version: "0.0.0".to_string(), title: String::new(), - sandbox_policy: crate::extract::enum_to_string(&SandboxPolicy::ReadOnly), + sandbox_policy: crate::extract::enum_to_string(&SandboxPolicy::new_read_only_policy()), approval_mode: crate::extract::enum_to_string(&AskForApproval::OnRequest), tokens_used: 0, first_user_message: Some("hello".to_string()), diff --git a/codex-rs/tui/src/additional_dirs.rs b/codex-rs/tui/src/additional_dirs.rs index 54746c1705..98724aa161 100644 --- a/codex-rs/tui/src/additional_dirs.rs +++ b/codex-rs/tui/src/additional_dirs.rs @@ -16,7 +16,7 @@ pub fn add_dir_warning_message( SandboxPolicy::WorkspaceWrite { .. } | SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => None, - SandboxPolicy::ReadOnly => Some(format_warning(additional_dirs)), + SandboxPolicy::ReadOnly { .. } => Some(format_warning(additional_dirs)), } } @@ -64,7 +64,7 @@ mod tests { #[test] fn warns_for_read_only() { - let sandbox = SandboxPolicy::ReadOnly; + let sandbox = SandboxPolicy::new_read_only_policy(); let dirs = vec![PathBuf::from("relative"), PathBuf::from("/abs")]; let message = add_dir_warning_message(&dirs, &sandbox) .expect("expected warning for read-only sandbox"); @@ -76,7 +76,7 @@ mod tests { #[test] fn returns_none_when_no_additional_dirs() { - let sandbox = SandboxPolicy::ReadOnly; + let sandbox = SandboxPolicy::new_read_only_policy(); let dirs: Vec = Vec::new(); assert_eq!(add_dir_warning_message(&dirs, &sandbox), None); } diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 1ef3475e44..f2d0a49d28 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -1156,7 +1156,7 @@ impl App { && matches!( app.config.sandbox_policy.get(), codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_core::protocol::SandboxPolicy::ReadOnly + | codex_core::protocol::SandboxPolicy::ReadOnly { .. } ) && !app .config @@ -1982,7 +1982,7 @@ impl App { let policy_is_workspace_write_or_ro = matches!( &policy, codex_core::protocol::SandboxPolicy::WorkspaceWrite { .. } - | codex_core::protocol::SandboxPolicy::ReadOnly + | codex_core::protocol::SandboxPolicy::ReadOnly { .. } ); if let Err(err) = self.config.sandbox_policy.set(policy.clone()) { @@ -1992,9 +1992,11 @@ impl App { return Ok(AppRunControl::Continue); } #[cfg(target_os = "windows")] - if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly) - || WindowsSandboxLevel::from_config(&self.config) - != WindowsSandboxLevel::Disabled + if !matches!( + &policy, + codex_core::protocol::SandboxPolicy::ReadOnly { .. } + ) || WindowsSandboxLevel::from_config(&self.config) + != WindowsSandboxLevel::Disabled { self.config.forced_auto_mode_downgraded_on_windows = false; } @@ -3047,7 +3049,7 @@ mod tests { model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: None, history_log_id: 0, @@ -3102,7 +3104,7 @@ mod tests { model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: None, history_log_id: 0, @@ -3149,7 +3151,7 @@ mod tests { model: "gpt-test".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: None, history_log_id: 0, diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e507a39a4d..4d5e05b5ac 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5408,15 +5408,16 @@ impl ChatWidget { } matches!( (&preset.sandbox, current_sandbox), - (SandboxPolicy::ReadOnly, SandboxPolicy::ReadOnly) - | ( - SandboxPolicy::DangerFullAccess, - SandboxPolicy::DangerFullAccess - ) - | ( - SandboxPolicy::WorkspaceWrite { .. }, - SandboxPolicy::WorkspaceWrite { .. } - ) + ( + SandboxPolicy::ReadOnly { .. }, + SandboxPolicy::ReadOnly { .. } + ) | ( + SandboxPolicy::DangerFullAccess, + SandboxPolicy::DangerFullAccess + ) | ( + SandboxPolicy::WorkspaceWrite { .. }, + SandboxPolicy::WorkspaceWrite { .. } + ) ) } @@ -5537,7 +5538,7 @@ impl ChatWidget { let mut header_children: Vec> = Vec::new(); let describe_policy = |policy: &SandboxPolicy| match policy { SandboxPolicy::WorkspaceWrite { .. } => "Agent mode", - SandboxPolicy::ReadOnly => "Read-Only mode", + SandboxPolicy::ReadOnly { .. } => "Read-Only mode", _ => "Agent mode", }; let mode_label = preset @@ -5949,7 +5950,7 @@ impl ChatWidget { /// Set the sandbox policy in the widget's config copy. pub(crate) fn set_sandbox_policy(&mut self, policy: SandboxPolicy) -> ConstraintResult<()> { #[cfg(target_os = "windows")] - let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly) + let should_clear_downgrade = !matches!(&policy, SandboxPolicy::ReadOnly { .. }) || WindowsSandboxLevel::from_config(&self.config) != WindowsSandboxLevel::Disabled; self.config.sandbox_policy.set(policy)?; diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 1f1af74a11..fb25306b1c 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -148,7 +148,7 @@ async fn resumed_initial_messages_render_history() { model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -216,7 +216,7 @@ async fn replayed_user_message_preserves_text_elements_and_local_images() { model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -334,7 +334,7 @@ async fn submission_preserves_text_elements_and_local_images() { model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -414,7 +414,7 @@ async fn submission_prefers_selected_duplicate_skill_path() { model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -2972,7 +2972,7 @@ async fn plan_slash_command_with_args_submits_prompt_in_plan_mode() { model: "test-model".to_string(), model_provider_id: "test-provider".to_string(), approval_policy: AskForApproval::Never, - sandbox_policy: SandboxPolicy::ReadOnly, + sandbox_policy: SandboxPolicy::new_read_only_policy(), cwd: PathBuf::from("/home/user/project"), reasoning_effort: Some(ReasoningEffortConfig::default()), history_log_id: 0, @@ -4032,6 +4032,7 @@ async fn preset_matching_ignores_extra_writable_roots() { .expect("auto preset exists"); let current_sandbox = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from("C:\\extra").unwrap()], + read_only_access: Default::default(), network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/tui/src/debug_config.rs b/codex-rs/tui/src/debug_config.rs index 681d9c3a9a..80e97d5dd8 100644 --- a/codex-rs/tui/src/debug_config.rs +++ b/codex-rs/tui/src/debug_config.rs @@ -403,7 +403,7 @@ mod tests { Some(RequirementSource::CloudRequirements), ); requirements.sandbox_policy = ConstrainedWithSource::new( - Constrained::allow_any(SandboxPolicy::ReadOnly), + Constrained::allow_any(SandboxPolicy::new_read_only_policy()), Some(RequirementSource::SystemRequirementsToml { file: requirements_file.clone(), }), diff --git a/codex-rs/tui/src/status/card.rs b/codex-rs/tui/src/status/card.rs index 23005274db..61421976f5 100644 --- a/codex-rs/tui/src/status/card.rs +++ b/codex-rs/tui/src/status/card.rs @@ -157,7 +157,7 @@ impl StatusHistoryCell { .unwrap_or_else(|| "".to_string()); let sandbox = match config.sandbox_policy.get() { SandboxPolicy::DangerFullAccess => "danger-full-access".to_string(), - SandboxPolicy::ReadOnly => "read-only".to_string(), + SandboxPolicy::ReadOnly { .. } => "read-only".to_string(), SandboxPolicy::WorkspaceWrite { .. } => "workspace-write".to_string(), SandboxPolicy::ExternalSandbox { network_access } => { if matches!(network_access, NetworkAccess::Enabled) { diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index c8844904ab..c9bd2c6e67 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -100,6 +100,7 @@ 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, diff --git a/codex-rs/windows-sandbox-rs/src/audit.rs b/codex-rs/windows-sandbox-rs/src/audit.rs index 1af9f1b919..2aefb7a3fd 100644 --- a/codex-rs/windows-sandbox-rs/src/audit.rs +++ b/codex-rs/windows-sandbox-rs/src/audit.rs @@ -262,7 +262,7 @@ pub fn apply_capability_denies_for_world_writable( } (sid, roots) } - SandboxPolicy::ReadOnly => ( + SandboxPolicy::ReadOnly { .. } => ( unsafe { convert_string_sid_to_sid(&caps.readonly) }.ok_or_else(|| { anyhow!("ConvertStringSidToSidW failed for readonly capability") })?, diff --git a/codex-rs/windows-sandbox-rs/src/command_runner_win.rs b/codex-rs/windows-sandbox-rs/src/command_runner_win.rs index 9ff61de932..da949f8778 100644 --- a/codex-rs/windows-sandbox-rs/src/command_runner_win.rs +++ b/codex-rs/windows-sandbox-rs/src/command_runner_win.rs @@ -114,6 +114,11 @@ pub fn main() -> Result<()> { ); let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?; + if !policy.has_full_disk_read_access() { + anyhow::bail!( + "Restricted read-only access is not yet supported by the Windows sandbox backend" + ); + } let mut cap_psids: Vec<*mut c_void> = Vec::new(); for sid in &req.cap_sids { let Some(psid) = (unsafe { convert_string_sid_to_sid(sid) }) else { @@ -129,7 +134,9 @@ pub fn main() -> Result<()> { let base = unsafe { get_current_token_for_restriction()? }; let token_res: Result = unsafe { match &policy { - SandboxPolicy::ReadOnly => create_readonly_token_with_caps_from(base, &cap_psids), + SandboxPolicy::ReadOnly { .. } => { + create_readonly_token_with_caps_from(base, &cap_psids) + } SandboxPolicy::WorkspaceWrite { .. } => { create_workspace_write_token_with_caps_from(base, &cap_psids) } diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index ecd5732c09..bc96599ed0 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -238,9 +238,14 @@ mod windows_impl { ) { anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") } + if !policy.has_full_disk_read_access() { + anyhow::bail!( + "Restricted read-only access is not yet supported by the Windows sandbox backend" + ); + } let caps = load_or_create_cap_sids(codex_home)?; let (psid_to_use, cap_sids) = match &policy { - SandboxPolicy::ReadOnly => ( + SandboxPolicy::ReadOnly { .. } => ( unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() }, vec![caps.readonly.clone()], ), @@ -487,7 +492,7 @@ mod windows_impl { #[test] fn applies_network_block_for_read_only() { - assert!(!SandboxPolicy::ReadOnly.has_full_network_access()); + assert!(!SandboxPolicy::new_read_only_policy().has_full_network_access()); } } } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 4a97efaaf8..5d71b4b206 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -258,10 +258,15 @@ mod windows_impl { ) { anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing") } + if !policy.has_full_disk_read_access() { + anyhow::bail!( + "Restricted read-only access is not yet supported by the Windows sandbox backend" + ); + } let caps = load_or_create_cap_sids(codex_home)?; let (h_token, psid_generic, psid_workspace): (HANDLE, *mut c_void, Option<*mut c_void>) = unsafe { match &policy { - SandboxPolicy::ReadOnly => { + SandboxPolicy::ReadOnly { .. } => { let psid = convert_string_sid_to_sid(&caps.readonly).unwrap(); let (h, _) = super::token::create_readonly_token_with_cap(psid)?; (h, psid, None) @@ -528,7 +533,9 @@ mod windows_impl { #[test] fn applies_network_block_for_read_only() { - assert!(should_apply_network_block(&SandboxPolicy::ReadOnly)); + assert!(should_apply_network_block( + &SandboxPolicy::new_read_only_policy() + )); } } } diff --git a/codex-rs/windows-sandbox-rs/src/policy.rs b/codex-rs/windows-sandbox-rs/src/policy.rs index 64fc56052f..3cee37cdf7 100644 --- a/codex-rs/windows-sandbox-rs/src/policy.rs +++ b/codex-rs/windows-sandbox-rs/src/policy.rs @@ -3,7 +3,7 @@ pub use codex_protocol::protocol::SandboxPolicy; pub fn parse_policy(value: &str) -> Result { match value { - "read-only" => Ok(SandboxPolicy::ReadOnly), + "read-only" => Ok(SandboxPolicy::new_read_only_policy()), "workspace-write" => Ok(SandboxPolicy::new_workspace_write_policy()), "danger-full-access" | "external-sandbox" => anyhow::bail!( "DangerFullAccess and ExternalSandbox are not supported for sandboxing" @@ -52,6 +52,9 @@ mod tests { #[test] fn parses_read_only_policy() { - assert_eq!(parse_policy("read-only").unwrap(), SandboxPolicy::ReadOnly); + assert_eq!( + parse_policy("read-only").unwrap(), + SandboxPolicy::new_read_only_policy() + ); } }