diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 0ccf453020..23e2512031 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1933,6 +1933,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -2037,6 +2040,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess2" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -3097,6 +3108,95 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] + }, + "WorkspaceReadAccess2": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccess2Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess2", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess2Type", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess2", + "type": "object" + } + ] } }, "description": "Request from the client to the server.", diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index f399912e03..7ebb8c4908 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -4303,6 +4303,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -5044,6 +5052,52 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "description": "Response event from the agent NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.", diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 0e2f94f4d8..2a035c538d 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -5343,6 +5343,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -7365,6 +7373,52 @@ "samplePaths" ], "type": "object" + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "description": "Notification sent from the server to the client.", 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 3b6e61fc23..71f6916474 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 @@ -7548,6 +7548,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -9684,6 +9692,52 @@ } ] }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] + }, "v2": { "AbsolutePathBuf": { "description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.", @@ -13464,6 +13518,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/v2/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -13501,6 +13558,9 @@ "default": false, "type": "boolean" }, + "read_access": { + "$ref": "#/definitions/v2/WorkspaceReadAccess" + }, "writable_roots": { "default": [], "items": { @@ -16055,6 +16115,49 @@ "title": "WindowsWorldWritableWarningNotification", "type": "object" }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/v2/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] + }, "WriteStatus": { "enum": [ "ok", 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..ae3104f573 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ExecOneOffCommandParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ExecOneOffCommandParams.json @@ -94,6 +94,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -116,6 +124,52 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { 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 a5838e89e7..7d8e3a3275 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ForkConversationResponse.json @@ -4303,6 +4303,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -5044,6 +5052,52 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { 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 718b17aa28..8224ae8e0d 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v1/ResumeConversationResponse.json @@ -4303,6 +4303,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -5044,6 +5052,52 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { 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..cc0cb4611e 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SendUserTurnParams.json @@ -255,6 +255,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -324,6 +332,52 @@ "byteRange" ], "type": "object" + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { 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 a85b78281b..2d3251feb2 100644 --- a/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v1/SessionConfiguredNotification.json @@ -4303,6 +4303,14 @@ "description": "When set to `true`, outbound network access is allowed. `false` by default.", "type": "boolean" }, + "read_access": { + "allOf": [ + { + "$ref": "#/definitions/WorkspaceReadAccess" + } + ], + "description": "Controls whether the workspace-write policy has full read access or an explicit read allowlist." + }, "type": { "enum": [ "workspace-write" @@ -5044,6 +5052,52 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "description": "Controls read access semantics for `workspace-write` sandbox policies.", + "oneOf": [ + { + "description": "Preserve current behavior where all file-system paths are readable.", + "properties": { + "type": { + "enum": [ + "full-read-access" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "description": "Restrict reads to an explicit allowlist plus implicitly readable paths.", + "properties": { + "readable_roots": { + "description": "Additional folders that should be readable from inside the sandbox.", + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restricted-read-access" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json index 6dd8fb7bc8..a66fa12758 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -84,6 +84,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -106,6 +109,49 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json index 96ce16be09..156fa2d58e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ConfigReadResponse.json @@ -530,6 +530,9 @@ "default": false, "type": "boolean" }, + "read_access": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "writable_roots": { "default": [], "items": { @@ -573,6 +576,49 @@ "live" ], "type": "string" + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 61b12ceff3..c5a4de38d2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -571,6 +571,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -1538,6 +1541,49 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 0534f6e16e..964af14de2 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -571,6 +571,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -1538,6 +1541,49 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index c85d7ce97a..b4464ec4c7 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -571,6 +571,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -1538,6 +1541,49 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json index be351a48ea..e9ac2f147f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -176,6 +176,9 @@ "default": false, "type": "boolean" }, + "readAccess": { + "$ref": "#/definitions/WorkspaceReadAccess" + }, "type": { "enum": [ "workspaceWrite" @@ -369,6 +372,49 @@ "type": "object" } ] + }, + "WorkspaceReadAccess": { + "oneOf": [ + { + "properties": { + "type": { + "enum": [ + "fullReadAccess" + ], + "title": "FullReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "FullReadAccessWorkspaceReadAccess", + "type": "object" + }, + { + "properties": { + "readableRoots": { + "default": [], + "items": { + "$ref": "#/definitions/AbsolutePathBuf" + }, + "type": "array" + }, + "type": { + "enum": [ + "restrictedReadAccess" + ], + "title": "RestrictedReadAccessWorkspaceReadAccessType", + "type": "string" + } + }, + "required": [ + "type" + ], + "title": "RestrictedReadAccessWorkspaceReadAccess", + "type": "object" + } + ] } }, "properties": { diff --git a/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts index 103a6863f4..d2b504794c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts @@ -3,6 +3,7 @@ // 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 { WorkspaceReadAccess } from "./WorkspaceReadAccess"; /** * Determines execution restrictions for model shell commands. @@ -32,4 +33,9 @@ exclude_tmpdir_env_var: boolean, * When set to `true`, will NOT include the `/tmp` among the default * writable roots on UNIX. Defaults to `false`. */ -exclude_slash_tmp: boolean, }; +exclude_slash_tmp: boolean, +/** + * Controls whether the workspace-write policy has full read access or + * an explicit read allowlist. + */ +read_access?: WorkspaceReadAccess, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/WorkspaceReadAccess.ts b/codex-rs/app-server-protocol/schema/typescript/WorkspaceReadAccess.ts new file mode 100644 index 0000000000..06c279b70e --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/WorkspaceReadAccess.ts @@ -0,0 +1,13 @@ +// 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"; + +/** + * Controls read access semantics for `workspace-write` sandbox policies. + */ +export type WorkspaceReadAccess = { "type": "full-read-access" } | { "type": "restricted-read-access", +/** + * Additional folders that should be readable from inside the sandbox. + */ +readable_roots?: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/index.ts b/codex-rs/app-server-protocol/schema/typescript/index.ts index a6ff6fbaf1..1ae5d2bfae 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -219,4 +219,5 @@ export type { WebSearchBeginEvent } from "./WebSearchBeginEvent"; export type { WebSearchEndEvent } from "./WebSearchEndEvent"; export type { WebSearchItem } from "./WebSearchItem"; export type { WebSearchMode } from "./WebSearchMode"; +export type { WorkspaceReadAccess } from "./WorkspaceReadAccess"; export * as v2 from "./v2"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts index 199d7f2a52..30b19ad7f7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts @@ -3,5 +3,6 @@ // 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 { WorkspaceReadAccess } from "./WorkspaceReadAccess"; -export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly" } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; +export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly" } | { "type": "externalSandbox", networkAccess: NetworkAccess, } | { "type": "workspaceWrite", writableRoots: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, readAccess?: WorkspaceReadAccess, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts index cd19d83f1f..0afc3d871b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxWorkspaceWrite.ts @@ -1,5 +1,6 @@ // GENERATED CODE! DO NOT MODIFY BY HAND! // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { WorkspaceReadAccess } from "./WorkspaceReadAccess"; -export type SandboxWorkspaceWrite = { writable_roots: Array, network_access: boolean, exclude_tmpdir_env_var: boolean, exclude_slash_tmp: boolean, }; +export type SandboxWorkspaceWrite = { writable_roots: Array, network_access: boolean, exclude_tmpdir_env_var: boolean, exclude_slash_tmp: boolean, read_access?: WorkspaceReadAccess, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/WorkspaceReadAccess.ts b/codex-rs/app-server-protocol/schema/typescript/v2/WorkspaceReadAccess.ts new file mode 100644 index 0000000000..fed75f3379 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/WorkspaceReadAccess.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { AbsolutePathBuf } from "../AbsolutePathBuf"; + +export type WorkspaceReadAccess = { "type": "fullReadAccess" } | { "type": "restrictedReadAccess", readableRoots: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 65a12daa34..000f1b1ba7 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -185,4 +185,5 @@ export type { TurnSteerResponse } from "./TurnSteerResponse"; export type { UserInput } from "./UserInput"; export type { WebSearchAction } from "./WebSearchAction"; export type { WindowsWorldWritableWarningNotification } from "./WindowsWorldWritableWarningNotification"; +export type { WorkspaceReadAccess } from "./WorkspaceReadAccess"; export type { WriteStatus } from "./WriteStatus"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index a978fec6db..0806628cfa 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -329,6 +329,8 @@ pub struct SandboxWorkspaceWrite { pub exclude_tmpdir_env_var: bool, #[serde(default)] pub exclude_slash_tmp: bool, + #[serde(default, skip_serializing_if = "WorkspaceReadAccess::is_full")] + pub read_access: WorkspaceReadAccess, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] @@ -595,6 +597,27 @@ pub enum NetworkAccess { Enabled, } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS, Default)] +#[serde(tag = "type", rename_all = "camelCase")] +#[ts(tag = "type")] +#[ts(export_to = "v2/")] +pub enum WorkspaceReadAccess { + #[default] + FullReadAccess, + #[serde(rename_all = "camelCase")] + #[ts(rename_all = "camelCase")] + RestrictedReadAccess { + #[serde(default)] + readable_roots: Vec, + }, +} + +impl WorkspaceReadAccess { + pub fn is_full(&self) -> bool { + matches!(self, WorkspaceReadAccess::FullReadAccess) + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] @@ -619,6 +642,8 @@ pub enum SandboxPolicy { exclude_tmpdir_env_var: bool, #[serde(default)] exclude_slash_tmp: bool, + #[serde(default, skip_serializing_if = "WorkspaceReadAccess::is_full")] + read_access: WorkspaceReadAccess, }, } @@ -642,11 +667,22 @@ impl SandboxPolicy { network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + read_access, } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, + read_access: match read_access { + WorkspaceReadAccess::FullReadAccess => { + codex_protocol::protocol::WorkspaceReadAccess::FullReadAccess + } + WorkspaceReadAccess::RestrictedReadAccess { readable_roots } => { + codex_protocol::protocol::WorkspaceReadAccess::RestrictedReadAccess { + readable_roots: readable_roots.clone(), + } + } + }, }, } } @@ -672,11 +708,20 @@ impl From for SandboxPolicy { network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + read_access, } => SandboxPolicy::WorkspaceWrite { writable_roots, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + read_access: match read_access { + codex_protocol::protocol::WorkspaceReadAccess::FullReadAccess => { + WorkspaceReadAccess::FullReadAccess + } + codex_protocol::protocol::WorkspaceReadAccess::RestrictedReadAccess { + readable_roots, + } => WorkspaceReadAccess::RestrictedReadAccess { readable_roots }, + }, }, } } @@ -3143,6 +3188,34 @@ mod tests { assert_eq!(back_to_v2, v2_policy); } + #[test] + fn sandbox_policy_round_trips_workspace_write_restricted_read_access() { + let readable_root = if cfg!(windows) { + AbsolutePathBuf::from_absolute_path("C:\\repo\\readable").expect("absolute path") + } else { + AbsolutePathBuf::from_absolute_path("/repo/readable").expect("absolute path") + }; + let writable_root = if cfg!(windows) { + AbsolutePathBuf::from_absolute_path("C:\\repo\\writable").expect("absolute path") + } else { + AbsolutePathBuf::from_absolute_path("/repo/writable").expect("absolute path") + }; + + let v2_policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![writable_root], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + read_access: WorkspaceReadAccess::RestrictedReadAccess { + readable_roots: vec![readable_root], + }, + }; + + let core_policy = v2_policy.to_core(); + let back_to_v2 = SandboxPolicy::from(core_policy); + assert_eq!(back_to_v2, v2_policy); + } + #[test] fn core_turn_item_into_thread_item_converts_supported_variants() { let user_item = TurnItem::UserMessage(UserMessageItem { diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a6d49f2e4c..d4db344d31 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -288,7 +288,11 @@ You can optionally specify config overrides on the new turn. If specified, these "sandboxPolicy": { "type": "workspaceWrite", "writableRoots": ["/Users/me/project"], - "networkAccess": true + "networkAccess": true, + "readAccess": { + "type": "restrictedReadAccess", + "readableRoots": ["/Users/me/project", "/Users/me/project/.cache"] + } }, "model": "gpt-5.1-codex", "effort": "medium", @@ -468,7 +472,7 @@ Run a standalone command (argv vector) in the server’s sandbox without creatin Notes: - Empty `command` arrays are rejected. -- `sandboxPolicy` accepts the same shape used by `turn/start` (e.g., `dangerFullAccess`, `readOnly`, `workspaceWrite` with flags, `externalSandbox` with `networkAccess` `restricted|enabled`). +- `sandboxPolicy` accepts the same shape used by `turn/start` (e.g., `dangerFullAccess`, `readOnly`, `workspaceWrite` with flags including optional `readAccess`, `externalSandbox` with `networkAccess` `restricted|enabled`). - When omitted, `timeoutMs` falls back to the server default. ## Events 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..b09710f630 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 @@ -447,6 +447,7 @@ async fn test_send_user_turn_updates_sandbox_and_cwd_between_turns() -> Result<( network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }, model: model.clone(), effort: Some(ReasoningEffort::Medium), diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index 395561f69a..f09d4c4523 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -1102,6 +1102,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }), model: Some("mock-model".to_string()), effort: Some(ReasoningEffort::Medium), diff --git a/codex-rs/common/src/sandbox_summary.rs b/codex-rs/common/src/sandbox_summary.rs index 45520b11a0..b19a783ad2 100644 --- a/codex-rs/common/src/sandbox_summary.rs +++ b/codex-rs/common/src/sandbox_summary.rs @@ -1,5 +1,6 @@ use codex_core::protocol::NetworkAccess; use codex_core::protocol::SandboxPolicy; +use codex_core::protocol::WorkspaceReadAccess; pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { match sandbox_policy { @@ -17,6 +18,7 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { network_access, exclude_tmpdir_env_var, exclude_slash_tmp, + read_access, } => { let mut summary = "workspace-write".to_string(); @@ -35,6 +37,18 @@ pub fn summarize_sandbox_policy(sandbox_policy: &SandboxPolicy) -> String { ); summary.push_str(&format!(" [{}]", writable_entries.join(", "))); + if let WorkspaceReadAccess::RestrictedReadAccess { readable_roots } = read_access { + summary.push_str(" (restricted read access"); + if !readable_roots.is_empty() { + let roots = readable_roots + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>() + .join(", "); + summary.push_str(&format!(": {roots}")); + } + summary.push(')'); + } if *network_access { summary.push_str(" (network access enabled)"); } @@ -74,6 +88,7 @@ mod tests { network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }); assert_eq!( summary, @@ -83,4 +98,29 @@ mod tests { ) ); } + + #[test] + fn workspace_write_summary_includes_restricted_read_access() { + let read_root = if cfg!(windows) { + AbsolutePathBuf::try_from("C:\\read").unwrap() + } else { + AbsolutePathBuf::try_from("/read").unwrap() + }; + let summary = summarize_sandbox_policy(&SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + read_access: WorkspaceReadAccess::RestrictedReadAccess { + readable_roots: vec![read_root.clone()], + }, + }); + assert_eq!( + summary, + format!( + "workspace-write [workdir] (restricted read access: {})", + read_root.to_string_lossy() + ) + ); + } } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 7de6f00ea8..87b096ed7a 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -1175,6 +1175,7 @@ impl ConfigToml { network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, + read_access: Default::default(), }, None => SandboxPolicy::new_workspace_write_policy(), }, @@ -2092,6 +2093,7 @@ exclude_slash_tmp = true network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }, forced_auto_mode_downgraded_on_windows: false, } @@ -2142,6 +2144,7 @@ trust_level = "trusted" network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }, forced_auto_mode_downgraded_on_windows: false, } diff --git a/codex-rs/core/src/config_loader/config_requirements.rs b/codex-rs/core/src/config_loader/config_requirements.rs index b3e6043288..aa5dcfa097 100644 --- a/codex-rs/core/src/config_loader/config_requirements.rs +++ b/codex-rs/core/src/config_loader/config_requirements.rs @@ -715,6 +715,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }) .is_ok() ); diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index d68093c30f..c1f2195695 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -415,6 +415,7 @@ allowed_sandbox_modes = ["read-only"] network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }) .is_err() ); diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 47a12e029e..cb762d6db3 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -198,6 +198,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; assert!(is_write_patch_constrained_to_writable_paths( @@ -219,6 +220,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; assert!(is_write_patch_constrained_to_writable_paths( &add_outside, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index a15ebb177b..99a2edccef 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -13,6 +13,8 @@ use crate::spawn::spawn_child_async; const MACOS_SEATBELT_BASE_POLICY: &str = include_str!("seatbelt_base_policy.sbpl"); const MACOS_SEATBELT_NETWORK_POLICY: &str = include_str!("seatbelt_network_policy.sbpl"); +const MACOS_SEATBELT_RESTRICTED_READ_BASE_POLICY: &str = + include_str!("seatbelt_restricted_read_base_policy.sbpl"); /// When working with `sandbox-exec`, only consider `sandbox-exec` in `/usr/bin` /// to defend against an attacker trying to inject a malicious version on the @@ -48,6 +50,8 @@ pub(crate) fn create_seatbelt_command_args( sandbox_policy: &SandboxPolicy, sandbox_policy_cwd: &Path, ) -> Vec { + let has_full_disk_read_access = sandbox_policy.has_full_disk_read_access(); + let (file_write_policy, file_write_dir_params) = { if sandbox_policy.has_full_disk_write_access() { // Allegedly, this is more permissive than `(allow file-write*)`. @@ -105,10 +109,39 @@ 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 has_full_disk_read_access { + ( + "; allow read-only file operations\n(allow file-read*)".to_string(), + Vec::new(), + ) } else { + let readable_roots = sandbox_policy.get_readable_roots_with_cwd(sandbox_policy_cwd); + let mut readable_folder_policies: Vec = Vec::new(); + let mut file_read_params = Vec::new(); + + for (index, root) in readable_roots.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_folder_policies.push(format!( + "(allow file-read* file-map-executable (subpath (param \"{root_param}\")))" + )); + readable_folder_policies.push(format!( + "(allow file-read-metadata file-test-existence (path-ancestors (param \"{root_param}\")))" + )); + } + + (readable_folder_policies.join("\n"), file_read_params) + }; + + let restricted_read_base_policy = if has_full_disk_read_access { "" + } else { + MACOS_SEATBELT_RESTRICTED_READ_BASE_POLICY }; // TODO(mbolin): apply_patch calls must also honor the SandboxPolicy. @@ -118,11 +151,24 @@ pub(crate) fn create_seatbelt_command_args( "" }; - let full_policy = format!( - "{MACOS_SEATBELT_BASE_POLICY}\n{file_read_policy}\n{file_write_policy}\n{network_policy}" - ); + let full_policy = [ + MACOS_SEATBELT_BASE_POLICY, + restricted_read_base_policy, + file_read_policy.as_str(), + file_write_policy.as_str(), + network_policy, + ] + .into_iter() + .filter(|policy| !policy.is_empty()) + .collect::>() + .join("\n"); - let dir_params = [file_write_dir_params, macos_dir_params()].concat(); + let dir_params = [ + file_write_dir_params, + file_read_dir_params, + macos_dir_params(), + ] + .concat(); let mut seatbelt_args: Vec = vec!["-p".to_string(), full_policy]; let definition_args = dir_params @@ -166,6 +212,7 @@ mod tests { use super::create_seatbelt_command_args; use super::macos_dir_params; use crate::protocol::SandboxPolicy; + use crate::protocol::WorkspaceReadAccess; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use pretty_assertions::assert_eq; use std::fs; @@ -210,6 +257,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; // Create the Seatbelt command to wrap a shell command that tries to @@ -240,8 +288,7 @@ mod tests { (allow file-read*) (allow file-write* (require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ) (subpath (param "WRITABLE_ROOT_1")) (subpath (param "WRITABLE_ROOT_2")) -) -"#, +)"#, ); let mut expected_args = vec![ @@ -371,6 +418,48 @@ mod tests { ); } + #[test] + fn create_seatbelt_args_with_restricted_read_roots() { + let tmp = TempDir::new().expect("tempdir"); + let cwd = tmp.path().join("cwd"); + fs::create_dir_all(&cwd).expect("create cwd"); + let readable_root = tmp.path().join("readable"); + fs::create_dir_all(&readable_root).expect("create readable root"); + let readable_root_canonical = readable_root.canonicalize().expect("canonicalize root"); + + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + read_access: WorkspaceReadAccess::RestrictedReadAccess { + readable_roots: vec![readable_root.try_into().expect("absolute path")], + }, + }; + + let args = create_seatbelt_command_args(vec!["/usr/bin/true".to_string()], &policy, &cwd); + let policy_text = args + .get(1) + .expect("seatbelt policy arg should be present") + .to_string(); + + assert!( + policy_text.contains("(allow file-map-executable"), + "restricted read baseline should include file-map-executable rules" + ); + assert!( + policy_text.contains("(param \"READABLE_ROOT_0\")"), + "restricted read policy should include parameterized readable roots" + ); + assert!( + args.contains(&format!( + "-DREADABLE_ROOT_0={}", + readable_root_canonical.to_string_lossy() + )), + "expected readable root parameter in args: {args:?}" + ); + } + #[test] fn create_seatbelt_args_with_read_only_git_pointer_file() { let tmp = TempDir::new().expect("tempdir"); @@ -394,6 +483,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; let shell_command: Vec = [ @@ -477,6 +567,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; let shell_command: Vec = [ @@ -518,8 +609,7 @@ mod tests { (allow file-read*) (allow file-write* (require-all (subpath (param "WRITABLE_ROOT_0")) (require-not (subpath (param "WRITABLE_ROOT_0_RO_0"))) (require-not (subpath (param "WRITABLE_ROOT_0_RO_1"))) ) (subpath (param "WRITABLE_ROOT_1")){tempdir_policy_entry} -) -"#, +)"#, ); let mut expected_args = vec![ diff --git a/codex-rs/core/src/seatbelt_restricted_read_base_policy.sbpl b/codex-rs/core/src/seatbelt_restricted_read_base_policy.sbpl new file mode 100644 index 0000000000..4e9a23010d --- /dev/null +++ b/codex-rs/core/src/seatbelt_restricted_read_base_policy.sbpl @@ -0,0 +1,49 @@ +; baseline read access for restricted-read workspace-write policies +; keeps the runtime and dynamic loader functional without globally allowing +; file-read* on all paths. + +; shell/runtime binaries +(allow file-read-data file-read-metadata + (subpath "/bin") + (subpath "/sbin") + (subpath "/usr/bin") + (subpath "/usr/sbin") + (subpath "/usr/libexec")) + +; standard shell configuration loaded by /bin/zsh +(allow file-read-data file-read-metadata + (literal "/etc/zshenv") + (literal "/etc/zprofile") + (literal "/etc/zlogin")) + +; dynamic loader + frameworks +(allow file-read* file-test-existence + (subpath "/Library/Apple") + (subpath "/System") + (subpath "/usr/lib") + (subpath "/usr/share")) + +(allow file-map-executable + (subpath "/Library/Apple/System/Library/Frameworks") + (subpath "/Library/Apple/System/Library/PrivateFrameworks") + (subpath "/Library/Apple/usr/lib") + (subpath "/System/Library/Frameworks") + (subpath "/System/Library/PrivateFrameworks") + (subpath "/System/Library/SubFrameworks") + (subpath "/usr/lib")) + +; allow path traversal for common symlinked roots +(allow file-read-metadata file-test-existence + (literal "/") + (literal "/etc") + (literal "/tmp") + (literal "/var") + (literal "/private/etc/localtime") + (path-ancestors "/System/Volumes/Data/private")) + +; resolver / service metadata commonly read at startup +(allow file-read* file-test-existence + (literal "/private/etc/master.passwd") + (literal "/private/etc/passwd") + (literal "/private/etc/protocols") + (literal "/private/etc/services")) diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index ccfa9fe654..7ddbbab5ed 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -581,6 +581,7 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace( network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; harness .submit_with_policy( @@ -637,6 +638,7 @@ async fn apply_patch_cli_rejects_move_path_traversal_outside_workspace( network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; harness .submit_with_policy("attempt move traversal via apply_patch", sandbox_policy) diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 1b295964e8..df994fcf73 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -631,6 +631,7 @@ fn scenarios() -> Vec { network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; vec![ @@ -1576,6 +1577,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; let sandbox_policy_for_config = sandbox_policy.clone(); diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index fc1f0fa0b6..580a39f6dc 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -460,6 +460,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; let sandbox_policy_for_config = sandbox_policy.clone(); diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 9b1548591b..69eea847cc 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -377,6 +377,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; codex .submit(Op::OverrideTurnContext { @@ -618,6 +619,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; codex .submit(Op::UserTurn { diff --git a/codex-rs/core/tests/suite/seatbelt.rs b/codex-rs/core/tests/suite/seatbelt.rs index 286bc8791b..eff95f4a4e 100644 --- a/codex-rs/core/tests/suite/seatbelt.rs +++ b/codex-rs/core/tests/suite/seatbelt.rs @@ -80,6 +80,7 @@ async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; test_scenario @@ -106,6 +107,7 @@ async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; test_scenario diff --git a/codex-rs/exec-server/tests/common/lib.rs b/codex-rs/exec-server/tests/common/lib.rs index 66bb4b7f9c..b6da9a373a 100644 --- a/codex-rs/exec-server/tests/common/lib.rs +++ b/codex-rs/exec-server/tests/common/lib.rs @@ -116,6 +116,7 @@ where // strict about what is writable. exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }, codex_linux_sandbox_exe, sandbox_cwd: writable_folder.as_ref().to_path_buf(), diff --git a/codex-rs/exec/tests/suite/sandbox.rs b/codex-rs/exec/tests/suite/sandbox.rs index ab8d3868d9..b15b7ff902 100644 --- a/codex-rs/exec/tests/suite/sandbox.rs +++ b/codex-rs/exec/tests/suite/sandbox.rs @@ -135,6 +135,7 @@ async fn python_multiprocessing_lock_works_under_sandbox() { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; let python_code = r#"import multiprocessing @@ -248,6 +249,7 @@ async fn sandbox_distinguishes_command_and_policy_cwds() { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; // Attempt to write inside the command cwd, which is outside of the sandbox policy cwd. diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index 52cd402a13..c8ce98243a 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -92,6 +92,7 @@ async fn run_cmd_result_with_writable_roots( // writing to in the sandbox. exclude_tmpdir_env_var: true, exclude_slash_tmp: true, + read_access: Default::default(), }; let sandbox_program = env!("CARGO_BIN_EXE_codex-linux-sandbox"); let codex_linux_sandbox_exe = Some(PathBuf::from(sandbox_program)); diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index ae38a7890c..8730453d9a 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1216,6 +1216,7 @@ mod tests { network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; let instructions = DeveloperInstructions::from_policy( diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 078ea31d18..9a101e411f 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -376,6 +376,29 @@ impl NetworkAccess { } } +/// Controls read access semantics for `workspace-write` sandbox policies. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, Default, JsonSchema, TS)] +#[strum(serialize_all = "kebab-case")] +#[serde(tag = "type", rename_all = "kebab-case")] +pub enum WorkspaceReadAccess { + /// Preserve current behavior where all file-system paths are readable. + #[default] + FullReadAccess, + + /// Restrict reads to an explicit allowlist plus implicitly readable paths. + RestrictedReadAccess { + /// Additional folders that should be readable from inside the sandbox. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + readable_roots: Vec, + }, +} + +impl WorkspaceReadAccess { + pub fn is_full(&self) -> bool { + matches!(self, WorkspaceReadAccess::FullReadAccess) + } +} + /// Determines execution restrictions for model shell commands. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)] #[strum(serialize_all = "kebab-case")] @@ -422,6 +445,11 @@ pub enum SandboxPolicy { /// writable roots on UNIX. Defaults to `false`. #[serde(default)] exclude_slash_tmp: bool, + + /// Controls whether the workspace-write policy has full read access or + /// an explicit read allowlist. + #[serde(default, skip_serializing_if = "WorkspaceReadAccess::is_full")] + read_access: WorkspaceReadAccess, }, } @@ -479,12 +507,48 @@ impl SandboxPolicy { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: WorkspaceReadAccess::FullReadAccess, } } - /// 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 => true, + SandboxPolicy::WorkspaceWrite { read_access, .. } => { + matches!(read_access, WorkspaceReadAccess::FullReadAccess) + } + } + } + + /// Returns readable roots tailored to cwd when read access is restricted. + /// Returns an empty list when the policy has full disk read access. + pub fn get_readable_roots_with_cwd(&self, cwd: &Path) -> Vec { + match self { + SandboxPolicy::DangerFullAccess => Vec::new(), + SandboxPolicy::ExternalSandbox { .. } => Vec::new(), + SandboxPolicy::ReadOnly => Vec::new(), + SandboxPolicy::WorkspaceWrite { read_access, .. } => match read_access { + WorkspaceReadAccess::FullReadAccess => Vec::new(), + WorkspaceReadAccess::RestrictedReadAccess { readable_roots } => { + let mut roots = readable_roots.clone(); + roots.extend( + self.get_writable_roots_with_cwd(cwd) + .into_iter() + .map(|root| root.root), + ); + + let mut deduped = Vec::new(); + for root in roots { + if !deduped.iter().any(|existing| existing == &root) { + deduped.push(root); + } + } + deduped + } + }, + } } pub fn has_full_disk_write_access(&self) -> bool { @@ -517,6 +581,7 @@ impl SandboxPolicy { writable_roots, exclude_tmpdir_env_var, exclude_slash_tmp, + read_access: _, network_access: _, } => { // Start from explicitly configured writable roots. @@ -2482,6 +2547,7 @@ mod tests { use crate::items::UserMessageItem; use crate::items::WebSearchItem; use anyhow::Result; + use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use serde_json::json; use tempfile::NamedTempFile; @@ -2501,6 +2567,52 @@ mod tests { assert!(enabled.has_full_network_access()); } + #[test] + fn workspace_write_restricted_read_reports_not_full_read_access() { + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + read_access: WorkspaceReadAccess::RestrictedReadAccess { + readable_roots: vec![], + }, + }; + + assert!(!policy.has_full_disk_read_access()); + } + + #[test] + fn workspace_write_restricted_readable_roots_include_writable_roots() -> Result<()> { + let (cwd, writable_root, readable_root) = if cfg!(windows) { + ( + AbsolutePathBuf::from_absolute_path("C:\\repo\\cwd")?, + AbsolutePathBuf::from_absolute_path("C:\\repo\\writable")?, + AbsolutePathBuf::from_absolute_path("C:\\repo\\readable")?, + ) + } else { + ( + AbsolutePathBuf::from_absolute_path("/repo/cwd")?, + AbsolutePathBuf::from_absolute_path("/repo/writable")?, + AbsolutePathBuf::from_absolute_path("/repo/readable")?, + ) + }; + + let policy = SandboxPolicy::WorkspaceWrite { + writable_roots: vec![writable_root.clone()], + network_access: false, + exclude_tmpdir_env_var: true, + exclude_slash_tmp: true, + read_access: WorkspaceReadAccess::RestrictedReadAccess { + readable_roots: vec![readable_root.clone()], + }, + }; + + let readable_roots = policy.get_readable_roots_with_cwd(cwd.as_path()); + assert_eq!(readable_roots, vec![readable_root, writable_root, cwd]); + Ok(()) + } + #[test] fn item_started_event_from_web_search_emits_begin_event() { let event = ItemStartedEvent { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index f49493b6a1..a167f6c2df 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3490,6 +3490,7 @@ async fn preset_matching_ignores_extra_writable_roots() { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; assert!( diff --git a/codex-rs/tui/src/status/tests.rs b/codex-rs/tui/src/status/tests.rs index c8844904ab..0835e387da 100644 --- a/codex-rs/tui/src/status/tests.rs +++ b/codex-rs/tui/src/status/tests.rs @@ -103,6 +103,7 @@ async fn status_snapshot_includes_reasoning_details() { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }) .expect("set sandbox policy"); diff --git a/codex-rs/windows-sandbox-rs/src/allow.rs b/codex-rs/windows-sandbox-rs/src/allow.rs index 83d72f7e55..91f96e277e 100644 --- a/codex-rs/windows-sandbox-rs/src/allow.rs +++ b/codex-rs/windows-sandbox-rs/src/allow.rs @@ -111,6 +111,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), }; let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); @@ -137,6 +138,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, + read_access: Default::default(), }; let mut env_map = HashMap::new(); env_map.insert("TEMP".into(), temp_dir.to_string_lossy().to_string()); @@ -164,6 +166,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, + read_access: Default::default(), }; let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); @@ -191,6 +194,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, + read_access: Default::default(), }; let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); @@ -216,6 +220,7 @@ mod tests { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false, + read_access: Default::default(), }; let paths = compute_allow_paths(&policy, &command_cwd, &command_cwd, &HashMap::new()); diff --git a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs index ecd5732c09..463e98efa6 100644 --- a/codex-rs/windows-sandbox-rs/src/elevated_impl.rs +++ b/codex-rs/windows-sandbox-rs/src/elevated_impl.rs @@ -472,6 +472,7 @@ mod windows_impl { network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), } } diff --git a/codex-rs/windows-sandbox-rs/src/lib.rs b/codex-rs/windows-sandbox-rs/src/lib.rs index 4a97efaaf8..e681a7819d 100644 --- a/codex-rs/windows-sandbox-rs/src/lib.rs +++ b/codex-rs/windows-sandbox-rs/src/lib.rs @@ -513,6 +513,7 @@ mod windows_impl { network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, + read_access: Default::default(), } } diff --git a/skyshield_base_policy.txt b/skyshield_base_policy.txt new file mode 100644 index 0000000000..5fe713fe7b --- /dev/null +++ b/skyshield_base_policy.txt @@ -0,0 +1,351 @@ +private func baseSandboxPolicy( + temporaryDirectory: URL, + allowedReadFolders: [URL], + allowedWriteFolders: [URL], + sandboxPolicyOptions: TinyskySandboxPolicyOptions?, +) -> String { + // Sandbox policies require fully-resolved paths + var temporaryPath = temporaryDirectory.resolvingSymlinksInPath().path + + // Per documentation, URL.resolveSymlinksInPath() strips /private from /var paths; we re-add it + if temporaryPath.hasPrefix("/var") { + temporaryPath = "/private" + temporaryDirectory.path + } + + let allowedReadOnlyFolderRules = allowedReadOnlyFolderRules(for: allowedReadFolders) + let allowedWriteOnlyFolderRules = allowedWriteFolderRules(for: allowedWriteFolders) + let sandboxPolicyOptionsRules = rules(for: sandboxPolicyOptions) ?? "" + + return """ + (version 1) + + ; inspired by Chrome's sandbox policy: + ; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/common.sb;l=273-319;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd + ; https://source.chromium.org/chromium/chromium/src/+/main:sandbox/policy/mac/renderer.sb;l=64;drc=7b3962fe2e5fc9e2ee58000dc8fbf3429d84d3bd + + ; start with closed-by-default + (deny default) + + ; Read access to standard system paths + (allow file-read* file-test-existence + (subpath "/Library/Apple") + (subpath "/Library/Filesystems/NetFSPlugins") + (subpath "/Library/Preferences/Logging") + (subpath "/System") + (literal "/private/var/db/DarwinDirectory/local/recordStore.data") + (subpath "/private/var/db/timezone") + (subpath "/usr/lib") + (subpath "/usr/share")) + + ; Map system frameworks + dylibs + (allow file-map-executable + (subpath "/Library/Apple/System/Library/Frameworks") + (subpath "/Library/Apple/System/Library/PrivateFrameworks") + (subpath "/Library/Apple/usr/lib") + (subpath "/System/Library/Extensions") + (subpath "/System/Library/Frameworks") + (subpath "/System/Library/PrivateFrameworks") + (subpath "/System/Library/SubFrameworks") + (subpath "/System/iOSSupport/System/Library/Frameworks") + (subpath "/System/iOSSupport/System/Library/PrivateFrameworks") + (subpath "/System/iOSSupport/System/Library/SubFrameworks") + (subpath "/usr/lib")) + + ; Allow guarded vnodes. + (allow system-mac-syscall (mac-policy-name "vnguard")) + + ; Determine whether a container is expected. + (allow system-mac-syscall + (require-all + (mac-policy-name "Sandbox") + (mac-syscall-number 67))) + + ; Allow resolution of standard system symlinks. + (allow file-read-metadata file-test-existence + (literal "/etc") + (literal "/tmp") + (literal "/var") + (literal "/private/etc/localtime")) + + ; Allow stat'ing of path components of firmlink targets. + (allow file-read-metadata file-test-existence + (path-ancestors "/System/Volumes/Data/private")) + + ; Allow processes to get their current working directory. + (allow file-read* file-test-existence + (literal "/")) + + ; Allow FSIOC_CAS_BSDFLAGS as an alternate chflags(2). + (allow system-fsctl (fsctl-command FSIOC_CAS_BSDFLAGS)) + + ; Allow access to standard special files. + (allow file-read* file-test-existence + (literal "/dev/autofs_nowait") + (literal "/dev/random") + (literal "/dev/urandom") + (literal "/private/etc/master.passwd") + (literal "/private/etc/passwd") + (literal "/private/etc/protocols") + (literal "/private/etc/services")) + + (allow file-read* file-test-existence file-write-data + (literal "/dev/null") + (literal "/dev/zero")) + + ; Allow read/write access to the file descriptors. + (allow file-read-data file-test-existence file-write-data + (subpath "/dev/fd")) + + (allow file-read* file-test-existence file-write-data file-ioctl + (literal "/dev/dtracehelper")) + + ; Regulatory domain support + (allow file-read* + (literal "/private/var/db/eligibilityd/eligibility.plist")) + + ; Allow IPC to standard system agents. + (allow network-outbound + (literal "/private/var/run/syslog")) + + (allow ipc-posix-shm-read* + (ipc-posix-name "apple.shm.notification_center") + (ipc-posix-name-prefix "apple.cfprefs.")) + + (allow mach-lookup + (global-name "com.apple.analyticsd") + (global-name "com.apple.analyticsd.messagetracer") + (global-name "com.apple.appsleep") + (global-name "com.apple.bsd.dirhelper") + (global-name "com.apple.cfprefsd.agent") + (global-name "com.apple.cfprefsd.daemon") + (global-name "com.apple.diagnosticd") + (global-name "com.apple.dt.automationmode.reader") + (global-name "com.apple.espd") + (global-name "com.apple.logd") + (global-name "com.apple.logd.events") + (global-name "com.apple.runningboard") + (global-name "com.apple.secinitd") + (global-name "com.apple.system.DirectoryService.libinfo_v1") + (global-name "com.apple.system.logger") + (global-name "com.apple.system.notification_center") + (global-name "com.apple.system.opendirectoryd.libinfo") + (global-name "com.apple.system.opendirectoryd.membership") + (global-name "com.apple.trustd") + (global-name "com.apple.trustd.agent") + (global-name "com.apple.xpc.activity.unmanaged") + (local-name "com.apple.cfprefsd.agent")) + + ; Allow mostly harmless operations. + (allow sysctl-read) + (allow sysctl-write + (sysctl-name "kern.grade_cputype" + "kern.wq_limit_cooperative_threads")) + + ; (system-graphics) + (define (system-graphics) + (allow user-preference-read + (preference-domain "com.apple.gpu") + (preference-domain "com.apple.opengl") + (preference-domain "com.nvidia.OpenGL")) + (allow mach-lookup + (global-name "com.apple.gpumemd.source")) + (allow mach-lookup + (global-name "com.apple.lsd.mapdb")) + (allow mach-lookup + (global-name "com.apple.CARenderServer") + (global-name "com.apple.CoreDisplay.master") + (global-name "com.apple.CoreDisplay.Notification")) + (allow mach-lookup + (global-name "com.apple.cvmsServ")) + (allow file-read* + (subpath "/private/var/db/CVMS")) + (allow iokit-open-service + (iokit-registry-entry-class "IOAccelerator" + "IOSurfaceRoot")) + (allow iokit-open-user-client + (iokit-connection "IOAccelerator") + (iokit-user-client-class "IOAccelerationUserClient" + "IOSurfaceAcceleratorClient" + "IOSurfaceRootUserClient" + "IOSurfaceSendRight")) + (allow iokit-open-service + (iokit-registry-entry-class "IOFramebuffer")) + (allow iokit-open-user-client + (iokit-user-client-class "IOFramebufferSharedUserClient")) + (allow iokit-open-service + (iokit-connection "AppleGraphicsDeviceControl")) + (allow iokit-open-user-client + (iokit-user-client-class "AppleIntelMEUserClient" + "AppleSNBFBUserClient")) + (allow iokit-open-service + (iokit-registry-entry-class "AGPM" + "AppleGraphicsControl" + "AppleGraphicsPolicy")) + (allow iokit-open-user-client + (iokit-user-client-class "AGPMClient" + "AppleGraphicsControlClient" + "AppleGraphicsPolicyClient")) + (allow iokit-open-user-client + (iokit-user-client-class "AppleMGPUPowerControlClient")) + (allow file-read* file-test-existence + (subpath "/Library/GPUBundles")) + (allow iokit-set-properties + (require-all + (iokit-connection "IODisplay") + (require-any + (iokit-property "brightness" + "linear-brightness" + "commit" + "rgcs" + "ggcs" + "bgcs"))))) + + ; OOPJIT support + (define (oopjit-runner) + (allow file-read* file-map-executable file-write-unlink + (extension "com.apple.sandbox.oopjit"))) + + ; child processes inherit the policy of their parent + (allow process-exec) + (allow process-fork) + (allow signal (target same-sandbox)) + + ; Allow cf prefs to work. + (allow user-preference-read) + + ; process-info + (allow process-info* (target same-sandbox)) + + (allow file-write-data + (require-all + (path "/dev/null") + (vnode-type CHARACTER-DEVICE))) + + ; --- Allow reading the minimum system runtime so exec works --- + (allow file-read-data (subpath "/bin")) + (allow file-read-metadata (subpath "/bin")) + (allow file-read-data (subpath "/sbin")) + (allow file-read-metadata (subpath "/sbin")) + (allow file-read-data (subpath "/usr/bin")) + (allow file-read-metadata (subpath "/usr/bin")) + (allow file-read-data (subpath "/usr/sbin")) + (allow file-read-metadata (subpath "/usr/sbin")) + (allow file-read-data (subpath "/usr/libexec")) + (allow file-read-metadata (subpath "/usr/libexec")) + + ; zsh system config + (allow file-read-data (literal "/etc/zshenv")) + (allow file-read-metadata (literal "/etc/zshenv")) + (allow file-read-data (literal "/etc/zprofile")) + (allow file-read-metadata (literal "/etc/zprofile")) + (allow file-read-data (literal "/etc/zlogin")) + (allow file-read-metadata (literal "/etc/zlogin")) + + (allow file-read* (subpath "/Library/Preferences")) + (allow file-read* (subpath "/var/db")) + (allow file-read* (subpath "/private/var/db")) + + ; dyld cache metadata outside /System + (allow file-read* (subpath "/private/var/db/dyld")) + (allow file-read* (subpath "/var/db/dyld")) + + ; common 3rd-party dylib / framework locations + ; Homebrew + (allow file-read* (subpath "/opt/homebrew/lib")) + (allow file-read* (subpath "/usr/local/lib")) + + ; App bundles + (allow file-read* (subpath "/Applications")) + + ; terminal basics + (allow file-read* (regex "^/dev/fd/(0|1|2)$")) + (allow file-write* (regex "^/dev/fd/(1|2)$")) + (allow file-read* file-write* (literal "/dev/null")) + (allow file-read* file-write* (literal "/dev/tty")) + (allow file-read-metadata (literal "/dev")) + (allow file-read-metadata (regex "^/dev/.*$")) + (allow file-read-metadata (literal "/dev/stdin")) + (allow file-read-metadata (literal "/dev/stdout")) + (allow file-read-metadata (literal "/dev/stderr")) + (allow file-read-metadata (regex "^/dev/tty[^/]*$")) + (allow file-read-metadata (regex "^/dev/pty[^/]*$")) + (allow file-read* file-write* (regex "^/dev/ttys[0-9]+$")) + (allow file-read* file-write* (literal "/dev/ptmx")) + + ; scratch space (so tools can create temp files) + (allow file-read* file-write* (subpath "/tmp")) + (allow file-read* file-write* (subpath "/private/tmp")) + (allow file-read* file-write* (subpath "/var/tmp")) + (allow file-read* file-write* (subpath "/private/var/tmp")) + + (allow file-read* (subpath "/etc")) + (allow file-read* (subpath "/private/etc")) + + ; Some processes read /var metadata during startup + (allow file-read-metadata (subpath "/var")) + (allow file-read-metadata (subpath "/private/var")) + + ; IOKit + (allow iokit-open + (iokit-registry-entry-class "RootDomainUserClient") + ) + + ; needed to look up user info, see https://crbug.com/792228 + (allow mach-lookup + (global-name "com.apple.system.opendirectoryd.libinfo") + ) + + ; Unified logging (os_log) needs logd + (allow mach-lookup + (global-name-prefix "com.apple.logd") + (global-name "com.apple.system.logger") + ) + + ; Diagnostics daemon (sometimes queried on startup) + (allow mach-lookup + (global-name "com.apple.diagnosticd") + (global-name-prefix "com.apple.diagnosticd") + ) + + ; Needed for python multiprocessing on MacOS for the SemLock + (allow ipc-posix-sem) + + (allow mach-lookup + (global-name "com.apple.PowerManagement.control") + ) + + ; allow openpty() + (allow pseudo-tty) + (allow file-read* file-write* file-ioctl (literal "/dev/ptmx")) + (allow file-read* file-write* + (require-all + (regex #"^/dev/ttys[0-9]+"))) + ; PTYs created before entering seatbelt may lack the extension; allow ioctl + ; on those slave ttys so interactive shells detect a TTY and remain functional. + (allow file-ioctl (regex #"^/dev/ttys[0-9]+")) + + (allow mach-lookup (global-name "com.apple.audio.audiohald")) + (allow mach-lookup (global-name "com.apple.audio.AudioComponentRegistrar")) + (allow file-read-data (subpath "/etc")) + (allow file-read-metadata (subpath "/etc")) + (allow file-read-data (subpath "/usr")) + (allow file-read-metadata (subpath "/usr")) + + ; allow metadata traversal for firmlink parents + (allow file-read-metadata (literal "/System/Volumes") (vnode-type DIRECTORY)) + (allow file-read-metadata (literal "/System/Volumes/Data") (vnode-type DIRECTORY)) + (allow file-read-metadata (literal "/System/Volumes/Data/Users") (vnode-type DIRECTORY)) + + \(allowedReadOnlyFolderRules) + \(allowedWriteOnlyFolderRules) + \(sandboxPolicyOptionsRules) + (allow file-read* file-write* (subpath "\(temporaryPath)")) + + (allow mach-lookup (global-name "\(SkyShieldCommandProxyIPCServer.messagePortName)")) + (allow mach-lookup (global-name "\(SkyShieldSandboxExtensionIPCServer.messagePortName)")) + + ; Allow app-sandbox file extensions to grant access + (allow file-read* (extension "com.apple.app-sandbox.read")) + (allow file-read* file-write* (extension "com.apple.app-sandbox.read-write")) + """ +}