权限模型 · 沙箱策略与审批策略

Codex 的安全边界由两个独立的量决定:PermissionProfile 规定命令在技术上能碰什么,AskForApproval 规定越界之前要不要问人。本篇讲清两者的取值、旧的 sandbox_mode 与新的权限配置档如何编译成同一个 PermissionProfile、“最具体者胜”的路径裁决、.git 等元数据保护,以及管理员下发的 deny_read。

作者 David更新于 第 27 篇(共 57 篇)

权限模型 · 沙箱策略与审批策略

第 4 部分讲安全。后面七篇分别拆审批流程、三个平台的沙箱、网络代理、执行策略和自动审查,它们消费的都是同两个值:一个 PermissionProfile(能做什么),一个 AskForApproval(什么时候问)。这一篇先把这两个数据结构讲透。

用户看到的样子

启动时用 -s/--sandbox 选沙箱模式(read-only、workspace-write、danger-full-access),用 -a/--ask-for-approval 选审批策略(交互模式只接受 on-request 与 never);--approve-for-me 只是三条配置覆盖的简写:approvals_reviewer="auto_review"、approval_policy="on-request"、sandbox_mode="workspace-write"(codex-rs/utils/cli/src/shared_options.rs:80)。config.toml 里有两套写法:旧的 sandbox_mode 加 [sandbox_workspace_write],新的 default_permissions 加 [permissions.<name>] 配置档,内置 :read-only、:workspace、:danger-full-access 三个。各种组合的效果与安全建议见手册的沙箱、审批与安全,本篇只看这些设置最后变成了什么。

两套表示,一个真身

旧表示是 SandboxPolicy(codex-rs/protocol/src/protocol.rs:1072),四个变体:DangerFullAccess、ReadOnly、ExternalSandbox、WorkspaceWrite,后者带 writable_roots、network_access、exclude_tmpdir_env_var、exclude_slash_tmp 四个旋钮。SandboxMode(命令行与 sandbox_mode 共用)只有三个取值,选不到 ExternalSandbox;后者表示“进程已经在别人搭好的沙箱里”,要经 app-server v2 协议这类途径传进来。运行时真正使用的是新表示 PermissionProfile:

pub enum PermissionProfile {
    /// Codex owns sandbox construction for this profile.
    #[serde(rename_all = "snake_case")]
    #[ts(rename_all = "snake_case")]
    Managed {
        file_system: ManagedFileSystemPermissions,
        network: NetworkSandboxPolicy,
    },
    /// Do not apply an outer sandbox.
    Disabled,
    /// Filesystem isolation is enforced by an external caller.
    #[serde(rename_all = "snake_case")]
    #[ts(rename_all = "snake_case")]
    External { network: NetworkSandboxPolicy },
}

(codex-rs/protocol/src/models.rs:422)

Managed 由 Codex 自己搭沙箱,文件系统部分要么是 Unrestricted,要么是 Restricted { entries, glob_scan_max_depth };网络 NetworkSandboxPolicy 只有 Restricted 与 Enabled 两档。Disabled 不套沙箱,External 把文件隔离交给外部、自己只管网络位。旧新之间的映射写在 SandboxEnforcement::from_legacy_sandbox_policy(codex-rs/protocol/src/models.rs:287):DangerFullAccess 对应 Disabled,ExternalSandbox 对应 External,其余对应 Managed。两边可以互相转换,旧表示留给还没迁移的代码路径;走旧语法时,推导出的配置档若无法表示成旧策略,配置加载会记一条日志、退回只读。

设置PermissionProfile文件系统条目网络
read-only、:read-onlyManaged:root 读Restricted
workspace-write、:workspaceManaged:root 读,工作区根写,/tmp 与 $TMPDIR 写,.git、.agents、.codex 读默认 Restricted
danger-full-access、:danger-full-accessDisabled不限Enabled

文件系统条目:最具体者胜

