# 执行策略 · Starlark 规则与提权

> 规则文件是一段受限的 Starlark 程序，只能调用 prefix_rule、network_rule、host_executable 三个内置函数；解析器边执行边收集规则，按命令首词建索引。匹配是逐词的前缀比较，多条命中取最严格的决定，没命中的交给内置启发式。结果变成审批判定：forbidden 直接拒绝，prompt 弹审批，每一段都被规则明确 allow 才跳过沙箱。在实验性的 zsh-fork 模式下，shell-escalation 还能在每一次 exec 时拦截子命令，按绝对路径重新判定并在沙箱外代为执行。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/execpolicy
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 执行策略 · Starlark 规则与提权

[审批流程](https://daiw.net/manual/codex-source/approvals-flow)的第一关是“这条命令要不要问”。对命令来说，这一关的规则由 `codex-execpolicy` 这个小 crate 提供（去掉测试约 1800 行），内核里的 `ExecPolicyManager`（`codex-rs/core/src/exec_policy.rs`）负责加载、求值和写回。

## 用户看到的样子

在 `~/.codex/rules/` 或受信任项目的 `.codex/rules/` 下放扩展名为 `.rules` 的文件，写 `prefix_rule(pattern = [...], decision = "allow" | "prompt" | "forbidden")`；在审批框里选 `Yes, and don't ask again for commands that start with ...`，Codex 会自动往 `~/.codex/rules/default.rules` 追加一条 allow 规则。隐藏子命令 `codex execpolicy check` 可以离线检验规则。完整写法见手册的[命令规则](https://daiw.net/manual/codex/rules)，下面这段是仓库自带的示例，`match` 与 `not_match` 是随规则一起加载的内联测试：

```python
prefix_rule(
    pattern = ["git", "reset", "--hard"],
    decision = "forbidden",
    justification = "destructive operation",
    match = [
        ["git", "reset", "--hard"],
    ],
    not_match = [
        ["git", "reset", "--keep"],
        "git reset --merge",
    ],
)
```

（`codex-rs/execpolicy/examples/example.codexpolicy:4`）

## 规则文件怎么变成 Policy

`load_exec_policy`（`codex-rs/core/src/exec_policy.rs:662`）从低优先级到高优先级遍历配置层，读每层配置目录下 `rules/` 里的 `.rules` 文件（同一目录按文件名排序）；`codex exec --ignore-rules` 会跳过用户层与项目层。所有文件交给同一个 `PolicyParser`，最后再叠上托管 `requirements.toml` 的 `[rules]`（`merge_overlay`）。

解析器用 `starlark` crate 的扩展方言执行每个文件，只注入三个内置函数，都在 `policy_builtins` 里定义：

```rust
    fn prefix_rule<'v>(
        pattern: UnpackList<Value<'v>>,
        decision: Option<&'v str>,
        r#match: Option<UnpackList<Value<'v>>>,
        not_match: Option<UnpackList<Value<'v>>>,
        justification: Option<&'v str>,
        eval: &mut Evaluator<'v, '_, '_>,
    ) -> anyhow::Result<NoneType> {
        let decision = match decision {
            Some(raw) => Decision::parse(raw)?,
            None => Decision::Allow,
        };
```

（`codex-rs/execpolicy/src/parser.rs:349`）

- `prefix_rule`：`decision` 缺省为 `allow`；`justification` 不能是空串；`pattern` 的每个元素是字符串或字符串列表（多选一），第一个元素是列表时会展开成多条规则，因为规则按命令首词建索引（`MultiMap<String, RuleRef>`）。`match`、`not_match` 的示例在整个文件执行完后才校验，写错的规则在加载时就报错，并带上文件与行号。
- `network_rule(host, protocol, decision, justification)`：给[网络代理](https://daiw.net/manual/codex-source/network-proxy)用的主机规则，`protocol` 取 `http`、`https`、`socks5_tcp`、`socks5_udp`，`decision` 可写 `deny`（等同 `forbidden`）；主机名不许带通配符、协议头或路径。它主要由 Codex 自己写入：在网络审批里选“以后都允许”或“以后都拦截这个主机”时，追加进 `default.rules` 的就是这种规则。
- `host_executable(name, paths)`：限定哪些绝对路径可以回退到按文件名匹配，路径必须是绝对路径且文件名与 `name` 一致。

文件执行时没有副作用，不读写文件系统，唯一的产出就是这张规则表。解析失败时，交互会话发一条警告，只保留托管规则继续运行（`load_exec_policy_with_warning`）。

## 前缀匹配，取最严格

```rust
    pub fn matches_prefix(&self, cmd: &[String]) -> Option<Vec<String>> {
        let pattern_length = self.rest.len() + 1;
        if cmd.len() < pattern_length || cmd[0] != self.first.as_ref() {
            return None;
        }

        for (pattern_token, cmd_token) in self.rest.iter().zip(&cmd[1..pattern_length]) {
            if !pattern_token.matches(cmd_token) {
                return None;
            }
        }

        Some(cmd[..pattern_length].to_vec())
    }
```

（`codex-rs/execpolicy/src/rule.rs:46`）

逐词精确比较，命令可以比模式长，没有正则也没有通配。首词是绝对路径（如 `/usr/bin/git`）且没有精确规则时，按文件名 `git` 再找一遍，前提是 `host_executable` 没有限制这个路径。多条规则同时命中时取最严格的：`Decision` 枚举按 `Allow`、`Prompt`、`Forbidden` 的顺序派生了 `Ord`（`codex-rs/execpolicy/src/decision.rs:9`），`Evaluation::from_matches` 直接对命中的决定取 `max()`。与 OpenCode 那种“最后一条匹配说了算”不同，这里书写顺序无关紧要，严格的规则永远压过宽松的。

## 判定怎么变成审批

判定前先拆段（`commands_for_exec_policy_for_platform`）：`bash -lc "..."` 这类调用里的脚本，若只由普通单词和 `&&`、`||`、`;`、`|` 组成，就用 tree-sitter 拆成多条命令分别判定，Windows 上对 PowerShell 的 `-Command` 也有类似处理（拆分细节见[读懂一条命令](https://daiw.net/manual/codex-source/shell-command-analysis)）。每一段先查规则，没命中的交给启发式兜底（以 `on-request` 为例：危险命令要问，申请越出沙箱要问，其余交给沙箱），整体仍取最严格的结果：

- `Forbidden` 直接拒绝，理由里带上规则的 `justification`；
- `Prompt` 变成需要审批，但审批策略是 `never` 或 `granular` 关了对应开关时，改为拒绝；
- `Allow` 变成不问，而且只有**每一段**都被规则明确放行时才跳过沙箱：

```rust
            Decision::Allow => ExecApprovalRequirement::Skip {
                // Bypass sandbox only when every parsed command segment is
                // explicitly allowed by execpolicy.
                bypass_sandbox: commands.iter().all(|command| {
                    exec_policy
                        .matches_for_command_with_options(
                            command,
                            /*heuristics_fallback*/ None,
                            &match_options,
                        )
                        .iter()
                        .any(|rule_match| {
                            is_policy_match(rule_match) && rule_match.decision() == Decision::Allow
                        })
                }),
```

（`codex-rs/core/src/exec_policy.rs:440`）

需要审批时，Codex 顺带算一条“建议规则”放进审批请求：优先用模型给的 `prefix_rule`，但它不能出现在 `BANNED_PREFIX_SUGGESTIONS` 里（`python`、`bash -lc`、`node -e`、`git`、`rm`、`sudo` 等能执行任意代码或过于宽泛的前缀），而且加上它之后必须能放行这条命令的每一段；否则退而取启发式判为“要问”的那一段命令。用户接受后，`blocking_append_allow_prefix_rule` 在文件锁保护下往 `default.rules` 追加一行 `prefix_rule(pattern=[...], decision="allow")`，内存里的策略经 `ArcSwap` 原子替换，下一条命令立刻生效。还有一个少见的例外：模型的 `model_specialty` 是 cyber，或托管要求的 `auto_review.ignore_rules` 列出了当前模型时，所有 allow 规则被忽略（`AllowPrefixRules::IgnoreForCyberModel`），prompt 与 forbidden 照常生效。

## 提权通道：拦截每一次 exec

前缀规则只看得到模型交来的那条命令。脚本里再调用的子命令，要到 exec 那一刻才现形。`codex-shell-escalation` 为此提供了一条拦截通道，只在开发中的 `shell_zsh_fork` 模式下启用（默认关闭，要求 Unix、用户 shell 为 zsh，并使用打过补丁的 zsh）：

```mermaid
sequenceDiagram
  participant Z as 打过补丁的 zsh
  participant W as codex-execve-wrapper
  participant S as EscalateServer
  Z->>W: 每次 exec 前先调用包装器
  W->>S: EscalateRequest 文件 argv 工作目录 环境
  S->>S: 按绝对路径匹配规则 必要时请求审批
  alt Run
    S-->>W: Run
    W->>W: 在沙箱内 execv 原命令
  else Escalate
    S-->>W: Escalate
    W->>S: 交出 stdin stdout stderr
    S->>S: 在沙箱外或按指定权限运行
    S-->>W: 退出码
  else Deny
    S-->>W: Deny
    W->>W: 打印原因 以 1 退出
  end
```

zsh 的补丁给 `exec` 加了 `EXEC_WRAPPER` 支持：设置了这个环境变量，每次 exec 都先运行 `codex-execve-wrapper`（又是 `codex` 本身的一个 arg0 别名）。包装器从环境变量 `CODEX_ESCALATE_SOCKET` 拿到继承下来的套接字，先发一个握手数据报，附带一对新建套接字的一端，此后的请求与响应都走这对专用套接字，于是服务端能同时处理多个并发的 exec。服务端 `CoreShellActionProvider::determine_action` 用程序的绝对路径查规则、没命中走启发式，需要时发起审批（请求里带 `approval_id`，客户端据此区分同一条命令下的多个子命令审批），再给出 `Run`、`Escalate` 或 `Deny`（`codex-rs/shell-escalation/src/unix/escalate_protocol.rs:37`）。`Run` 表示就在当前沙箱里照常 exec；`Escalate` 时包装器把自己的标准输入输出复制一份，经 `SCM_RIGHTS` 交给服务端，由 Codex 按 `EscalationExecution` 的三种方式之一重新运行这个程序：完全不套沙箱（`Unsandboxed`）、用本轮的沙箱配置（`TurnDefault`）、或用请求附带的权限（`Permissions`），跑完把退出码传回，包装器以同样的退出码结束。于是“规则明确放行的子命令在沙箱外跑、其余留在沙箱里”可以细化到一条脚本内部的每一次 exec。

## `codex execpolicy check`

这个子命令在 `codex --help` 里是隐藏的，参数定义在 `ExecPolicyCheckCommand`（`codex-rs/execpolicy/src/execpolicycheck.rs:17`）：`-r`/`--rules` 可重复，`--pretty` 美化输出，`--resolve-host-executables` 打开绝对路径到文件名的回退。它只输出命中的规则与最严格的决定，既不拆分 `bash -lc` 脚本，也不跑启发式，所以结果是“规则怎么看这条参数列表”，不完全等于运行时的判定。

## 和《从 LLM 到 Coding Agent》对照

那本书的[权限系统](https://daiw.net/manual/llm-to-agent/permissions)第二层是“工具名加参数模式”的规则，比如 `Bash(git status)` 永远放行。Codex 把这一层做成了真正的程序：规则是 Starlark，自带内联测试，按参数列表逐词匹配，取最严格而不是最后一条；启发式兜底和“每段都放行才跳过沙箱”保证复合命令里夹带的危险片段不会被一条宽松规则顺带放过。OpenCode 的[权限系统](https://daiw.net/manual/opencode/permission)对 bash 同样按命令前缀记“总是允许”，但只存在内存里，重启即失效；Codex 的规则落盘在 `default.rules`，跨会话生效。

---

上一篇：[网络代理 · 联网权限怎么落地](https://daiw.net/manual/codex-source/network-proxy) · 下一篇：[自动审查 · 让另一个模型替你审批](https://daiw.net/manual/codex-source/guardian)
