SandboxPolicy完整参考
SandboxPolicy 在 rust-v0.150.0 中仍然存在,但它不再是运行时权限的唯一权威。新的主模型是 PermissionProfile,文件和网络分别由 FileSystemSandboxPolicy 与 NetworkSandboxPolicy 表达;旧枚举主要服务配置兼容、协议兼容和旧客户端显示。
因此阅读顺序必须反过来:先理解 canonical profile,再看四个 legacy 变体如何投影。否则会漏掉旧枚举无法表达的能力,例如 deny-read、glob、symbolic project roots、glob scan depth,以及“Codex 管理、禁用、外部管理”三种 enforcement owner。
本文承接权限审批沙箱三层模型。前文解释批准如何进入策略与强制;本文专门回答字段参考问题:四个变体各自表示什么、转换后得到什么、反向转换何时有损或失败。
实线表示权威数据流;虚线表示为了兼容旧接口而生成的近似表示。反向投影不保证能保留 canonical policy 的全部细节。
1. Legacy枚举
旧枚举有四个变体。DangerFullAccess 没有字段;ReadOnly 与 WorkspaceWrite 使用 bool 网络开关;ExternalSandbox 使用显式 NetworkAccess 枚举,以保留“文件由外部强制、网络仍独立配置”的组合。
源码位置:codex-rs/protocol/src/protocol.rs :: SandboxPolicy, NetworkAccess
pub enum NetworkAccess {
#[default]
Restricted,
Enabled,
}
pub enum SandboxPolicy {
#[serde(rename = "danger-full-access")]
DangerFullAccess,
#[serde(rename = "read-only")]
ReadOnly {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
network_access: bool,
},
#[serde(rename = "external-sandbox")]
ExternalSandbox {
#[serde(default)]
network_access: NetworkAccess,
},
#[serde(rename = "workspace-write")]
WorkspaceWrite {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
writable_roots: Vec<AbsolutePathBuf>,
#[serde(default)]
network_access: bool,
#[serde(default)]
exclude_tmpdir_env_var: bool,
#[serde(default)]
exclude_slash_tmp: bool,
},
}所有 legacy 变体都允许全盘读取。ReadOnly 的“只读”指禁止写入,不是只能读取 workspace;WorkspaceWrite 则是在全盘可读基础上开放有限写入。
源码位置:codex-rs/protocol/src/protocol.rs :: SandboxPolicy access helpers
pub fn has_full_disk_read_access(&self) -> bool {
true
}
pub fn has_full_disk_write_access(&self) -> bool {
match self {
SandboxPolicy::DangerFullAccess => true,
SandboxPolicy::ExternalSandbox { .. } => true,
SandboxPolicy::ReadOnly { .. } => false,
SandboxPolicy::WorkspaceWrite { .. } => false,
}
}
pub fn has_full_network_access(&self) -> bool {
match self {
SandboxPolicy::DangerFullAccess => true,
SandboxPolicy::ExternalSandbox { network_access } => network_access.is_enabled(),
SandboxPolicy::ReadOnly { network_access, .. } => *network_access,
SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
}
}因此不能从名称直接推出网络能力:ReadOnly { network_access: true } 仍可联网;ExternalSandbox { Restricted } 虽然不由 Codex 限制磁盘,却保持网络受限。
2. Canonical模型
canonical PermissionProfile 把 enforcement owner 与权限内容分开。Managed 表示 Codex 或 executor 构造 sandbox;Disabled 表示不应用外层文件 sandbox;External 表示文件隔离由外部 caller 负责。
源码位置:codex-rs/protocol/src/models.rs :: PermissionProfile
pub enum PermissionProfile {
Managed {
file_system: ManagedFileSystemPermissions,
network: NetworkSandboxPolicy,
},
Disabled,
External {
network: NetworkSandboxPolicy,
},
}文件策略还能独立表示 Restricted、Unrestricted、ExternalSandbox,并携带任意 entry 列表。
源码位置:codex-rs/protocol/src/permissions.rs :: FileSystemAccessMode, FileSystemSandboxKind, FileSystemSandboxPolicy
pub enum FileSystemAccessMode {
Read,
Write,
#[serde(alias = "none")]
Deny,
}
pub enum FileSystemSandboxKind {
Restricted,
Unrestricted,
ExternalSandbox,
}
pub struct FileSystemSandboxPolicy {
pub kind: FileSystemSandboxKind,
pub glob_scan_max_depth: Option<usize>,
pub entries: Vec<FileSystemSandboxEntry>,
}这就是 legacy enum 与 canonical profile 的能力差:旧枚举只能生成固定形状,canonical policy 可以对具体 path、glob 与 special path 分别授予 Read、Write 或 Deny。
3. 正向投影
legacy policy 转为 profile 时,先分别生成 enforcement、文件策略和网络策略,再组合成 PermissionProfile。
源码位置:codex-rs/protocol/src/models.rs :: SandboxEnforcement::from_legacy_sandbox_policy
pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy) -> Self {
match sandbox_policy {
SandboxPolicy::DangerFullAccess => Self::Disabled,
SandboxPolicy::ExternalSandbox { .. } => Self::External,
SandboxPolicy::ReadOnly { .. } | SandboxPolicy::WorkspaceWrite { .. } => Self::Managed,
}
}源码位置:codex-rs/protocol/src/permissions.rs :: From<&SandboxPolicy> for FileSystemSandboxPolicy, NetworkSandboxPolicy
impl From<&SandboxPolicy> for NetworkSandboxPolicy {
fn from(value: &SandboxPolicy) -> Self {
if value.has_full_network_access() {
NetworkSandboxPolicy::Enabled
} else {
NetworkSandboxPolicy::Restricted
}
}
}
impl From<&SandboxPolicy> for FileSystemSandboxPolicy {
fn from(value: &SandboxPolicy) -> Self {
match value {
SandboxPolicy::DangerFullAccess => FileSystemSandboxPolicy::unrestricted(),
SandboxPolicy::ExternalSandbox { .. } => FileSystemSandboxPolicy::external_sandbox(),
SandboxPolicy::ReadOnly { .. } => {
FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
FileSystemAccessMode::Read,
)])
}
SandboxPolicy::WorkspaceWrite {
writable_roots,
exclude_tmpdir_env_var,
exclude_slash_tmp,
..
} => FileSystemSandboxPolicy::workspace_write(
writable_roots,
*exclude_tmpdir_env_var,
*exclude_slash_tmp,
),
}
}
}最终 profile 使用 cwd-aware 转换,因为 WorkspaceWrite 的 cwd 与临时目录默认值只有在给定执行目录后才能完整确定。
源码位置:codex-rs/protocol/src/models.rs :: PermissionProfile::from_legacy_sandbox_policy_for_cwd
pub fn from_legacy_sandbox_policy_for_cwd(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
Self::from_runtime_permissions_with_enforcement(
SandboxEnforcement::from_legacy_sandbox_policy(sandbox_policy),
&FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd),
NetworkSandboxPolicy::from(sandbox_policy),
)
}映射结果如下:
| Legacy变体 | Enforcement | 文件策略 | 网络策略 |
|---|---|---|---|
DangerFullAccess | Disabled | Unrestricted | Enabled |
ReadOnly | Managed | Restricted:Root Read | bool 映射 |
WorkspaceWrite | Managed | Restricted:Root Read + project/temp/extra writes | bool 映射 |
ExternalSandbox | External | ExternalSandbox | enum 映射 |
4. Workspace映射
WorkspaceWrite 的 canonical 形状不是简单的 writable root 数组。转换首先增加 Root Read,然后增加符号化 project roots Write;按配置添加 /tmp、TMPDIR 与额外 writable roots,最后为 .git、.agents、.codex 添加默认只读保护。
源码位置:codex-rs/protocol/src/permissions.rs :: FileSystemSandboxPolicy::workspace_write
let mut entries = vec![FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
FileSystemAccessMode::Read,
)];
entries.push(FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
},
FileSystemAccessMode::Write,
));
if !exclude_slash_tmp {
entries.push(FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::SlashTmp,
},
FileSystemAccessMode::Write,
));
}
if !exclude_tmpdir_env_var {
entries.push(FileSystemSandboxEntry::new(
FileSystemPath::Special {
value: FileSystemSpecialPath::Tmpdir,
},
FileSystemAccessMode::Write,
));
}源码位置:codex-rs/protocol/src/permissions.rs :: FileSystemSandboxPolicy::workspace_write
entries.extend(
writable_roots
.iter()
.cloned()
.map(|path| FileSystemSandboxEntry::new(path.into(), FileSystemAccessMode::Write)),
);
append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".git");
append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".agents");
append_default_read_only_project_root_subpath_if_no_explicit_rule(&mut entries, ".codex");
for writable_root in writable_roots {
for protected_path in default_read_only_subpaths_for_writable_root(
writable_root,
/*protect_missing_dot_codex*/ false,
) {
append_default_read_only_path_if_no_explicit_rule(&mut entries, protected_path);
}
}
FileSystemSandboxPolicy::restricted(entries)符号化 ProjectRoots 在 TurnEnvironment 确定 workspace roots 后才物化。这使一个 profile 可以安全复用到本地 Unix、远程 Windows 或不同 workspace,而不在配置加载时绑定错误的主机路径。
5. 四变体语义
5.1 DangerFullAccess
正向投影为 PermissionProfile::Disabled。它表示不应用外层文件 sandbox,并将网络视为 Enabled;但进程仍可能被外部容器、父进程或基础设施限制。
5.2 ReadOnly
正向投影为 Managed + Restricted,文件 entry 是 Root Read。这仍然是全盘读,不是 workspace-only read;网络由 bool 单独决定。
5.3 WorkspaceWrite
正向投影为 Managed + Restricted。全盘读仍保留,写入只授予 project roots、可选 temp 和显式 roots;默认元数据保护使“workspace 可写”不自动包含 .git、.agents、.codex。
5.4 ExternalSandbox
正向投影为 PermissionProfile::External,文件 kind 是 ExternalSandbox,网络仍由 NetworkAccess 映射。它与 DangerFullAccess 的文件能力看起来都可全盘读写,但 enforcement owner 不同:一个禁用外层强制,一个声明外部 caller 强制。
6. Cwd相关转换
legacy WorkspaceWrite 的 cwd 是隐式 writable root。FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd 会在给定 cwd 后补齐保护路径;额外 writable roots 同样生成默认只读子路径。
源码位置:codex-rs/protocol/src/permissions.rs :: FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd
let mut file_system_policy = Self::from(sandbox_policy);
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = sandbox_policy {
if let Ok(cwd_root) = AbsolutePathBuf::from_absolute_path(cwd) {
for protected_path in default_read_only_subpaths_for_writable_root(
&cwd_root, /*protect_missing_dot_codex*/ true,
) {
append_default_read_only_path_if_no_explicit_rule(
&mut file_system_policy.entries,
protected_path,
);
}
}
for writable_root in writable_roots {
for protected_path in default_read_only_subpaths_for_writable_root(
writable_root,
/*protect_missing_dot_codex*/ false,
) {
append_default_read_only_path_if_no_explicit_rule(
&mut file_system_policy.entries,
protected_path,
);
}
}
}
file_system_policy相同的 legacy policy 在不同 cwd 下可以产生不同 canonical entries。保存或比较权限时必须同时知道 cwd,不能只比较 enum JSON。
7. 反向投影
canonical profile 回写 legacy enum 时,Disabled 与 External 可以直接映射;Managed profile 先取出文件和网络策略,再尝试 FileSystemSandboxPolicy::to_legacy_sandbox_policy。
源码位置:codex-rs/protocol/src/models.rs :: PermissionProfile::to_legacy_sandbox_policy
match self {
Self::Managed {
file_system,
network,
} => file_system
.to_sandbox_policy()
.to_legacy_sandbox_policy(*network, cwd),
Self::Disabled => Ok(SandboxPolicy::DangerFullAccess),
Self::External { network } => Ok(SandboxPolicy::ExternalSandbox {
network_access: if network.is_enabled() {
crate::protocol::NetworkAccess::Enabled
} else {
crate::protocol::NetworkAccess::Restricted
},
}),
}Restricted policy 只有在符合旧形状时才能无损回写:全盘写可变成 DangerFullAccess 或 ExternalSandbox;project root 可写可变成 WorkspaceWrite;只有读权限可变成 ReadOnly。若 canonical policy 请求 workspace 之外的独立写入,但没有 project-root write,旧枚举没有对应表达,会返回 InvalidInput。
源码位置:codex-rs/protocol/src/permissions.rs :: FileSystemSandboxPolicy::to_legacy_sandbox_policy
if workspace_root_writable {
SandboxPolicy::WorkspaceWrite {
writable_roots: dedup_absolute_paths(
writable_roots,
/*normalize_effective_paths*/ false,
),
network_access: network_policy.is_enabled(),
exclude_tmpdir_env_var: !tmpdir_writable,
exclude_slash_tmp: !slash_tmp_writable,
}
} else if unbridgeable_root_write
|| !writable_roots.is_empty()
|| tmpdir_writable
|| (cfg!(unix) && slash_tmp_writable)
{
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"permissions profile requests filesystem writes outside the workspace root, which is not supported until the runtime enforces FileSystemSandboxPolicy directly",
));
} else {
SandboxPolicy::ReadOnly {
network_access: network_policy.is_enabled(),
}
}Deny 与 glob 也没有 legacy 字段。即使某些 allow 形状能回写,不能假设 round trip 会保留细粒度限制;更新 legacy 配置时,Core 使用专门函数把既有 Deny entry 重新附加到重建 policy。
源码位置:codex-rs/protocol/src/permissions.rs :: from_legacy_sandbox_policy_preserving_deny_entries
let mut rebuilt = Self::from_legacy_sandbox_policy_for_cwd(sandbox_policy, cwd);
if !matches!(rebuilt.kind, FileSystemSandboxKind::Restricted) {
return rebuilt;
}
rebuilt.glob_scan_max_depth = existing.glob_scan_max_depth;
for deny_entry in existing
.entries
.iter()
.filter(|entry| entry.access == FileSystemAccessMode::Deny)
{
if !rebuilt.entries.iter().any(|entry| entry == deny_entry) {
rebuilt.entries.push(deny_entry.clone());
}
}
rebuilt8. 兼容回退
部分旧消费者必须得到一个 SandboxPolicy,即使 canonical profile 无法精确表达。compatibility_sandbox_policy_for_permission_profile 先尝试精确回写;失败后生成近似 WorkspaceWrite,把当前可写 roots、temp 状态和网络开关投影进去。
源码位置:codex-rs/sandboxing/src/manager.rs :: compatibility_sandbox_policy_for_permission_profile
pub fn compatibility_sandbox_policy_for_permission_profile(
permissions: &PermissionProfile,
cwd: &Path,
) -> SandboxPolicy {
permissions
.to_legacy_sandbox_policy(cwd)
.unwrap_or_else(|_| {
let (file_system_policy, network_policy) = permissions.to_runtime_permissions();
compatibility_workspace_write_policy(file_system_policy, network_policy, cwd)
})
}这是显示与旧 API 兼容,不是 canonical policy 的替代品。近似 WorkspaceWrite 无法携带 unreadable roots、deny globs 或 scan depth;运行时消费者必须继续使用 PermissionProfile 和 split policy。
9. Turn消费者
TurnContext 已明确把 permission_profile 作为权威来源。sandbox_policy() 调用 compatibility projection;序列化 TurnContextItem 时同时发布 legacy sandbox policy、canonical profile,以及在 split filesystem 与 legacy 投影不等价时发布单独的 file_system_sandbox_policy。
源码位置:codex-rs/core/src/session/turn_context.rs :: TurnContext::sandbox_policy, to_turn_context_item
pub(crate) fn sandbox_policy(&self) -> SandboxPolicy {
#[allow(deprecated)]
codex_sandboxing::compatibility_sandbox_policy_for_permission_profile(
&self.permission_profile(),
&self.cwd,
)
}源码位置:codex-rs/core/src/session/turn_context.rs :: TurnContext::to_turn_context_item
sandbox_policy: self.sandbox_policy(),
permission_profile: Some(self.permission_profile()),
active_permission_profile: self.environments.primary().map_or_else(
|| self.config.permissions.active_permission_profile(),
TurnEnvironment::active_permission_profile,
),
network: self.turn_context_network_item(),
file_system_sandbox_policy: self.non_legacy_file_system_sandbox_policy(),权限消费者应优先读取 permission_profile,需要兼容旧客户端时才读取 sandbox_policy。file_system_sandbox_policy 只在 split policy 与 legacy 投影不等价时出现。
10. 变体矩阵
| Legacy变体 | 文件读取 | 文件写入 | Enforcement owner | 网络默认 | Canonical结果 |
|---|---|---|---|---|---|
DangerFullAccess | 全盘 | 全盘 | Disabled | Enabled | PermissionProfile::Disabled |
ReadOnly | 全盘 | 无 | Managed | Restricted | Root Read restricted profile |
WorkspaceWrite | 全盘 | project/temp/extra roots | Managed | Restricted | entry-based restricted profile |
ExternalSandbox | 外部定义 | 外部定义 | External | Restricted | PermissionProfile::External |
“文件读取全盘”是 legacy 语义;canonical profile 可以用 Minimal、具体 readable roots 和 Deny 进一步缩小读取范围。新配置和新消费者不应被这张 legacy 矩阵限制。
11. 测试反推边界
协议层测试覆盖 legacy/canonical round trip、workspace root 物化、Deny 保留和不可表示形状;Core 测试覆盖配置与 TurnContext 双字段发布:
cd codex-rs
cargo test -p codex-protocol --lib permissions::tests:: -- --nocapture --test-threads=1
cargo test -p codex-protocol --lib models::tests::permission_profile -- --nocapture --test-threads=1
cargo test -p codex-core --lib config::tests::legacy_sandbox_mode_builds_profiles_with_compatible_projection -- --nocapture --test-threads=1
cargo test -p codex-core --lib session::tests::turn_context_item_ -- --nocapture --test-threads=1四组测试分别通过 43、8、1、4 项,共 56 项。它们验证路径权限、legacy/profile round trip、配置投影和 TurnContext 双字段发布;没有启动 Seatbelt、Linux sandbox 或 Windows token 执行真实命令。
legacy_bridge_preserves_explicit_deny_entries 先从 legacy policy 重建 allow side,再要求原有 Deny entry 仍存在。关键断言是兼容更新不会删除旧枚举无法表示的读取禁区;它不说明 legacy JSON 自己包含 Deny。
permission_profile_round_trip_preserves_disabled_sandbox 与 external 对应测试分别断言 Disabled 回写 DangerFullAccess、External 回写 ExternalSandbox,并在重新解析后保留 enforcement owner。它们验证两种看似全盘访问的模式不会互相混淆。
turn_context_item_stores_split_file_system_sandbox_policy_when_different 构造无法由 legacy policy 完整表达的 canonical filesystem,断言 TurnContextItem 除 compatibility sandbox_policy 外还带 split policy。它验证消费者有机会获得完整限制,不验证旧客户端会主动使用新字段。
测试可以证明转换和序列化语义,不能证明所有旧客户端都正确处理额外字段,也不能替代 Seatbelt、Linux sandbox 或 Windows executor 的平台强制测试。
阅读或调试时,看到 SandboxPolicy 先问它来自用户 legacy 输入,还是从 canonical profile 派生的 compatibility 值。只有前者能代表原始配置意图;后者可能已经是近似显示。真正决定执行权限的仍是 PermissionProfile、FileSystemSandboxPolicy 与 NetworkSandboxPolicy。
下一篇ApprovalPolicy完整参考将继续解释 AskForApproval、granular categories、reviewer 与 exec-policy prompt 如何共同决定是否进入审批流程。