每个条目 FileSystemSandboxEntry 由路径、访问级别和“路径不存在时怎么办”组成。路径 FileSystemPath 有三种:具体路径、glob 模式(目前只用于拒绝)、特殊路径(:root、:minimal、:workspace_roots、:tmpdir、:slash_tmp)。访问级别 FileSystemAccessMode 是 read、write、deny 三档(none 是旧别名),枚举顺序就是冲突优先级。内置 :workspace 在 :root 只读之后追加这些条目:

        entries.push(FileSystemSandboxEntry::new(
            FileSystemPath::Special {
                value: FileSystemSpecialPath::project_roots(/*subpath*/ None),
            },
            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");

(codex-rs/protocol/src/permissions.rs:823)

判定一个路径能不能读写时,Unrestricted 与外部沙箱直接放行;否则找出所有覆盖它的条目,取路径最深的那条,深度相同再取优先级高的:

    pub fn resolve_access(
        &self,
        path: &PathUri,
        context: &FileSystemSandboxPolicyContext<'_>,
    ) -> FileSystemAccessMode {
        // ...
        entries
            .into_iter()
            .filter(|(root, _, _)| path.starts_with(root))
            .max_by_key(|(_, access, depth)| (*depth, *access))
            .map(|(_, access, _)| access)
            .unwrap_or(FileSystemAccessMode::Deny)
    }

(codex-rs/protocol/src/permissions.rs:998)

于是“/repo 可写、/repo/secrets 拒绝、/repo/secrets/tmp 又可写”这样的嵌套可以层层开合,同一路径上 deny 压过 write、write 压过 read。省略的中间部分还有几处兜底:路径约定(POSIX 与 Windows)对不上、路径无法按层级解析,一律返回 Deny,典型的失败即关闭(fail closed)。

元数据保护:.git、.agents、.codex 三个名字(PROTECTED_METADATA_PATH_NAMES,codex-rs/protocol/src/permissions.rs:45)在任何可写根下默认只读。除了上面追加的只读条目,写入判定还要过 metadata_write_denial:目标落在某个可写根下的这三个目录里,又没有更深的显式 write 条目,就拒绝;目录不存在也一样,首次创建同样得走审批。.git 是指向别处的 gitdir: 文件时(worktree、submodule),被指向的真实目录也一并保护。

配置档:从 TOML 编译成运行时

[permissions.<name>] 反序列化成 PermissionProfileToml,只有 description、extends、workspace_roots、filesystem、network 五个键(codex-rs/config/src/permissions_toml.rs:113)。编译规则集中在 codex-rs/core/src/config/permissions.rs:

  • 自定义配置档不能以 : 开头,这个前缀留给内置档。extends 只能继承 :read-only 与 :workspace,继承 :danger-full-access 会报 “cannot extend unsupported built-in profile”;继承链先父后子合并 TOML,子覆盖父,成环时报 “inheritance cycle detected”。
  • filesystem 的键是路径或特殊路径,值是访问级别,或者一张子表,给特殊路径下的相对子路径定规则,比如 ":workspace_roots" 下写 "**/*.env" = "deny"。glob 只能配 deny,read、write 只允许末尾的 /**(整棵子树);非 macOS 平台上,带 ** 的拒绝 glob 没设 glob_scan_max_depth 会得到启动警告,因为 Linux 要在起沙箱前把 glob 展开成具体路径(见 Linux 沙箱)。
  • 不认识的特殊路径只警告、不报错。parse_special_path 上方的注释写明了原因:0.112.0 曾因拒绝未知值,导致新版本写的配置在旧版本上加载失败。
  • network.enabled 决定 NetworkSandboxPolicy;表里其余的代理设置只是备料,network_proxy_config_from_profile_network 会把代理的 enabled 强制置为 false,配置档本身不会拉起代理(见网络代理)。

compile_permission_profile(codex-rs/core/src/config/permissions.rs:376)的产物是 CompiledPermissionProfile { permission_profile, workspace_roots }。:workspace_roots 这样的符号先保留在条目里,等当前工作区根(cwd 加 --add-dir 追加的目录)确定后再物化成具体路径,同一份配置档因此能跟着工作目录走。

两套语法选哪套,默认是什么

图表加载中…

resolve_permission_config_syntax(codex-rs/core/src/config/mod.rs:2565)的顺序是:命令行传了 --sandbox 就用旧语法;会话级覆盖里设了 default_permissions 就用配置档;否则从低到高扫一遍配置层,最后一个提到这两个键的层说了算。所以用户层写了 sandbox_mode、优先级更高的项目层写了 default_permissions,结果用配置档;同一层两个都写,也是配置档胜出。

两个都没写时走隐式内置档:default_builtin_permission_profile_name(codex-rs/core/src/config/permissions.rs:51)在项目有信任决定(trusted 或 untrusted)时选 :workspace,否则选 :read-only,Windows 上未启用沙箱时一律 :read-only。隐式 :workspace 仍会叠加旧的 [sandbox_workspace_write] 旋钮,显式写 default_permissions = ":workspace" 则忽略它们。另外,定义了 [permissions] 表、又没走旧语法,却没选 default_permissions,会直接报错,不会悄悄落到某个默认档上。

审批策略 AskForApproval

pub enum AskForApproval {
    /// Internal policy for projects marked untrusted. Commands require
    /// approval unless an explicit exec policy rule allows them.
    #[serde(rename = "untrusted")]
    #[strum(serialize = "untrusted")]
    UnlessTrusted,

    /// The model decides when to ask the user for approval.
    #[serde(alias = "on-failure")]
    #[default]
    OnRequest,
    // ...
    Granular(GranularApprovalConfig),
    // ...
    Never,
}

(codex-rs/protocol/src/protocol.rs:986)

  • on-request 是默认值,旧值 on-failure 作为别名保留。注释说得直白:由模型决定何时请求。模型调用 exec_command 时带上 sandbox_permissions = "require_escalated" 和一句 justification,才会弹出审批,细节见下一篇。
  • untrusted 已经降为内部策略:配置里显式写它会报 “approval_policy = "untrusted" is no longer supported”。没有显式设置时,受信任项目用 OnRequest,标为 untrusted 的项目用 UnlessTrusted(规则没放行的命令都要批准),其余也是 OnRequest。
  • Granular 有五个开关:sandbox_approval、rules、mcp_elicitations 必填,skill_approval、request_permissions 缺省为 false;为 false 的那类请求不弹窗,直接拒绝。never 则从不询问,失败直接交还模型。

审批交给谁由另一个值 ApprovalsReviewer 决定:user(默认)或 auto_review(旧名 guardian_subagent 仍可解析),后者见自动审查。

deny_read:管理员划的读禁区

托管配置 requirements.toml 可以用 [permissions.filesystem] 下的 deny_read 列一组路径或 glob。与多数 requirements 键不同,它在各层之间是累加的(codex-rs/config/src/requirements_layers/permissions.rs),最后由 FilesystemConstraints::apply_to_policy 转成 deny 条目追加进生效的配置档。配置加载还给受约束的配置档挂了一个校验器:之后用 /permissions 换配置档,新档必须保留全部托管拒绝项,也不能把拒绝范围内的具体路径设成可读。

读拒绝只存在于沙箱内部,由此有一个连锁效应:策略里只要有读拒绝,unsandboxed_execution_allowed 就返回 false,即使用户批准了提权,命令也仍在沙箱里跑,保住这些拒绝(见审批流程)。

模型怎么知道自己的边界

边界不仅要执行,还要告诉模型,否则它会反复撞墙。PermissionsInstructions::from_permission_profile(codex-rs/prompts/src/permissions_instructions.rs:160)从配置档反推一个模式名(全盘可写算 danger-full-access,没有可写根算 read-only,否则 workspace-write),连同可写根、网络开关、读拒绝清单、按审批策略挑选的说明模板(on-request 与 granular 时还附上规则里已放行的命令前缀),渲染成一段 <permissions instructions> 开发者消息注入上下文。读拒绝那一节明说:这些是策略限制,不要为读它们申请提权。

和《从 LLM 到 Coding Agent》对照

那本书的权限系统是三层:工具自分类、规则、一个从 plan 到 bypass 的权限模式总开关。Codex 把总开关拆成正交的两个量:沙箱决定“技术上能做什么”,由操作系统强制执行;审批决定“越界前问不问”。两者自由组合,read-only 加 never 适合 CI,workspace-write 加 on-request 是日常最常见的组合(项目有信任决定时,隐式默认就是 :workspace,否则是 :read-only)。Grok Build 的沙箱 profile 同样是“可写路径加是否限网”的组合,也能 extends 内置档;OpenCode 的权限规则按“最后一条匹配”求值,Codex 的文件系统条目则按“最深者胜、同深度 deny 胜”裁决,与书写顺序无关。


上一篇:Code mode · 让模型写 JavaScript 编排工具 · 下一篇:审批流程 · 什么时候停下来问你

本页目录