Codex worktree snapshot: new-branch-cleanup

Co-authored-by: Codex
This commit is contained in:
viyatb-oai
2026-03-07 15:07:15 -08:00
co-authored by Codex
parent 685883a321
commit eb04235b41
49 changed files with 683 additions and 439 deletions
+1
View File
@@ -1792,6 +1792,7 @@ dependencies = [
"env-flags",
"eventsource-stream",
"futures",
"globset",
"http 1.4.0",
"iana-time-zone",
"image",
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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"
},
@@ -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;
@@ -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<AbsolutePathBuf>, } | { "type": "external-sandbox",
deny_read_patterns?: Array<DenyReadPattern>, } | { "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<AbsolutePathBuf>, } | { "type": "workspace-write",
deny_read_patterns?: Array<DenyReadPattern>, } | { "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<AbsolutePathBuf>,
*/
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<AbsolutePathBuf>,
deny_read_patterns?: Array<DenyReadPattern>,
/**
* When set to `true`, outbound network access is allowed. `false` by
* default.
@@ -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";
@@ -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;
@@ -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<AbsolutePathBuf>, } | { "type": "externalSandbox", networkAccess: NetworkAccess, denyReadPaths: Array<AbsolutePathBuf>, } | { "type": "workspaceWrite", writableRoots: Array<AbsolutePathBuf>, readOnlyAccess: ReadOnlyAccess, denyReadPaths: Array<AbsolutePathBuf>, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, };
export type SandboxPolicy = { "type": "dangerFullAccess" } | { "type": "readOnly", access: ReadOnlyAccess, denyReadPatterns: Array<DenyReadPattern>, } | { "type": "externalSandbox", networkAccess: NetworkAccess, denyReadPatterns: Array<DenyReadPattern>, } | { "type": "workspaceWrite", writableRoots: Array<AbsolutePathBuf>, readOnlyAccess: ReadOnlyAccess, denyReadPatterns: Array<DenyReadPattern>, networkAccess: boolean, excludeTmpdirEnvVar: boolean, excludeSlashTmp: boolean, };
@@ -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";
+54 -24
View File
@@ -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<CoreReadOnlyAccess> for ReadOnlyAccess {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash, JsonSchema, TS)]
#[serde(transparent)]
#[ts(export_to = "v2/")]
pub struct DenyReadPattern(String);
impl From<CoreDenyReadPattern> for DenyReadPattern {
fn from(value: CoreDenyReadPattern) -> Self {
Self(value.as_str().to_string())
}
}
impl From<DenyReadPattern> 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<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
},
#[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<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
},
#[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<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
#[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<codex_protocol::protocol::SandboxPolicy> 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![],
}
);
}
+6 -2
View File
@@ -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<Self, String> {
@@ -280,7 +280,7 @@ fn deserialize_absolute_path(input: &str) -> Result<AbsolutePathBuf, String> {
}
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 {
+1
View File
@@ -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 }
+42 -29
View File
@@ -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::<Vec<_>>();
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!(
+1 -1
View File
@@ -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,
+30 -28
View File
@@ -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<String>,
pub timezone: Option<String>,
pub network: Option<NetworkContext>,
pub deny_read_paths: Vec<AbsolutePathBuf>,
pub deny_read_patterns: Vec<DenyReadPattern>,
pub subagents: Option<String>,
}
@@ -34,7 +34,7 @@ impl EnvironmentContext {
current_date: Option<String>,
timezone: Option<String>,
network: Option<NetworkContext>,
deny_read_paths: Vec<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
subagents: Option<String>,
) -> 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 {
/// <environment_context>
/// <cwd>...</cwd>
/// <shell>...</shell>
/// <deny_read_patterns>
/// <pattern>...</pattern>
/// </deny_read_patterns>
/// </environment_context>
/// ```
pub fn serialize_to_xml(self) -> String {
@@ -206,12 +209,12 @@ impl EnvironmentContext {
// lines.push(" <network enabled=\"false\" />".to_string());
}
}
if !self.deny_read_paths.is_empty() {
lines.push(" <deny_read_paths>".to_string());
for path in &self.deny_read_paths {
lines.push(format!(" <path>{}</path>", path.to_string_lossy()));
if !self.deny_read_patterns.is_empty() {
lines.push(" <deny_read_patterns>".to_string());
for pattern in &self.deny_read_patterns {
lines.push(format!(" <pattern>{}</pattern>", pattern.as_str()));
}
lines.push(" </deny_read_paths>".to_string());
lines.push(" </deny_read_patterns>".to_string());
}
if let Some(subagents) = self.subagents {
lines.push(" <subagents>".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 {
<shell>bash</shell>
<current_date>2026-02-26</current_date>
<timezone>America/Los_Angeles</timezone>
<deny_read_paths>
<path>/repo/.gitconfig</path>
<path>/repo/.ssh</path>
</deny_read_paths>
<deny_read_patterns>
<pattern>/repo/.gitconfig</pattern>
<pattern>/repo/.ssh</pattern>
</deny_read_patterns>
</environment_context>"#,
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(),
+2 -1
View File
@@ -159,7 +159,8 @@ pub async fn process_exec_tool_call(
) -> Result<ExecToolCallOutput> {
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 {
+4 -2
View File
@@ -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
+111 -171
View File
@@ -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<AbsolutePathBuf> {
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<AbsolutePathBuf> {
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<PathBuf> {
@@ -117,24 +135,37 @@ fn push_unique_absolute(candidates: &mut Vec<AbsolutePathBuf>, candidate: Absolu
}
}
fn expand_glob_pattern(pattern: &FilesystemDenyReadPattern, expanded: &mut Vec<AbsolutePathBuf>) {
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<AbsolutePathBuf> {
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<AbsolutePathBuf>) {
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<char> = pattern.chars().collect();
let candidate_chars: Vec<char> = 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
);
}
}
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -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,
+4 -2
View File
@@ -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,
+10 -10
View File
@@ -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,
+198 -14
View File
@@ -186,20 +186,172 @@ fn normalize_path_for_sandbox(path: &Path) -> Option<AbsolutePathBuf> {
normalized_path.or(Some(absolute_path))
}
fn normalize_glob_pattern_for_sandbox(pattern: &str) -> Option<String> {
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<AbsolutePathBuf> {
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<char> = 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(&regex)
));
}
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,
+1 -1
View File
@@ -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,
+4 -4
View File
@@ -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,
+9 -5
View File
@@ -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(),
),
],
};
+8 -3
View File
@@ -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,
+1 -1
View File
@@ -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,
@@ -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,
+1 -1
View File
@@ -364,7 +364,7 @@ mod tests {
AskForApproval::OnRequest,
&SandboxPolicy::ExternalSandbox {
network_access: NetworkAccess::Restricted,
deny_read_paths: vec![],
deny_read_patterns: vec![],
},
),
ExecApprovalRequirement::Skip {
+1 -1
View File
@@ -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(),
}
}
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -682,7 +682,7 @@ fn scenarios() -> Vec<ScenarioSpec> {
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| {
@@ -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,
+2 -2
View File
@@ -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,
@@ -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![],
}
}
+2 -2
View File
@@ -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,
+22 -22
View File
@@ -91,7 +91,7 @@ pub(crate) fn create_bwrap_command_args(
) -> Result<Vec<String>> {
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<PathBuf> = 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<PathBuf> = writable_roots
@@ -467,10 +467,10 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
#[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");
+2 -2
View File
@@ -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(),
));
}
+1 -1
View File
@@ -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,
@@ -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
+1 -1
View File
@@ -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,
+77 -36
View File
@@ -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<AbsolutePathBuf> {
(!self.contains_glob())
.then(|| AbsolutePathBuf::try_from(self.0.as_str()).ok())
.flatten()
}
}
impl From<String> 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<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
},
/// 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<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
},
/// 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<AbsolutePathBuf>,
deny_read_patterns: Vec<DenyReadPattern>,
/// 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<AbsolutePathBuf> {
pub fn deny_read_patterns(&self) -> Vec<DenyReadPattern> {
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<AbsolutePathBuf> = 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,