Skip to content

SandboxPolicy完整参考

解释 legacy SandboxPolicy 四个变体如何投影为 PermissionProfile、文件与网络策略,以及细粒度权限为何无法无损回写旧枚举。

基于rust-v0.150.0
CodexRustSecuritySandbox

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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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文件策略网络策略
DangerFullAccessDisabledUnrestrictedEnabled
ReadOnlyManagedRestricted:Root Readbool 映射
WorkspaceWriteManagedRestricted:Root Read + project/temp/extra writesbool 映射
ExternalSandboxExternalExternalSandboxenum 映射

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

rust
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

rust
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

rust
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

rust
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

rust
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

rust
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());
    }
}

rebuilt

8. 兼容回退 ​

部分旧消费者必须得到一个 SandboxPolicy,即使 canonical profile 无法精确表达。compatibility_sandbox_policy_for_permission_profile 先尝试精确回写;失败后生成近似 WorkspaceWrite,把当前可写 roots、temp 状态和网络开关投影进去。

源码位置:codex-rs/sandboxing/src/manager.rs :: compatibility_sandbox_policy_for_permission_profile

rust
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

rust
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

rust
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全盘全盘DisabledEnabledPermissionProfile::Disabled
ReadOnly全盘无ManagedRestrictedRoot Read restricted profile
WorkspaceWrite全盘project/temp/extra rootsManagedRestrictedentry-based restricted profile
ExternalSandbox外部定义外部定义ExternalRestrictedPermissionProfile::External

“文件读取全盘”是 legacy 语义;canonical profile 可以用 Minimal、具体 readable roots 和 Deny 进一步缩小读取范围。新配置和新消费者不应被这张 legacy 矩阵限制。

11. 测试反推边界 ​

协议层测试覆盖 legacy/canonical round trip、workspace root 物化、Deny 保留和不可表示形状;Core 测试覆盖配置与 TurnContext 双字段发布:

text
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 如何共同决定是否进入审批流程。