From eb04235b41d92a9b3ae3cfcfee73eab0e6024461 Mon Sep 17 00:00:00 2001 From: viyatb-oai Date: Sat, 7 Mar 2026 15:07:15 -0800 Subject: [PATCH] Codex worktree snapshot: new-branch-cleanup Co-authored-by: Codex --- codex-rs/Cargo.lock | 1 + .../schema/json/ClientRequest.json | 12 +- .../schema/json/EventMsg.json | 18 +- .../codex_app_server_protocol.schemas.json | 12 +- .../schema/json/v2/CommandExecParams.json | 12 +- .../schema/json/v2/ThreadForkResponse.json | 12 +- .../schema/json/v2/ThreadResumeResponse.json | 12 +- .../schema/json/v2/ThreadStartResponse.json | 12 +- .../schema/json/v2/TurnStartParams.json | 12 +- .../schema/typescript/DenyReadPattern.ts | 5 + .../schema/typescript/SandboxPolicy.ts | 13 +- .../schema/typescript/index.ts | 1 + .../schema/typescript/v2/DenyReadPattern.ts | 5 + .../schema/typescript/v2/SandboxPolicy.ts | 3 +- .../schema/typescript/v2/index.ts | 1 + .../app-server-protocol/src/protocol/v2.rs | 78 +++-- codex-rs/config/src/config_requirements.rs | 8 +- codex-rs/core/Cargo.toml | 1 + codex-rs/core/src/config/mod.rs | 71 +++-- codex-rs/core/src/config_loader/tests.rs | 2 +- codex-rs/core/src/environment_context.rs | 58 ++-- codex-rs/core/src/exec.rs | 3 +- codex-rs/core/src/exec_policy.rs | 6 +- codex-rs/core/src/filesystem_deny_read.rs | 282 +++++++----------- codex-rs/core/src/memories/phase2.rs | 2 +- codex-rs/core/src/safety.rs | 10 +- codex-rs/core/src/sandbox_tags.rs | 6 +- codex-rs/core/src/sandboxing/mod.rs | 20 +- codex-rs/core/src/seatbelt.rs | 212 ++++++++++++- codex-rs/core/src/skills/loader.rs | 2 +- codex-rs/core/src/skills/permissions.rs | 8 +- codex-rs/core/src/tools/handlers/list_dir.rs | 14 +- codex-rs/core/src/tools/handlers/mod.rs | 11 +- codex-rs/core/src/tools/orchestrator.rs | 2 +- .../runtimes/shell/unix_escalation_tests.rs | 2 +- codex-rs/core/src/tools/sandboxing.rs | 2 +- codex-rs/core/tests/common/zsh_fork.rs | 2 +- codex-rs/core/tests/suite/apply_patch_cli.rs | 4 +- codex-rs/core/tests/suite/approvals.rs | 6 +- .../core/tests/suite/permissions_messages.rs | 2 +- codex-rs/core/tests/suite/prompt_caching.rs | 4 +- .../core/tests/suite/request_permissions.rs | 2 +- codex-rs/core/tests/suite/seatbelt.rs | 4 +- codex-rs/linux-sandbox/src/bwrap.rs | 44 +-- codex-rs/linux-sandbox/src/landlock.rs | 4 +- codex-rs/linux-sandbox/src/linux_run_main.rs | 2 +- .../linux-sandbox/tests/suite/landlock.rs | 2 +- codex-rs/protocol/src/models.rs | 2 +- codex-rs/protocol/src/protocol.rs | 113 ++++--- 49 files changed, 683 insertions(+), 439 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/DenyReadPattern.ts create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/DenyReadPattern.ts diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2ab1d5943c..16b9381bd1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1792,6 +1792,7 @@ dependencies = [ "env-flags", "eventsource-stream", "futures", + "globset", "http 1.4.0", "iana-time-zone", "image", diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 63e48c5037..4e522e9b48 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -1618,10 +1618,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -1641,10 +1641,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -1672,10 +1672,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/json/EventMsg.json b/codex-rs/app-server-protocol/schema/json/EventMsg.json index 1198708345..e869261197 100644 --- a/codex-rs/app-server-protocol/schema/json/EventMsg.json +++ b/codex-rs/app-server-protocol/schema/json/EventMsg.json @@ -5261,10 +5261,10 @@ ], "description": "Read access granted while running under this policy." }, - "deny_read_paths": { - "description": "Paths that must not be readable, even when broad read access is otherwise allowed.", + "deny_read_patterns": { + "description": "Patterns that must not be readable, even when broad read access is otherwise allowed.", "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -5285,10 +5285,10 @@ { "description": "Indicates the process is already in an external sandbox. Allows full disk access while honoring the provided network setting.", "properties": { - "deny_read_paths": { - "description": "Paths that must not be readable, even when broad read access is otherwise allowed.", + "deny_read_patterns": { + "description": "Patterns that must not be readable, even when broad read access is otherwise allowed.", "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -5318,10 +5318,10 @@ { "description": "Same as `ReadOnly` but additionally grants write access to the current working directory (\"workspace\").", "properties": { - "deny_read_paths": { - "description": "Paths that must not be readable, even when broad read access is otherwise allowed.", + "deny_read_patterns": { + "description": "Patterns that must not be readable, even when broad read access is otherwise allowed.", "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, 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 321859456d..d49e67f1be 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 @@ -11782,10 +11782,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -11805,10 +11805,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -11836,10 +11836,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/v2/AbsolutePathBuf" + "type": "string" }, "type": "array" }, 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 9f9872ab0c..8f8a98b3a3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/CommandExecParams.json @@ -89,10 +89,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -112,10 +112,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -143,10 +143,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, 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 306974764b..c2c3f0b913 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -653,10 +653,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -676,10 +676,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -707,10 +707,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, 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 6927996e3d..89fca2078d 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -653,10 +653,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -676,10 +676,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -707,10 +707,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, 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 085ce8544b..90d2c1a630 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -653,10 +653,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -676,10 +676,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -707,10 +707,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, 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 73d628d35e..24abab295f 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartParams.json @@ -214,10 +214,10 @@ "type": "fullAccess" } }, - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -237,10 +237,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, @@ -268,10 +268,10 @@ }, { "properties": { - "denyReadPaths": { + "denyReadPatterns": { "default": [], "items": { - "$ref": "#/definitions/AbsolutePathBuf" + "type": "string" }, "type": "array" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/DenyReadPattern.ts b/codex-rs/app-server-protocol/schema/typescript/DenyReadPattern.ts new file mode 100644 index 0000000000..fc16ac93f2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/DenyReadPattern.ts @@ -0,0 +1,5 @@ +// 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. + +export type DenyReadPattern = string; diff --git a/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts index 26fd90be05..42b075899c 100644 --- a/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/SandboxPolicy.ts @@ -2,6 +2,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 { DenyReadPattern } from "./DenyReadPattern"; import type { NetworkAccess } from "./NetworkAccess"; import type { ReadOnlyAccess } from "./ReadOnlyAccess"; @@ -14,17 +15,17 @@ export type SandboxPolicy = { "type": "danger-full-access" } | { "type": "read-o */ access?: ReadOnlyAccess, /** - * Paths that must not be readable, even when broad read access is otherwise allowed. + * Patterns that must not be readable, even when broad read access is otherwise allowed. */ -deny_read_paths?: Array, } | { "type": "external-sandbox", +deny_read_patterns?: Array, } | { "type": "external-sandbox", /** * Whether the external sandbox permits outbound network traffic. */ network_access: NetworkAccess, /** - * Paths that must not be readable, even when broad read access is otherwise allowed. + * Patterns that must not be readable, even when broad read access is otherwise allowed. */ -deny_read_paths?: Array, } | { "type": "workspace-write", +deny_read_patterns?: Array, } | { "type": "workspace-write", /** * Additional folders (beyond cwd and possibly TMPDIR) that should be * writable from within the sandbox. @@ -35,9 +36,9 @@ writable_roots?: Array, */ read_only_access?: ReadOnlyAccess, /** - * Paths that must not be readable, even when broad read access is otherwise allowed. + * Patterns that must not be readable, even when broad read access is otherwise allowed. */ -deny_read_paths?: Array, +deny_read_patterns?: Array, /** * 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 bc5b0297b8..53edfec164 100644 --- a/codex-rs/app-server-protocol/schema/typescript/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/index.ts @@ -51,6 +51,7 @@ export type { ConversationGitInfo } from "./ConversationGitInfo"; export type { ConversationSummary } from "./ConversationSummary"; export type { CreditsSnapshot } from "./CreditsSnapshot"; export type { CustomPrompt } from "./CustomPrompt"; +export type { DenyReadPattern } from "./DenyReadPattern"; export type { DeprecationNoticeEvent } from "./DeprecationNoticeEvent"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallRequest } from "./DynamicToolCallRequest"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DenyReadPattern.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DenyReadPattern.ts new file mode 100644 index 0000000000..fc16ac93f2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/DenyReadPattern.ts @@ -0,0 +1,5 @@ +// 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. + +export type DenyReadPattern = string; 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 15866f2c35..d82b9e7278 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/SandboxPolicy.ts @@ -2,7 +2,8 @@ // 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 { DenyReadPattern } from "./DenyReadPattern"; import type { NetworkAccess } from "./NetworkAccess"; import type { ReadOnlyAccess } from "./ReadOnlyAccess"; -export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", access: ReadOnlyAccess, denyReadPaths: Array, } | { "type": "externalSandbox", networkAccess: NetworkAccess, denyReadPaths: Array, } | { "type": "workspaceWrite", writableRoots: Array, readOnlyAccess: ReadOnlyAccess, denyReadPaths: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; +export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", access: ReadOnlyAccess, denyReadPatterns: Array, } | { "type": "externalSandbox", networkAccess: NetworkAccess, denyReadPatterns: Array, } | { "type": "workspaceWrite", writableRoots: Array, readOnlyAccess: ReadOnlyAccess, denyReadPatterns: Array, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index 757eb72893..19ed1d45b2 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -58,6 +58,7 @@ export type { ConfigWarningNotification } from "./ConfigWarningNotification"; export type { ConfigWriteResponse } from "./ConfigWriteResponse"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; export type { CreditsSnapshot } from "./CreditsSnapshot"; +export type { DenyReadPattern } from "./DenyReadPattern"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index b52604e8d6..364d118216 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -42,6 +42,7 @@ use codex_protocol::protocol::AgentStatus as CoreAgentStatus; use codex_protocol::protocol::AskForApproval as CoreAskForApproval; use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo; use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot; +use codex_protocol::protocol::DenyReadPattern as CoreDenyReadPattern; use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus; use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason; use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess; @@ -936,6 +937,23 @@ impl From for ReadOnlyAccess { } } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema, TS)] +#[serde(transparent)] +#[ts(export_to = "v2/")] +pub struct DenyReadPattern(String); + +impl From for DenyReadPattern { + fn from(value: CoreDenyReadPattern) -> Self { + Self(value.as_str().to_string()) + } +} + +impl From for CoreDenyReadPattern { + fn from(value: DenyReadPattern) -> Self { + CoreDenyReadPattern::from(value.0) + } +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] @@ -948,7 +966,7 @@ pub enum SandboxPolicy { #[serde(default)] access: ReadOnlyAccess, #[serde(default)] - deny_read_paths: Vec, + deny_read_patterns: Vec, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -956,7 +974,7 @@ pub enum SandboxPolicy { #[serde(default)] network_access: NetworkAccess, #[serde(default)] - deny_read_paths: Vec, + deny_read_patterns: Vec, }, #[serde(rename_all = "camelCase")] #[ts(rename_all = "camelCase")] @@ -966,7 +984,7 @@ pub enum SandboxPolicy { #[serde(default)] read_only_access: ReadOnlyAccess, #[serde(default)] - deny_read_paths: Vec, + deny_read_patterns: Vec, #[serde(default)] network_access: bool, #[serde(default)] @@ -984,32 +1002,44 @@ impl SandboxPolicy { } SandboxPolicy::ReadOnly { access, - deny_read_paths, + deny_read_patterns, } => codex_protocol::protocol::SandboxPolicy::ReadOnly { access: access.to_core(), - deny_read_paths: deny_read_paths.clone(), + deny_read_patterns: deny_read_patterns + .clone() + .into_iter() + .map(Into::into) + .collect(), }, SandboxPolicy::ExternalSandbox { network_access, - deny_read_paths, + deny_read_patterns, } => codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access: match network_access { NetworkAccess::Restricted => CoreNetworkAccess::Restricted, NetworkAccess::Enabled => CoreNetworkAccess::Enabled, }, - deny_read_paths: deny_read_paths.clone(), + deny_read_patterns: deny_read_patterns + .clone() + .into_iter() + .map(Into::into) + .collect(), }, SandboxPolicy::WorkspaceWrite { writable_roots, read_only_access, - deny_read_paths, + deny_read_patterns, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, } => codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), read_only_access: read_only_access.to_core(), - deny_read_paths: deny_read_paths.clone(), + deny_read_patterns: deny_read_patterns + .clone() + .into_iter() + .map(Into::into) + .collect(), network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, @@ -1026,32 +1056,32 @@ impl From for SandboxPolicy { } codex_protocol::protocol::SandboxPolicy::ReadOnly { access, - deny_read_paths, + deny_read_patterns, } => SandboxPolicy::ReadOnly { access: ReadOnlyAccess::from(access), - deny_read_paths, + deny_read_patterns: deny_read_patterns.into_iter().map(Into::into).collect(), }, codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access, - deny_read_paths, + deny_read_patterns, } => SandboxPolicy::ExternalSandbox { network_access: match network_access { CoreNetworkAccess::Restricted => NetworkAccess::Restricted, CoreNetworkAccess::Enabled => NetworkAccess::Enabled, }, - deny_read_paths, + deny_read_patterns: deny_read_patterns.into_iter().map(Into::into).collect(), }, codex_protocol::protocol::SandboxPolicy::WorkspaceWrite { writable_roots, read_only_access, - deny_read_paths, + deny_read_patterns, network_access, exclude_tmpdir_env_var, exclude_slash_tmp, } => SandboxPolicy::WorkspaceWrite { writable_roots, read_only_access: ReadOnlyAccess::from(read_only_access), - deny_read_paths, + deny_read_patterns: deny_read_patterns.into_iter().map(Into::into).collect(), network_access, exclude_tmpdir_env_var, exclude_slash_tmp, @@ -4235,7 +4265,7 @@ mod tests { fn sandbox_policy_round_trips_external_sandbox_network_access() { let v2_policy = SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; let core_policy = v2_policy.to_core(); @@ -4243,7 +4273,7 @@ mod tests { core_policy, codex_protocol::protocol::SandboxPolicy::ExternalSandbox { network_access: CoreNetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], } ); @@ -4259,7 +4289,7 @@ mod tests { include_platform_defaults: false, readable_roots: vec![readable_root.clone()], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; let core_policy = v2_policy.to_core(); @@ -4270,7 +4300,7 @@ mod tests { include_platform_defaults: false, readable_roots: vec![readable_root], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], } ); @@ -4287,7 +4317,7 @@ mod tests { include_platform_defaults: false, readable_roots: vec![readable_root.clone()], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -4302,7 +4332,7 @@ mod tests { include_platform_defaults: false, readable_roots: vec![readable_root], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -4323,7 +4353,7 @@ mod tests { policy, SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], } ); } @@ -4343,7 +4373,7 @@ mod tests { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -4362,7 +4392,7 @@ mod tests { policy, SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], } ); } diff --git a/codex-rs/config/src/config_requirements.rs b/codex-rs/config/src/config_requirements.rs index f764d72355..ab82278a19 100644 --- a/codex-rs/config/src/config_requirements.rs +++ b/codex-rs/config/src/config_requirements.rs @@ -231,7 +231,7 @@ impl FilesystemDenyReadPattern { } pub fn contains_glob(&self) -> bool { - self.0.contains('*') + self.0.chars().any(is_glob_metacharacter) } pub fn from_input(input: &str) -> Result { @@ -280,7 +280,7 @@ fn deserialize_absolute_path(input: &str) -> Result { } fn split_glob_pattern(input: &str) -> (&str, &str) { - let Some(first_glob) = input.find('*') else { + let Some(first_glob) = input.find(is_glob_metacharacter) else { return ("", input); }; let separator_index = input[..first_glob] @@ -312,6 +312,10 @@ fn is_path_separator(ch: char) -> bool { } } +fn is_glob_metacharacter(ch: char) -> bool { + matches!(ch, '*' | '?' | '[') +} + #[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[serde(rename_all = "lowercase")] pub enum WebSearchModeRequirement { diff --git a/codex-rs/core/Cargo.toml b/codex-rs/core/Cargo.toml index 87f18eb800..f1c58af4ed 100644 --- a/codex-rs/core/Cargo.toml +++ b/codex-rs/core/Cargo.toml @@ -60,6 +60,7 @@ encoding_rs = { workspace = true } env-flags = { workspace = true } eventsource-stream = { workspace = true } futures = { workspace = true } +globset = { workspace = true } http = { workspace = true } iana-time-zone = { workspace = true } indexmap = { workspace = true } diff --git a/codex-rs/core/src/config/mod.rs b/codex-rs/core/src/config/mod.rs index 8ac654f316..844ca84952 100644 --- a/codex-rs/core/src/config/mod.rs +++ b/codex-rs/core/src/config/mod.rs @@ -39,7 +39,6 @@ use crate::features::Feature; use crate::features::FeatureOverrides; use crate::features::Features; use crate::features::FeaturesToml; -use crate::filesystem_deny_read::expand_deny_read_patterns; use crate::git_info::resolve_root_git_project_for_trust; use crate::model_provider_info::LEGACY_OLLAMA_CHAT_PROVIDER_ID; use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID; @@ -50,6 +49,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::DenyReadPattern; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; use crate::unified_exec::DEFAULT_MAX_BACKGROUND_TERMINAL_TIMEOUT_MS; @@ -1463,7 +1463,7 @@ impl ConfigToml { }) => SandboxPolicy::WorkspaceWrite { writable_roots: writable_roots.clone(), read_only_access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, @@ -2050,7 +2050,20 @@ impl Config { }) = filesystem_requirements && !filesystem_requirements.deny_read.is_empty() { - let deny_read_paths = expand_deny_read_patterns(&filesystem_requirements.deny_read); + let has_glob_patterns = filesystem_requirements + .deny_read + .iter() + .any(crate::config_loader::FilesystemDenyReadPattern::contains_glob); + let deny_read_patterns = filesystem_requirements + .deny_read + .iter() + .map(|pattern| DenyReadPattern::from(pattern.as_str())) + .collect::>(); + if has_glob_patterns && !cfg!(target_os = "macos") { + startup_warnings.push(format!( + "managed filesystem deny_read from {filesystem_requirements_source} only supports glob patterns on macOS; use literal paths on this platform" + )); + } let requirement_source = filesystem_requirements_source.clone(); constrained_sandbox_policy .value @@ -2069,7 +2082,7 @@ impl Config { constrained_sandbox_policy .value .add_normalizer(move |mut policy| { - policy.append_deny_read_paths(&deny_read_paths); + policy.append_deny_read_patterns(&deny_read_patterns); policy }) .map_err(std::io::Error::from)?; @@ -2850,7 +2863,7 @@ exclude_slash_tmp = true SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root.clone()], read_only_access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -2893,7 +2906,7 @@ trust_level = "trusted" SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], read_only_access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -6053,30 +6066,24 @@ mcp_oauth_callback_url = "https://example.com/callback" .await?; assert_eq!( - config.permissions.sandbox_policy.get().denied_read_paths(), - vec![denied_path] + config.permissions.sandbox_policy.get().deny_read_patterns(), + vec![crate::protocol::DenyReadPattern::from( + denied_path.to_string_lossy().into_owned(), + )] ); Ok(()) } #[tokio::test] - async fn requirements_filesystem_deny_read_glob_expands_to_matching_paths() + async fn requirements_filesystem_deny_read_glob_is_preserved_for_macos_policy() -> std::io::Result<()> { let codex_home = TempDir::new()?; let sensitive_dir = codex_home.path().join("sensitive"); let nested_dir = sensitive_dir.join("nested"); std::fs::create_dir_all(&nested_dir)?; - let top_match = sensitive_dir.join("top.txt"); - let nested_match = nested_dir.join("deep.txt"); - let ignored = sensitive_dir.join("top.log"); - std::fs::write(&top_match, "top")?; - std::fs::write(&nested_match, "deep")?; - std::fs::write(&ignored, "ignored")?; - let glob_pattern = format!("{}/sensitive/**/*.txt", codex_home.path().display()); - let top_match = AbsolutePathBuf::try_from(top_match).expect("absolute path"); - let nested_match = AbsolutePathBuf::try_from(nested_match).expect("absolute path"); + let expected_glob_pattern = glob_pattern.clone(); let config = ConfigBuilder::default() .codex_home(codex_home.path().to_path_buf()) @@ -6095,10 +6102,11 @@ mcp_oauth_callback_url = "https://example.com/callback" .build() .await?; - assert_eq!( - config.permissions.sandbox_policy.get().denied_read_paths(), - vec![top_match, nested_match] - ); + let policy = config.permissions.sandbox_policy.get(); + let expected_patterns = vec![crate::protocol::DenyReadPattern::from( + expected_glob_pattern, + )]; + assert_eq!(policy.deny_read_patterns(), expected_patterns); Ok(()) } @@ -6138,8 +6146,10 @@ mcp_oauth_callback_url = "https://example.com/callback" .map_err(std::io::Error::from)?; assert_eq!( - config.permissions.sandbox_policy.get().denied_read_paths(), - vec![denied_path] + config.permissions.sandbox_policy.get().deny_read_patterns(), + vec![crate::protocol::DenyReadPattern::from( + denied_path.to_string_lossy().into_owned(), + )] ); Ok(()) @@ -6186,7 +6196,7 @@ mcp_oauth_callback_url = "https://example.com/callback" .sandbox_policy .set(SandboxPolicy::ExternalSandbox { network_access: crate::protocol::NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], }) .expect_err("external-sandbox should be rejected"); @@ -6236,7 +6246,12 @@ mcp_oauth_callback_url = "https://example.com/callback" policy, SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } )); - assert_eq!(policy.denied_read_paths(), vec![denied_path]); + assert_eq!( + policy.deny_read_patterns(), + vec![crate::protocol::DenyReadPattern::from( + denied_path.to_string_lossy().into_owned(), + )] + ); assert!( config.startup_warnings.iter().any( |warning| warning.contains("Configured value for `sandbox_mode` is disallowed") @@ -6268,9 +6283,7 @@ mcp_oauth_callback_url = "https://example.com/callback" }; let sandbox_policy = SandboxPolicy::ExternalSandbox { network_access: crate::protocol::NetworkAccess::Enabled, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_path).expect("absolute deny_read path"), - ], + deny_read_patterns: vec![crate::protocol::DenyReadPattern::from(denied_path)], }; assert_eq!( diff --git a/codex-rs/core/src/config_loader/tests.rs b/codex-rs/core/src/config_loader/tests.rs index be2c3b57cb..8598a6e50c 100644 --- a/codex-rs/core/src/config_loader/tests.rs +++ b/codex-rs/core/src/config_loader/tests.rs @@ -430,7 +430,7 @@ allowed_sandbox_modes = ["read-only"] .can_set(&SandboxPolicy::WorkspaceWrite { writable_roots: Vec::new(), read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/environment_context.rs b/codex-rs/core/src/environment_context.rs index fd895da3b7..caba1ab50f 100644 --- a/codex-rs/core/src/environment_context.rs +++ b/codex-rs/core/src/environment_context.rs @@ -2,9 +2,9 @@ use crate::codex::TurnContext; use crate::contextual_user_message::ENVIRONMENT_CONTEXT_FRAGMENT; use crate::shell::Shell; use codex_protocol::models::ResponseItem; +use codex_protocol::protocol::DenyReadPattern; use codex_protocol::protocol::TurnContextItem; use codex_protocol::protocol::TurnContextNetworkItem; -use codex_utils_absolute_path::AbsolutePathBuf; use serde::Deserialize; use serde::Serialize; use std::path::PathBuf; @@ -17,7 +17,7 @@ pub(crate) struct EnvironmentContext { pub current_date: Option, pub timezone: Option, pub network: Option, - pub deny_read_paths: Vec, + pub deny_read_patterns: Vec, pub subagents: Option, } @@ -34,7 +34,7 @@ impl EnvironmentContext { current_date: Option, timezone: Option, network: Option, - deny_read_paths: Vec, + deny_read_patterns: Vec, subagents: Option, ) -> Self { Self { @@ -43,7 +43,7 @@ impl EnvironmentContext { current_date, timezone, network, - deny_read_paths, + deny_read_patterns, subagents, } } @@ -57,7 +57,7 @@ impl EnvironmentContext { current_date, timezone, network, - deny_read_paths, + deny_read_patterns, subagents, shell: _, } = other; @@ -65,7 +65,7 @@ impl EnvironmentContext { && self.current_date == *current_date && self.timezone == *timezone && self.network == *network - && self.deny_read_paths == *deny_read_paths + && self.deny_read_patterns == *deny_read_patterns && self.subagents == *subagents } @@ -76,8 +76,8 @@ impl EnvironmentContext { ) -> Self { let before_network = Self::network_from_turn_context_item(before); let after_network = Self::network_from_turn_context(after); - let before_deny_read_paths = before.sandbox_policy.denied_read_paths(); - let after_deny_read_paths = after.sandbox_policy.denied_read_paths(); + let before_deny_read_patterns = before.sandbox_policy.deny_read_patterns(); + let after_deny_read_patterns = after.sandbox_policy.deny_read_patterns(); let cwd = if before.cwd != after.cwd { Some(after.cwd.clone()) } else { @@ -90,10 +90,10 @@ impl EnvironmentContext { } else { before_network }; - let deny_read_paths = if before_deny_read_paths != after_deny_read_paths { - after_deny_read_paths + let deny_read_patterns = if before_deny_read_patterns != after_deny_read_patterns { + after_deny_read_patterns } else { - before_deny_read_paths + before_deny_read_patterns }; EnvironmentContext::new( cwd, @@ -101,7 +101,7 @@ impl EnvironmentContext { current_date, timezone, network, - deny_read_paths, + deny_read_patterns, None, ) } @@ -113,7 +113,7 @@ impl EnvironmentContext { turn_context.current_date.clone(), turn_context.timezone.clone(), Self::network_from_turn_context(turn_context), - turn_context.sandbox_policy.denied_read_paths(), + turn_context.sandbox_policy.deny_read_patterns(), None, ) } @@ -125,7 +125,7 @@ impl EnvironmentContext { turn_context_item.current_date.clone(), turn_context_item.timezone.clone(), Self::network_from_turn_context_item(turn_context_item), - turn_context_item.sandbox_policy.denied_read_paths(), + turn_context_item.sandbox_policy.deny_read_patterns(), None, ) } @@ -174,6 +174,9 @@ impl EnvironmentContext { /// /// ... /// ... + /// + /// ... + /// /// /// ``` pub fn serialize_to_xml(self) -> String { @@ -206,12 +209,12 @@ impl EnvironmentContext { // lines.push(" ".to_string()); } } - if !self.deny_read_paths.is_empty() { - lines.push(" ".to_string()); - for path in &self.deny_read_paths { - lines.push(format!(" {}", path.to_string_lossy())); + if !self.deny_read_patterns.is_empty() { + lines.push(" ".to_string()); + for pattern in &self.deny_read_patterns { + lines.push(format!(" {}", pattern.as_str())); } - lines.push(" ".to_string()); + lines.push(" ".to_string()); } if let Some(subagents) = self.subagents { lines.push(" ".to_string()); @@ -233,7 +236,6 @@ mod tests { use crate::shell::ShellType; use super::*; - use codex_utils_absolute_path::AbsolutePathBuf; use core_test_support::test_path_buf; use pretty_assertions::assert_eq; @@ -245,8 +247,8 @@ mod tests { } } - fn deny_path(path: &str) -> AbsolutePathBuf { - AbsolutePathBuf::try_from(path).expect("deny path should be absolute") + fn deny_path(path: &str) -> DenyReadPattern { + DenyReadPattern::from(path) } #[test] @@ -441,7 +443,7 @@ mod tests { } #[test] - fn serialize_environment_context_with_deny_read_paths() { + fn serialize_environment_context_with_deny_read_patterns() { let denied = vec![deny_path("/repo/.gitconfig"), deny_path("/repo/.ssh")]; let context = EnvironmentContext::new( Some(test_path_buf("/repo")), @@ -459,10 +461,10 @@ mod tests { bash 2026-02-26 America/Los_Angeles - - /repo/.gitconfig - /repo/.ssh - + + /repo/.gitconfig + /repo/.ssh + "#, test_path_buf("/repo").display() ); @@ -495,7 +497,7 @@ mod tests { } #[test] - fn equals_except_shell_compares_deny_read_paths() { + fn equals_except_shell_compares_deny_read_patterns() { let context1 = EnvironmentContext::new( Some(PathBuf::from("/repo")), fake_shell(), diff --git a/codex-rs/core/src/exec.rs b/codex-rs/core/src/exec.rs index c657d0346d..3e71c37620 100644 --- a/codex-rs/core/src/exec.rs +++ b/codex-rs/core/src/exec.rs @@ -159,7 +159,8 @@ pub async fn process_exec_tool_call( ) -> Result { let windows_sandbox_level = params.windows_sandbox_level; let enforce_managed_network = params.network.is_some(); - let enforce_deny_read = sandbox_policy.has_denied_read_paths() && !cfg!(target_os = "windows"); + let enforce_deny_read = + sandbox_policy.has_denied_read_restrictions() && !cfg!(target_os = "windows"); let sandbox_type = match &sandbox_policy { SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => { if enforce_managed_network || enforce_deny_read { diff --git a/codex-rs/core/src/exec_policy.rs b/codex-rs/core/src/exec_policy.rs index 8b1cbf84bf..21cc5deae2 100644 --- a/codex-rs/core/src/exec_policy.rs +++ b/codex-rs/core/src/exec_policy.rs @@ -270,7 +270,7 @@ impl ExecPolicyManager { // Bypass sandbox for trusted execpolicy allow rules unless the current // sandbox policy carries filesystem deny_read restrictions that must // still be enforced for shell/unified_exec commands. - bypass_sandbox: !sandbox_policy.has_denied_read_paths() + bypass_sandbox: !sandbox_policy.has_denied_read_restrictions() && evaluation.matched_rules.iter().any(|rule_match| { is_policy_match(rule_match) && rule_match.decision() == Decision::Allow }), @@ -2101,7 +2101,9 @@ prefix_rule(pattern=["git"], decision="prompt") let mut sandbox_policy = SandboxPolicy::new_workspace_write_policy(); let deny_path = AbsolutePathBuf::try_from("/tmp/secret-config").expect("absolute path"); - sandbox_policy.append_deny_read_paths(&[deny_path]); + sandbox_policy.append_deny_read_patterns(&[crate::protocol::DenyReadPattern::from( + deny_path.to_string_lossy().into_owned(), + )]); let manager = ExecPolicyManager::new(policy); let requirement = manager diff --git a/codex-rs/core/src/filesystem_deny_read.rs b/codex-rs/core/src/filesystem_deny_read.rs index e9d3241de7..b1b213a67c 100644 --- a/codex-rs/core/src/filesystem_deny_read.rs +++ b/codex-rs/core/src/filesystem_deny_read.rs @@ -1,10 +1,11 @@ use std::path::Path; use std::path::PathBuf; -use codex_config::FilesystemDenyReadPattern; use codex_utils_absolute_path::AbsolutePathBuf; +use globset::GlobBuilder; use crate::function_tool::FunctionCallError; +use crate::protocol::DenyReadPattern; use crate::protocol::SandboxPolicy; const DENY_READ_POLICY_MESSAGE: &str = @@ -36,52 +37,69 @@ pub(crate) fn ensure_search_root_does_not_overlap_deny_read( Ok(()) } -pub(crate) fn expand_deny_read_patterns( - patterns: &[FilesystemDenyReadPattern], -) -> Vec { - let mut expanded = Vec::new(); - for pattern in patterns { - if pattern.contains_glob() { - expand_glob_pattern(pattern, &mut expanded); - } else if let Ok(path) = AbsolutePathBuf::try_from(pattern.as_str()) { - push_unique_absolute(&mut expanded, path); - } - } - expanded -} - pub(crate) fn is_read_denied(path: &Path, sandbox_policy: &SandboxPolicy) -> bool { - let denied_paths = sandbox_policy.denied_read_paths(); - if denied_paths.is_empty() { - return false; - } - + let denied_paths = literal_deny_read_paths(sandbox_policy); let path_candidates = normalized_and_canonical_candidates(path); - denied_paths.iter().any(|denied| { + if denied_paths.iter().any(|denied| { let denied_candidates = normalized_and_canonical_candidates(denied.as_path()); path_candidates.iter().any(|candidate| { denied_candidates.iter().any(|denied_candidate| { candidate == denied_candidate || candidate.starts_with(denied_candidate) }) }) - }) -} + }) { + return true; + } -pub(crate) fn overlaps_deny_read(path: &Path, sandbox_policy: &SandboxPolicy) -> bool { - let denied_paths = sandbox_policy.denied_read_paths(); - if denied_paths.is_empty() { + if !cfg!(target_os = "macos") { return false; } + sandbox_policy + .deny_read_patterns() + .iter() + .filter(|pattern| pattern.contains_glob()) + .any(|pattern| glob_pattern_matches_any_candidate(&path_candidates, pattern)) +} + +pub(crate) fn overlaps_deny_read(path: &Path, sandbox_policy: &SandboxPolicy) -> bool { + let denied_paths = literal_deny_read_paths(sandbox_policy); let path_candidates = normalized_and_canonical_candidates(path); - denied_paths.iter().any(|denied| { + if denied_paths.iter().any(|denied| { let denied_candidates = normalized_and_canonical_candidates(denied.as_path()); path_candidates.iter().any(|candidate| { denied_candidates.iter().any(|denied_candidate| { candidate.starts_with(denied_candidate) || denied_candidate.starts_with(candidate) }) }) - }) + }) { + return true; + } + + if !cfg!(target_os = "macos") { + return false; + } + + sandbox_policy + .deny_read_patterns() + .iter() + .filter(|pattern| pattern.contains_glob()) + .filter_map(glob_pattern_prefix) + .any(|prefix| { + path_candidates.iter().any(|candidate| { + candidate.starts_with(prefix.as_path()) || prefix.as_path().starts_with(candidate) + }) + }) +} + +fn literal_deny_read_paths(sandbox_policy: &SandboxPolicy) -> Vec { + let mut denied_paths = Vec::new(); + for pattern in sandbox_policy.deny_read_patterns() { + if let Some(path) = pattern.literal_path() { + push_unique_absolute(&mut denied_paths, path); + } + } + denied_paths } fn normalized_and_canonical_candidates(path: &Path) -> Vec { @@ -117,24 +135,37 @@ fn push_unique_absolute(candidates: &mut Vec, candidate: Absolu } } -fn expand_glob_pattern(pattern: &FilesystemDenyReadPattern, expanded: &mut Vec) { - let (root, components) = split_glob_pattern(pattern.as_str()); - let Ok(root) = AbsolutePathBuf::try_from(root) else { - return; +fn glob_pattern_matches_any_candidate( + path_candidates: &[PathBuf], + pattern: &DenyReadPattern, +) -> bool { + let Ok(glob) = GlobBuilder::new(pattern.as_str()) + .literal_separator(true) + .build() + else { + return false; }; - expand_glob_components(root.as_path(), &components, expanded); + let matcher = glob.compile_matcher(); + path_candidates + .iter() + .any(|candidate| matcher.is_match(candidate)) } -fn split_glob_pattern(pattern: &str) -> (&str, Vec<&str>) { - let Some(first_glob) = pattern.find('*') else { - return (pattern, Vec::new()); +fn glob_pattern_prefix(pattern: &DenyReadPattern) -> Option { + let (root, _) = split_glob_pattern(pattern.as_str()); + AbsolutePathBuf::try_from(root).ok() +} + +fn split_glob_pattern(pattern: &str) -> (&str, &str) { + let Some(first_glob) = pattern.find(is_glob_metacharacter) else { + return (pattern, ""); }; let separator_index = pattern[..first_glob] .char_indices() .rev() .find(|(_, ch)| is_path_separator(*ch)) .map(|(index, _)| index); - let (root, suffix) = match separator_index { + match separator_index { Some(0) => ("/", &pattern[1..]), Some(index) if cfg!(windows) @@ -146,109 +177,7 @@ fn split_glob_pattern(pattern: &str) -> (&str, Vec<&str>) { } Some(index) => (&pattern[..index], &pattern[index + 1..]), None => ("", pattern), - }; - let components = suffix - .split(is_path_separator) - .filter(|component| !component.is_empty()) - .collect(); - (root, components) -} - -fn expand_glob_components(current: &Path, remaining: &[&str], expanded: &mut Vec) { - if remaining.is_empty() { - if let Ok(path) = AbsolutePathBuf::try_from(current) { - push_unique_absolute(expanded, path); - } - return; } - - let component = remaining[0]; - let rest = &remaining[1..]; - - if component == "**" { - expand_glob_components(current, rest, expanded); - - let Ok(entries) = std::fs::read_dir(current) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - if rest.is_empty() - && let Ok(path) = AbsolutePathBuf::try_from(path.as_path()) - { - push_unique_absolute(expanded, path); - } - if path.is_dir() { - expand_glob_components(&path, remaining, expanded); - } - } - return; - } - - let Ok(entries) = std::fs::read_dir(current) else { - return; - }; - for entry in entries.flatten() { - let path = entry.path(); - let Some(name) = path.file_name().and_then(|name| name.to_str()) else { - continue; - }; - if !segment_matches(component, name) { - continue; - } - if rest.is_empty() { - if let Ok(path) = AbsolutePathBuf::try_from(path.as_path()) { - push_unique_absolute(expanded, path); - } - } else if path.is_dir() { - expand_glob_components(&path, rest, expanded); - } - } -} - -fn segment_matches(pattern: &str, candidate: &str) -> bool { - if !pattern.contains('*') { - return pattern == candidate; - } - - let pattern_chars: Vec = pattern.chars().collect(); - let candidate_chars: Vec = candidate.chars().collect(); - let mut pattern_index = 0; - let mut candidate_index = 0; - let mut star_index = None; - let mut star_candidate_index = 0; - - while candidate_index < candidate_chars.len() { - if pattern_index < pattern_chars.len() - && pattern_chars[pattern_index] == candidate_chars[candidate_index] - { - pattern_index += 1; - candidate_index += 1; - continue; - } - - if pattern_index < pattern_chars.len() && pattern_chars[pattern_index] == '*' { - star_index = Some(pattern_index); - pattern_index += 1; - star_candidate_index = candidate_index; - continue; - } - - if let Some(star_index) = star_index { - pattern_index = star_index + 1; - star_candidate_index += 1; - candidate_index = star_candidate_index; - continue; - } - - return false; - } - - while pattern_index < pattern_chars.len() && pattern_chars[pattern_index] == '*' { - pattern_index += 1; - } - - pattern_index == pattern_chars.len() } fn is_path_separator(ch: char) -> bool { @@ -259,22 +188,25 @@ fn is_path_separator(ch: char) -> bool { } } +fn is_glob_metacharacter(ch: char) -> bool { + matches!(ch, '*' | '?' | '[') +} + #[cfg(test)] mod tests { - use super::expand_deny_read_patterns; - use super::is_read_denied; - use super::overlaps_deny_read; + use crate::protocol::DenyReadPattern; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; - use codex_config::FilesystemDenyReadPattern; - use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; + use super::is_read_denied; + use super::overlaps_deny_read; + fn deny_policy(path: &std::path::Path) -> SandboxPolicy { SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![AbsolutePathBuf::try_from(path).expect("absolute deny path")], + deny_read_patterns: vec![DenyReadPattern::from(path.to_string_lossy().into_owned())], } } @@ -332,37 +264,45 @@ mod tests { } #[test] - fn expand_deny_read_patterns_supports_star_and_globstar() { + fn literal_patterns_are_denied_and_globs_are_ignored_off_macos() { let temp = tempdir().expect("temp dir"); - let secrets = temp.path().join("secrets"); - let nested = secrets.join("nested"); - std::fs::create_dir_all(&nested).expect("create nested"); - let top = secrets.join("top.txt"); - let deep = nested.join("deep.txt"); - let ignored = secrets.join("top.log"); - std::fs::write(&top, "top").expect("write top"); - std::fs::write(&deep, "deep").expect("write deep"); - std::fs::write(&ignored, "ignored").expect("write ignored"); + let literal = temp.path().join("private"); + let other = temp.path().join("notes.txt"); + std::fs::create_dir_all(&literal).expect("create literal dir"); + std::fs::write(&other, "notes").expect("write notes"); - let top_pattern = FilesystemDenyReadPattern::from_input(&format!( - "{}/secrets/*.txt", - temp.path().display() - )) - .expect("normalize pattern"); - let deep_pattern = FilesystemDenyReadPattern::from_input(&format!( - "{}/secrets/**/*.txt", - temp.path().display() - )) - .expect("normalize pattern"); + let policy = SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::FullAccess, + deny_read_patterns: vec![ + DenyReadPattern::from(literal.to_string_lossy().into_owned()), + DenyReadPattern::from(format!("{}/**/*.txt", temp.path().display())), + ], + }; - let expanded = expand_deny_read_patterns(&[top_pattern, deep_pattern]); + assert_eq!(is_read_denied(&literal, &policy), true); + assert_eq!(is_read_denied(&other, &policy), cfg!(target_os = "macos")); + } + #[cfg(target_os = "macos")] + #[test] + fn glob_patterns_deny_matching_paths_on_macos() { + let temp = tempdir().expect("temp dir"); + let denied = temp.path().join("private").join("secret1.txt"); + std::fs::create_dir_all(denied.parent().expect("parent")).expect("create parent"); + std::fs::write(&denied, "secret").expect("write secret"); + + let policy = SandboxPolicy::ReadOnly { + access: ReadOnlyAccess::FullAccess, + deny_read_patterns: vec![DenyReadPattern::from(format!( + "{}/private/secret?.txt", + temp.path().display() + ))], + }; + + assert_eq!(is_read_denied(&denied, &policy), true); assert_eq!( - expanded, - vec![ - AbsolutePathBuf::try_from(top).expect("absolute path"), - AbsolutePathBuf::try_from(deep).expect("absolute path"), - ] + overlaps_deny_read(&temp.path().join("private"), &policy), + true ); } } diff --git a/codex-rs/core/src/memories/phase2.rs b/codex-rs/core/src/memories/phase2.rs index 2ba1f0f9e4..534cc691da 100644 --- a/codex-rs/core/src/memories/phase2.rs +++ b/codex-rs/core/src/memories/phase2.rs @@ -281,7 +281,7 @@ mod agent { let consolidation_sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots, read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/safety.rs b/codex-rs/core/src/safety.rs index 7cdfea4282..f2130c3596 100644 --- a/codex-rs/core/src/safety.rs +++ b/codex-rs/core/src/safety.rs @@ -216,7 +216,7 @@ mod tests { let policy_workspace_only = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -239,7 +239,7 @@ mod tests { let policy_with_parent = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(parent).unwrap()], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -259,7 +259,7 @@ mod tests { let policy = SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; assert_eq!( @@ -287,7 +287,7 @@ mod tests { let policy_workspace_only = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -329,7 +329,7 @@ mod tests { let policy_workspace_only = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/src/sandbox_tags.rs b/codex-rs/core/src/sandbox_tags.rs index 16eb8e63c7..4c361d7292 100644 --- a/codex-rs/core/src/sandbox_tags.rs +++ b/codex-rs/core/src/sandbox_tags.rs @@ -11,7 +11,9 @@ pub(crate) fn sandbox_tag( if matches!(policy, SandboxPolicy::DangerFullAccess) { return "none"; } - if matches!(policy, SandboxPolicy::ExternalSandbox { .. }) && !policy.has_denied_read_paths() { + if matches!(policy, SandboxPolicy::ExternalSandbox { .. }) + && !policy.has_denied_read_restrictions() + { return "external"; } if cfg!(target_os = "windows") && matches!(windows_sandbox_level, WindowsSandboxLevel::Elevated) @@ -52,7 +54,7 @@ mod tests { let actual = sandbox_tag( &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], }, WindowsSandboxLevel::Disabled, true, diff --git a/codex-rs/core/src/sandboxing/mod.rs b/codex-rs/core/src/sandboxing/mod.rs index 1b73673fbe..b3f441128e 100644 --- a/codex-rs/core/src/sandboxing/mod.rs +++ b/codex-rs/core/src/sandboxing/mod.rs @@ -209,7 +209,7 @@ fn sandbox_policy_with_additional_permissions( network_access, exclude_tmpdir_env_var, exclude_slash_tmp, - deny_read_paths, + deny_read_patterns, } => { let mut merged_writes = writable_roots.clone(); merged_writes.extend(extra_writes); @@ -222,17 +222,17 @@ fn sandbox_policy_with_additional_permissions( network_access: *network_access, exclude_tmpdir_env_var: *exclude_tmpdir_env_var, exclude_slash_tmp: *exclude_slash_tmp, - deny_read_paths: deny_read_paths.clone(), + deny_read_patterns: deny_read_patterns.clone(), } } SandboxPolicy::ReadOnly { access, - deny_read_paths, + deny_read_patterns, } => { if extra_writes.is_empty() { SandboxPolicy::ReadOnly { access: merge_read_only_access_with_additional_reads(access, extra_reads), - deny_read_paths: deny_read_paths.clone(), + deny_read_patterns: deny_read_patterns.clone(), } } else { // todo(dylan) - for now, this grants more access than the request. We should restrict this, @@ -247,7 +247,7 @@ fn sandbox_policy_with_additional_permissions( network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, - deny_read_paths: deny_read_paths.clone(), + deny_read_patterns: deny_read_patterns.clone(), } } } @@ -271,7 +271,8 @@ impl SandboxManager { windows_sandbox_level: WindowsSandboxLevel, has_managed_network_requirements: bool, ) -> SandboxType { - let enforce_deny_read = policy.has_denied_read_paths() && !cfg!(target_os = "windows"); + let enforce_deny_read = + policy.has_denied_read_restrictions() && !cfg!(target_os = "windows"); match pref { SandboxablePreference::Forbid => SandboxType::None, SandboxablePreference::Require => { @@ -467,10 +468,9 @@ mod tests { let sandbox = manager.select_initial( &SandboxPolicy::ExternalSandbox { network_access: codex_protocol::protocol::NetworkAccess::Enabled, - deny_read_paths: vec![ - codex_utils_absolute_path::AbsolutePathBuf::from_absolute_path(denied_path) - .expect("absolute path"), - ], + deny_read_patterns: vec![codex_protocol::protocol::DenyReadPattern::from( + denied_path, + )], }, SandboxablePreference::Auto, WindowsSandboxLevel::Disabled, diff --git a/codex-rs/core/src/seatbelt.rs b/codex-rs/core/src/seatbelt.rs index 7479e791a7..9adefc0cd9 100644 --- a/codex-rs/core/src/seatbelt.rs +++ b/codex-rs/core/src/seatbelt.rs @@ -186,20 +186,172 @@ fn normalize_path_for_sandbox(path: &Path) -> Option { normalized_path.or(Some(absolute_path)) } +fn normalize_glob_pattern_for_sandbox(pattern: &str) -> Option { + let (prefix, suffix) = split_glob_pattern(pattern); + let normalized_prefix = normalize_path_for_sandbox(Path::new(prefix))?; + if suffix.is_empty() { + return Some(normalized_prefix.to_string_lossy().to_string()); + } + if normalized_prefix.as_path() == Path::new("/") { + Some(format!("/{suffix}")) + } else { + Some(format!("{}/{suffix}", normalized_prefix.to_string_lossy())) + } +} + +fn split_glob_pattern(pattern: &str) -> (&str, &str) { + let Some(first_glob) = pattern.find(is_glob_metacharacter) else { + return (pattern, ""); + }; + let separator_index = pattern[..first_glob].rfind('/'); + match separator_index { + Some(0) => ("/", &pattern[1..]), + Some(index) => (&pattern[..index], &pattern[index + 1..]), + None => ("", pattern), + } +} + +fn glob_pattern_base_directory(pattern: &str) -> Option { + let (prefix, _) = split_glob_pattern(pattern); + if prefix.is_empty() || prefix == "/" { + return None; + } + normalize_path_for_sandbox(Path::new(prefix)) +} + +fn glob_pattern_to_regex(pattern: &str) -> String { + let chars: Vec = pattern.chars().collect(); + let mut regex = String::from("^"); + let mut index = 0; + + while index < chars.len() { + if chars[index] == '*' { + let has_globstar = chars.get(index + 1) == Some(&'*'); + if has_globstar { + let ends_pattern = index + 2 == chars.len(); + let followed_by_separator = chars.get(index + 2) == Some(&'/'); + if ends_pattern && index > 0 && chars[index - 1] == '/' { + regex.pop(); + regex.push_str("(?:/.*)?"); + break; + } + if followed_by_separator { + regex.push_str("(?:[^/]+/)*"); + index += 3; + continue; + } + regex.push_str(".*"); + index += 2; + continue; + } + regex.push_str("[^/]*"); + index += 1; + continue; + } + if chars[index] == '?' { + regex.push_str("[^/]"); + index += 1; + continue; + } + if chars[index] == '[' { + let mut class_end = index + 1; + while class_end < chars.len() && chars[class_end] != ']' { + class_end += 1; + } + if class_end == chars.len() { + regex.push_str("\\["); + index += 1; + continue; + } + + regex.push('['); + let mut class_index = index + 1; + if matches!(chars.get(class_index), Some('!')) { + regex.push('^'); + class_index += 1; + } else if matches!(chars.get(class_index), Some('^')) { + regex.push('\\'); + regex.push('^'); + class_index += 1; + } + while class_index < class_end { + if matches!(chars[class_index], '\\' | ']') { + regex.push('\\'); + } + regex.push(chars[class_index]); + class_index += 1; + } + regex.push(']'); + index = class_end + 1; + continue; + } + + if matches!( + chars[index], + '.' | '+' | '(' | ')' | '[' | ']' | '{' | '}' | '^' | '$' | '|' | '?' | '\\' + ) { + regex.push('\\'); + } + regex.push(chars[index]); + index += 1; + } + + regex.push('$'); + regex +} + +fn escape_seatbelt_string(value: &str) -> String { + value.replace('\\', "\\\\").replace('"', "\\\"") +} + +fn is_glob_metacharacter(ch: char) -> bool { + matches!(ch, '*' | '?' | '[') +} + fn deny_read_policy_and_params(sandbox_policy: &SandboxPolicy) -> (String, Vec<(String, PathBuf)>) { - let denied_paths = sandbox_policy.denied_read_paths(); - if denied_paths.is_empty() { + let deny_read_patterns = sandbox_policy.deny_read_patterns(); + if deny_read_patterns.is_empty() { return (String::new(), Vec::new()); } let mut read_deny_params = BTreeMap::new(); + let mut read_deny_regexes = BTreeSet::new(); let mut unlink_deny_params = BTreeMap::new(); - for denied_path in denied_paths { - let Some(normalized) = normalize_path_for_sandbox(denied_path.as_path()) else { + for denied_pattern in deny_read_patterns { + if denied_pattern.contains_glob() { + let Some(normalized_pattern) = + normalize_glob_pattern_for_sandbox(denied_pattern.as_str()) + else { + warn!( + "ignoring deny_read pattern because it could not be normalized for seatbelt: {}", + denied_pattern.as_str() + ); + continue; + }; + + read_deny_regexes.insert(glob_pattern_to_regex(&normalized_pattern)); + + if let Some(base_directory) = glob_pattern_base_directory(&normalized_pattern) { + for ancestor in base_directory.as_path().ancestors() { + if ancestor == Path::new("/") { + break; + } + if let Some(normalized_ancestor) = normalize_path_for_sandbox(ancestor) { + unlink_deny_params + .entry(normalized_ancestor.to_string_lossy().to_string()) + .or_insert(normalized_ancestor); + } + } + } + continue; + } + + let Some(normalized) = normalize_path_for_sandbox(Path::new(denied_pattern.as_str())) + else { warn!( - "ignoring deny_read path because it could not be normalized for seatbelt: {}", - denied_path.display() + "ignoring deny_read pattern because it could not be normalized for seatbelt: {}", + denied_pattern.as_str() ); continue; }; @@ -220,7 +372,7 @@ fn deny_read_policy_and_params(sandbox_policy: &SandboxPolicy) -> (String, Vec<( } } - if read_deny_params.is_empty() { + if read_deny_params.is_empty() && read_deny_regexes.is_empty() { return (String::new(), Vec::new()); } @@ -232,6 +384,12 @@ fn deny_read_policy_and_params(sandbox_policy: &SandboxPolicy) -> (String, Vec<( policy.push_str(&format!("(deny file-read* (subpath (param \"{key}\")))\n")); params.push((key, path.into_path_buf())); } + for regex in read_deny_regexes { + policy.push_str(&format!( + "(deny file-read* (regex \"{}\"))\n", + escape_seatbelt_string(®ex) + )); + } if !unlink_deny_params.is_empty() { policy.push_str("; reduce rename/unlink bypasses into denied read paths\n"); @@ -548,6 +706,7 @@ mod tests { use super::normalize_path_for_sandbox; use super::unix_socket_dir_params; use super::unix_socket_policy; + use crate::protocol::DenyReadPattern; use crate::protocol::SandboxPolicy; use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE; use crate::seatbelt_permissions::MacOsAutomationPermission; @@ -585,7 +744,9 @@ mod tests { let policy = SandboxPolicy::ReadOnly { access: Default::default(), - deny_read_paths: vec![AbsolutePathBuf::try_from(denied_file).expect("absolute path")], + deny_read_patterns: vec![DenyReadPattern::from( + denied_file.to_string_lossy().into_owned(), + )], }; let (policy_text, params) = deny_read_policy_and_params(&policy); @@ -606,6 +767,29 @@ mod tests { ); } + #[test] + fn deny_read_policy_emits_regex_for_glob_patterns() { + let tmp = TempDir::new().expect("tempdir"); + let denied_pattern = format!("{}/private/**/*.txt", tmp.path().display()); + + let policy = SandboxPolicy::ReadOnly { + access: Default::default(), + deny_read_patterns: vec![DenyReadPattern::from(denied_pattern)], + }; + + let (policy_text, params) = deny_read_policy_and_params(&policy); + + assert!(policy_text.contains("(deny file-read* (regex \"^")); + assert!(policy_text.contains("private/(?:[^/]+/)*[^/]*\\.txt$")); + assert!(policy_text.contains("(deny file-write-unlink")); + assert!( + params + .iter() + .any(|(key, value)| key.starts_with("DENY_UNLINK_") + && value.ends_with(Path::new("private"))) + ); + } + #[test] fn base_policy_allows_node_cpu_sysctls() { assert!( @@ -801,7 +985,7 @@ sys.exit(0 if allowed else 13) &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -831,7 +1015,7 @@ sys.exit(0 if allowed else 13) &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -950,7 +1134,7 @@ sys.exit(0 if allowed else 13) &SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -1002,7 +1186,7 @@ sys.exit(0 if allowed else 13) .map(|p| p.try_into().unwrap()) .collect(), read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1197,7 +1381,7 @@ sys.exit(0 if allowed else 13) let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![worktree_root.try_into().expect("worktree_root is absolute")], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -1283,7 +1467,7 @@ sys.exit(0 if allowed else 13) let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/skills/loader.rs b/codex-rs/core/src/skills/loader.rs index ac76218678..d0f7e2b6d6 100644 --- a/codex-rs/core/src/skills/loader.rs +++ b/codex-rs/core/src/skills/loader.rs @@ -1461,7 +1461,7 @@ permissions: .expect("absolute data path") ], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/src/skills/permissions.rs b/codex-rs/core/src/skills/permissions.rs index f18a21a72b..6a7c4a8f12 100644 --- a/codex-rs/core/src/skills/permissions.rs +++ b/codex-rs/core/src/skills/permissions.rs @@ -48,7 +48,7 @@ pub(crate) fn compile_permission_profile( readable_roots: fs_read, } }, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: network.unwrap_or_default(), exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -59,7 +59,7 @@ pub(crate) fn compile_permission_profile( include_platform_defaults: true, readable_roots: fs_read, }, - deny_read_paths: vec![], + deny_read_patterns: vec![], } } else { // Default sandbox policy @@ -262,7 +262,7 @@ mod tests { .expect("absolute read path") ], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -360,7 +360,7 @@ mod tests { .expect("absolute read path") ], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], }), network: None, allow_login_shell: true, diff --git a/codex-rs/core/src/tools/handlers/list_dir.rs b/codex-rs/core/src/tools/handlers/list_dir.rs index e3deb2e16c..6654a547ec 100644 --- a/codex-rs/core/src/tools/handlers/list_dir.rs +++ b/codex-rs/core/src/tools/handlers/list_dir.rs @@ -296,9 +296,9 @@ impl From<&FileType> for DirEntryKind { #[cfg(test)] mod tests { use super::*; + use crate::protocol::DenyReadPattern; use crate::protocol::ReadOnlyAccess; use crate::protocol::SandboxPolicy; - use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::tempdir; @@ -564,10 +564,14 @@ mod tests { let policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_dir.as_path()).expect("absolute denied dir"), - AbsolutePathBuf::try_from(dir_path.join("top_secret.txt")) - .expect("absolute denied file"), + deny_read_patterns: vec![ + DenyReadPattern::from(denied_dir.to_string_lossy().into_owned()), + DenyReadPattern::from( + dir_path + .join("top_secret.txt") + .to_string_lossy() + .into_owned(), + ), ], }; diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index bfa85d1275..fbeb9d8bc8 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -147,7 +147,7 @@ fn reject_explicit_escalation_if_deny_read_present( sandbox_policy: &SandboxPolicy, ) -> Result<(), FunctionCallError> { if sandbox_permissions.requires_escalated_permissions() - && sandbox_policy.has_denied_read_paths() + && sandbox_policy.has_denied_read_restrictions() { return Err(FunctionCallError::RespondToModel( "filesystem deny_read policy is enforced; reject command — you cannot ask for escalated permissions because managed read restrictions must remain sandboxed".to_string(), @@ -161,6 +161,7 @@ fn reject_explicit_escalation_if_deny_read_present( mod tests { use super::reject_explicit_escalation_if_deny_read_present; use crate::function_tool::FunctionCallError; + use crate::protocol::DenyReadPattern; use crate::protocol::SandboxPolicy; use crate::sandboxing::SandboxPermissions; use codex_utils_absolute_path::AbsolutePathBuf; @@ -169,7 +170,9 @@ mod tests { fn explicit_escalation_is_rejected_when_deny_read_paths_exist() { let mut policy = SandboxPolicy::new_workspace_write_policy(); let path = AbsolutePathBuf::try_from("/tmp/deny-read-test").expect("absolute path"); - policy.append_deny_read_paths(&[path]); + policy.append_deny_read_patterns(&[DenyReadPattern::from( + path.to_string_lossy().into_owned(), + )]); let result = reject_explicit_escalation_if_deny_read_present( SandboxPermissions::RequireEscalated, @@ -186,7 +189,9 @@ mod tests { fn non_escalated_command_is_allowed_when_deny_read_paths_exist() { let mut policy = SandboxPolicy::new_workspace_write_policy(); let path = AbsolutePathBuf::try_from("/tmp/deny-read-test").expect("absolute path"); - policy.append_deny_read_paths(&[path]); + policy.append_deny_read_patterns(&[DenyReadPattern::from( + path.to_string_lossy().into_owned(), + )]); let result = reject_explicit_escalation_if_deny_read_present( SandboxPermissions::UseDefault, diff --git a/codex-rs/core/src/tools/orchestrator.rs b/codex-rs/core/src/tools/orchestrator.rs index eb805c7e7c..cb730ca187 100644 --- a/codex-rs/core/src/tools/orchestrator.rs +++ b/codex-rs/core/src/tools/orchestrator.rs @@ -226,7 +226,7 @@ impl ToolOrchestrator { // Managed filesystem read denies are non-overridable. If a command // was denied by the sandbox, do not offer a no-sandbox retry path // because the retry would bypass deny_read enforcement. - if turn_ctx.sandbox_policy.has_denied_read_paths() { + if turn_ctx.sandbox_policy.has_denied_read_restrictions() { return Err(ToolError::Codex(CodexErr::Sandbox(SandboxErr::Denied { output, network_policy_decision, diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs index 9d5360002a..77a03f7dd9 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation_tests.rs @@ -202,7 +202,7 @@ fn shell_request_escalation_execution_is_explicit() { network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; let macos_seatbelt_profile_extensions = MacOsSeatbeltProfileExtensions { macos_preferences: MacOsPreferencesPermission::ReadWrite, diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index faab3b83c7..7401be1ca7 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -364,7 +364,7 @@ mod tests { AskForApproval::OnRequest, &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, - deny_read_paths: vec![], + deny_read_patterns: vec![], }, ), ExecApprovalRequirement::Skip { diff --git a/codex-rs/core/tests/common/zsh_fork.rs b/codex-rs/core/tests/common/zsh_fork.rs index e988a8f238..eefa53d233 100644 --- a/codex-rs/core/tests/common/zsh_fork.rs +++ b/codex-rs/core/tests/common/zsh_fork.rs @@ -41,7 +41,7 @@ pub fn restrictive_workspace_write_policy() -> SandboxPolicy { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, - deny_read_paths: Vec::new(), + deny_read_patterns: Vec::new(), } } diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index 55495ad7b5..03395845bc 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -580,7 +580,7 @@ async fn apply_patch_cli_rejects_path_traversal_outside_workspace( let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -638,7 +638,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(), - deny_read_paths: vec![], + deny_read_patterns: vec![], 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 223fb0b037..06401b04cb 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -682,7 +682,7 @@ fn scenarios() -> Vec { let workspace_write = |network_access| SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -1704,7 +1704,7 @@ async fn approving_apply_patch_for_session_skips_future_prompts_for_same_file() let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -2245,7 +2245,7 @@ allow_local_binding = true network_access: true, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; let sandbox_policy_for_config = sandbox_policy.clone(); let mut builder = test_codex().with_home(home).with_config(move |config| { diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index ee63e99827..26d6da2193 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -458,7 +458,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable_root], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 4ea2388907..15d6153e51 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -400,7 +400,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an let new_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![writable.path().try_into().unwrap()], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -665,7 +665,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res let new_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(writable.path()).unwrap()], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: true, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/core/tests/suite/request_permissions.rs b/codex-rs/core/tests/suite/request_permissions.rs index d4287e915c..99bf523632 100644 --- a/codex-rs/core/tests/suite/request_permissions.rs +++ b/codex-rs/core/tests/suite/request_permissions.rs @@ -183,7 +183,7 @@ fn workspace_write_excluding_tmp() -> SandboxPolicy { network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, - deny_read_paths: vec![], + deny_read_patterns: vec![], } } diff --git a/codex-rs/core/tests/suite/seatbelt.rs b/codex-rs/core/tests/suite/seatbelt.rs index 840f29e591..29c9d77001 100644 --- a/codex-rs/core/tests/suite/seatbelt.rs +++ b/codex-rs/core/tests/suite/seatbelt.rs @@ -78,7 +78,7 @@ async fn if_parent_of_repo_is_writable_then_dot_git_folder_is_writable() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![test_scenario.repo_parent.as_path().try_into().unwrap()], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -106,7 +106,7 @@ async fn if_git_repo_is_writable_root_then_dot_git_folder_is_read_only() { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![test_scenario.repo_root.as_path().try_into().unwrap()], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, diff --git a/codex-rs/linux-sandbox/src/bwrap.rs b/codex-rs/linux-sandbox/src/bwrap.rs index 669c601a58..5ef642406a 100644 --- a/codex-rs/linux-sandbox/src/bwrap.rs +++ b/codex-rs/linux-sandbox/src/bwrap.rs @@ -91,7 +91,7 @@ pub(crate) fn create_bwrap_command_args( ) -> Result> { if sandbox_policy.has_full_disk_write_access() { return if options.network_mode == BwrapNetworkMode::FullAccess { - if sandbox_policy.has_denied_read_paths() { + if sandbox_policy.has_denied_read_patterns() { Ok(create_bwrap_flags_full_filesystem( command, sandbox_policy, @@ -316,9 +316,9 @@ fn apply_deny_read_overlays( writable_roots: &[WritableRoot], ) { let deny_read_paths: BTreeSet = sandbox_policy - .denied_read_paths() + .deny_read_patterns() .into_iter() - .map(PathBuf::from) + .filter_map(|pattern| pattern.literal_path().map(PathBuf::from)) .collect(); let writable_roots: Vec = writable_roots @@ -467,10 +467,10 @@ fn find_first_non_existent_component(target_path: &Path) -> Option { #[cfg(test)] mod tests { use super::*; + use codex_protocol::protocol::DenyReadPattern; use codex_protocol::protocol::NetworkAccess; use codex_protocol::protocol::ReadOnlyAccess; use codex_protocol::protocol::SandboxPolicy; - use codex_utils_absolute_path::AbsolutePathBuf; use pretty_assertions::assert_eq; use tempfile::TempDir; @@ -501,9 +501,9 @@ mod tests { command.clone(), &SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_file.as_path()).expect("absolute path"), - ], + deny_read_patterns: vec![DenyReadPattern::from( + denied_file.to_string_lossy().into_owned(), + )], }, Path::new("/"), BwrapOptions { @@ -561,7 +561,7 @@ mod tests { let sandbox_policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![AbsolutePathBuf::try_from(Path::new("/dev")).expect("/dev path")], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: true, @@ -600,7 +600,7 @@ mod tests { .expect("absolute readable root"), ], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args"); @@ -626,7 +626,7 @@ mod tests { include_platform_defaults: true, readable_roots: Vec::new(), }, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; // `ReadOnlyAccess::Restricted` always includes `cwd` as a readable @@ -654,9 +654,9 @@ mod tests { let policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_dir.as_path()).expect("absolute dir"), - AbsolutePathBuf::try_from(denied_file.as_path()).expect("absolute file"), + deny_read_patterns: vec![ + DenyReadPattern::from(denied_dir.to_string_lossy().into_owned()), + DenyReadPattern::from(denied_file.to_string_lossy().into_owned()), ], }; @@ -690,9 +690,9 @@ mod tests { .expect("absolute readable root"), ], }, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_file.as_path()).expect("absolute denied file"), - ], + deny_read_patterns: vec![DenyReadPattern::from( + denied_file.to_string_lossy().into_owned(), + )], }; let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args"); @@ -727,9 +727,9 @@ mod tests { let policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_dir.as_path()).expect("absolute dir"), - AbsolutePathBuf::try_from(denied_file.as_path()).expect("absolute file"), + deny_read_patterns: vec![ + DenyReadPattern::from(denied_dir.to_string_lossy().into_owned()), + DenyReadPattern::from(denied_file.to_string_lossy().into_owned()), ], }; @@ -755,9 +755,9 @@ mod tests { let policy = SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![ - AbsolutePathBuf::try_from(denied_file.as_path()).expect("absolute file"), - ], + deny_read_patterns: vec![DenyReadPattern::from( + denied_file.to_string_lossy().into_owned(), + )], }; let args = create_filesystem_args(&policy, temp_dir.path()).expect("filesystem args"); diff --git a/codex-rs/linux-sandbox/src/landlock.rs b/codex-rs/linux-sandbox/src/landlock.rs index 32764d5904..dfeaafd47a 100644 --- a/codex-rs/linux-sandbox/src/landlock.rs +++ b/codex-rs/linux-sandbox/src/landlock.rs @@ -65,9 +65,9 @@ pub(crate) fn apply_sandbox_policy_to_current_thread( install_network_seccomp_filter_on_current_thread(mode)?; } - if apply_landlock_fs && sandbox_policy.has_denied_read_paths() { + if apply_landlock_fs && sandbox_policy.has_denied_read_patterns() { return Err(CodexErr::UnsupportedOperation( - "Filesystem deny_read paths are not supported by the legacy Linux Landlock filesystem backend." + "Filesystem deny_read patterns are not supported by the legacy Linux Landlock filesystem backend." .to_string(), )); } diff --git a/codex-rs/linux-sandbox/src/linux_run_main.rs b/codex-rs/linux-sandbox/src/linux_run_main.rs index a16be37399..347d479871 100644 --- a/codex-rs/linux-sandbox/src/linux_run_main.rs +++ b/codex-rs/linux-sandbox/src/linux_run_main.rs @@ -116,7 +116,7 @@ pub fn run_main() -> ! { if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy - && !sandbox_policy.has_denied_read_paths() + && !sandbox_policy.has_denied_read_patterns() { if let Err(e) = apply_sandbox_policy_to_current_thread( &sandbox_policy, diff --git a/codex-rs/linux-sandbox/tests/suite/landlock.rs b/codex-rs/linux-sandbox/tests/suite/landlock.rs index fd8c8eb5a0..e7e829b48e 100644 --- a/codex-rs/linux-sandbox/tests/suite/landlock.rs +++ b/codex-rs/linux-sandbox/tests/suite/landlock.rs @@ -88,7 +88,7 @@ async fn run_cmd_result_with_writable_roots( .iter() .map(|p| AbsolutePathBuf::try_from(p.as_path()).unwrap()) .collect(), - deny_read_paths: vec![], + deny_read_patterns: vec![], read_only_access: Default::default(), network_access, // Exclude tmp-related folders from writable roots because we need a diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index ef7f0f81d5..6fc719e35f 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1371,7 +1371,7 @@ mod tests { let policy = SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: Default::default(), - deny_read_paths: vec![], + deny_read_patterns: vec![], 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 8a674a0ad1..5867ab262d 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -558,6 +558,42 @@ impl ReadOnlyAccess { } } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)] +#[serde(transparent)] +/// A deny-only filesystem read pattern. +/// +/// Literal entries deny that exact path (or subtree for directory paths). +/// Pattern entries are interpreted by platform-specific sandbox backends. +pub struct DenyReadPattern(String); + +impl DenyReadPattern { + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn contains_glob(&self) -> bool { + self.0.chars().any(|ch| matches!(ch, '*' | '?' | '[')) + } + + pub fn literal_path(&self) -> Option { + (!self.contains_glob()) + .then(|| AbsolutePathBuf::try_from(self.0.as_str()).ok()) + .flatten() + } +} + +impl From for DenyReadPattern { + fn from(value: String) -> Self { + Self(value) + } +} + +impl From<&str> for DenyReadPattern { + fn from(value: &str) -> Self { + Self(value.to_string()) + } +} + /// Determines execution restrictions for model shell commands. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, JsonSchema, TS)] #[strum(serialize_all = "kebab-case")] @@ -576,9 +612,9 @@ pub enum SandboxPolicy { skip_serializing_if = "ReadOnlyAccess::has_full_disk_read_access" )] access: ReadOnlyAccess, - /// Paths that must not be readable, even when broad read access is otherwise allowed. + /// Patterns that must not be readable, even when broad read access is otherwise allowed. #[serde(default, skip_serializing_if = "Vec::is_empty")] - deny_read_paths: Vec, + deny_read_patterns: Vec, }, /// Indicates the process is already in an external sandbox. Allows full @@ -588,9 +624,9 @@ pub enum SandboxPolicy { /// Whether the external sandbox permits outbound network traffic. #[serde(default)] network_access: NetworkAccess, - /// Paths that must not be readable, even when broad read access is otherwise allowed. + /// Patterns that must not be readable, even when broad read access is otherwise allowed. #[serde(default, skip_serializing_if = "Vec::is_empty")] - deny_read_paths: Vec, + deny_read_patterns: Vec, }, /// Same as `ReadOnly` but additionally grants write access to the current @@ -609,9 +645,9 @@ pub enum SandboxPolicy { )] read_only_access: ReadOnlyAccess, - /// Paths that must not be readable, even when broad read access is otherwise allowed. + /// Patterns that must not be readable, even when broad read access is otherwise allowed. #[serde(default, skip_serializing_if = "Vec::is_empty")] - deny_read_paths: Vec, + deny_read_patterns: Vec, /// When set to `true`, outbound network access is allowed. `false` by /// default. @@ -675,7 +711,7 @@ impl SandboxPolicy { pub fn new_read_only_policy() -> Self { SandboxPolicy::ReadOnly { access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], } } @@ -686,7 +722,7 @@ impl SandboxPolicy { SandboxPolicy::WorkspaceWrite { writable_roots: vec![], read_only_access: ReadOnlyAccess::FullAccess, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: false, exclude_slash_tmp: false, @@ -729,8 +765,8 @@ impl SandboxPolicy { SandboxPolicy::DangerFullAccess => true, SandboxPolicy::ExternalSandbox { network_access, - deny_read_paths, - } => network_access.is_enabled() && !deny_read_paths.is_empty(), + deny_read_patterns, + } => network_access.is_enabled() && !deny_read_patterns.is_empty(), SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => false, } } @@ -749,57 +785,61 @@ impl SandboxPolicy { } } - pub fn denied_read_paths(&self) -> Vec { + pub fn deny_read_patterns(&self) -> Vec { match self { SandboxPolicy::ReadOnly { - deny_read_paths, .. + deny_read_patterns, .. } | SandboxPolicy::ExternalSandbox { - deny_read_paths, .. + deny_read_patterns, .. } | SandboxPolicy::WorkspaceWrite { - deny_read_paths, .. - } => deny_read_paths.clone(), + deny_read_patterns, .. + } => deny_read_patterns.clone(), SandboxPolicy::DangerFullAccess => Vec::new(), } } - pub fn has_denied_read_paths(&self) -> bool { + pub fn has_denied_read_patterns(&self) -> bool { match self { SandboxPolicy::ReadOnly { - deny_read_paths, .. + deny_read_patterns, .. } | SandboxPolicy::ExternalSandbox { - deny_read_paths, .. + deny_read_patterns, .. } | SandboxPolicy::WorkspaceWrite { - deny_read_paths, .. - } => !deny_read_paths.is_empty(), + deny_read_patterns, .. + } => !deny_read_patterns.is_empty(), SandboxPolicy::DangerFullAccess => false, } } - pub fn append_deny_read_paths(&mut self, new_paths: &[AbsolutePathBuf]) { - if new_paths.is_empty() { + pub fn has_denied_read_restrictions(&self) -> bool { + self.has_denied_read_patterns() + } + + pub fn append_deny_read_patterns(&mut self, new_patterns: &[DenyReadPattern]) { + if new_patterns.is_empty() { return; } - let target_paths = match self { + let target_patterns = match self { SandboxPolicy::ReadOnly { - deny_read_paths, .. + deny_read_patterns, .. } | SandboxPolicy::ExternalSandbox { - deny_read_paths, .. + deny_read_patterns, .. } | SandboxPolicy::WorkspaceWrite { - deny_read_paths, .. - } => deny_read_paths, + deny_read_patterns, .. + } => deny_read_patterns, SandboxPolicy::DangerFullAccess => return, }; - target_paths.extend(new_paths.iter().cloned()); + target_patterns.extend(new_patterns.iter().cloned()); let mut seen = HashSet::new(); - target_paths.retain(|path| seen.insert(path.to_path_buf())); + target_patterns.retain(|pattern| seen.insert(pattern.as_str().to_string())); } /// Returns the list of readable roots (tailored to the current working @@ -842,7 +882,7 @@ impl SandboxPolicy { exclude_tmpdir_env_var, exclude_slash_tmp, network_access: _, - deny_read_paths: _, + deny_read_patterns: _, } => { // Start from explicitly configured writable roots. let mut roots: Vec = writable_roots.clone(); @@ -3146,21 +3186,21 @@ mod tests { fn external_sandbox_reports_full_access_flags() { let restricted = SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Restricted, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; assert!(restricted.has_full_disk_write_access()); assert!(!restricted.has_full_network_access()); let enabled = SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], }; assert!(enabled.has_full_disk_write_access()); assert!(enabled.has_full_network_access()); } #[test] - fn append_deny_read_paths_ignores_danger_full_access() { + fn append_deny_read_patterns_ignores_danger_full_access() { let denied_path = if cfg!(windows) { AbsolutePathBuf::try_from(r"C:\sensitive\secret.txt").expect("absolute path") } else { @@ -3168,7 +3208,8 @@ mod tests { }; let mut policy = SandboxPolicy::DangerFullAccess; - policy.append_deny_read_paths(std::slice::from_ref(&denied_path)); + let denied_pattern = DenyReadPattern::from(denied_path.to_string_lossy().into_owned()); + policy.append_deny_read_patterns(std::slice::from_ref(&denied_pattern)); assert_eq!(policy, SandboxPolicy::DangerFullAccess); } @@ -3185,7 +3226,7 @@ mod tests { policy, SandboxPolicy::ExternalSandbox { network_access: NetworkAccess::Enabled, - deny_read_paths: vec![], + deny_read_patterns: vec![], } ); } @@ -3223,7 +3264,7 @@ mod tests { include_platform_defaults: false, readable_roots: vec![], }, - deny_read_paths: vec![], + deny_read_patterns: vec![], network_access: false, exclude_tmpdir_env_var: true, exclude_slash_tmp: false,