mirror of
https://github.com/openai/codex.git
synced 2026-11-03 19:53:25 +00:00
Codex worktree snapshot: new-branch-cleanup
Co-authored-by: Codex
This commit is contained in:
Generated
+1
@@ -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";
|
||||
|
||||
@@ -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![],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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(®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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -364,7 +364,7 @@ mod tests {
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::ExternalSandbox {
|
||||
network_access: NetworkAccess::Restricted,
|
||||
deny_read_paths: vec![],
|
||||
deny_read_patterns: vec![],
|
||||
},
|
||||
),
|
||||
ExecApprovalRequirement::Skip {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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![],
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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(),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user