读懂一条命令 · 解析、安全判断与 shell 快照

模型交来的只是一段 shell 字符串,Codex 要从三个角度读它:给人看的语义摘要(读文件、搜索、列目录)、给审批用的安全判断、给执行提速的 shell 快照。解析靠 tree-sitter 分严格与宽松两档,前者用来逐条匹配规则,后者只用来找危险命令;0.158.0 的内置危险判断只盯带强制选项的 rm 与几类 Windows 命令,其余交给审批策略和沙箱。

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

读懂一条命令 · 解析、安全判断与 shell 快照

上一篇里,exec_command 把 cmd 原样交给 shell,比如 bash -lc。但在交出去之前和之后,Codex 还要把这段字符串拆开读好几遍。这些逻辑大多在 codex-shell-command 这个 crate 里,它依赖 tree-sitter、tree-sitter-bash、tree-sitter-powershell 与 shlex,只管“读懂”,执行命令不归它管。

用户看到的样子

在终端界面里,只由读文件、列目录、搜索组成的命令会被收进一个 “Exploring / Explored” 分组,每行一个动作:Read foo.rs、List src、Search TODO in src;其它命令照常显示命令本身和输出。审批方面,没有规则放行时,rm -rf 这类命令即使在沙箱里也会停下来问你,审批策略设为 never 时则直接拒绝(见沙箱、审批与安全与规则)。速度方面,[features] 里的 shell_snapshot 默认开启,它让每条命令不必重新执行一遍 ~/.bashrc 或 ~/.zshrc(见配置参考)。

图表加载中…

两档解析:严格与宽松

bash.rs 用 tree-sitter-bash 解析 -c / -lc 后面的脚本,分两档。严格档 try_parse_word_only_commands_sequence 只接受由 &&、||、;、| 连起来的“纯单词命令”:语法树里一旦出现括号、重定向、命令替换、控制流,或者变量展开这类非字面量的词,就整体放弃、返回 None。宽松档则反过来,接受任何合法语法,把每个命令节点里能静态确定的词都抠出来,注释把它的用途限定得很清楚:

/// Extracts the literal portions of command invocations from a shell script.
///
/// Unlike [`parse_shell_lc_plain_commands`], this accepts complex shell syntax
/// and returns the statically known words from every command node in a valid
/// syntax tree. Dynamic words and redirections are omitted. This is suitable
/// for identifying dangerous literal commands, but must not be used to prove
/// that a command is safe.
pub(crate) fn parse_shell_lc_literal_commands(command: &[String]) -> Option<Vec<Vec<String>>> {

(codex-rs/shell-command/src/bash.rs:129)

这是一个不对称的设计:想证明“安全”,只能用看得懂每个字的严格档;想发现“危险”,宽松档看漏几个动态词也不至于放大风险,因为它的结论只会让命令多一次审批,从不让命令少一次审批。

语义摘要:读、搜、列

parse_command.rs 把命令归成 ParsedCommand 的四种之一:Read(带文件路径)、ListFiles、Search(带查询词与路径)、Unknown。summarize_main_tokens 认得 cat、bat、head、tail、less、more、nl、awk、sed -n 这类读文件的命令,ls、eza、tree、du、git ls-files 这类列目录的,rg、grep、ag、git grep 这类搜索的(fd、find 与 rg --files 视有无查询词归入搜索或列目录),PowerShell 的 Get-Content 也算读。脚本先走严格解析拆成多条,解析不了(比如用了重定向)就整段记为 Unknown;管道里的 wc、sort、head -n 40 这类“小格式化命令”被丢掉,好让摘要落在主命令上;cd 不单独成条,但会被记下来,拼出后续读文件的完整路径。只要有一段被判成 Unknown,整条命令就退化成一个 Unknown,宁可不摘要,也不给出半对半错的摘要。

这段代码的文档注释很少见:开头第一行是 “DO NOT REVIEW THIS CODE BY HAND”,接着说最省事的迭代方式是补单元测试、再让 Codex 去修实现(codex-rs/shell-command/src/parse_command.rs:44)。摘要的去处有三个:ToolEmitter 把它放进命令开始事件,终端据此画出 Exploring 分组;app-server 协议把它转成 CommandAction 交给其它客户端;注册表分派时用它给遥测打 command_category 标签。

危险命令:只盯少数几种

is_dangerous_command.rs 的判断出奇地克制。POSIX 下只有一条规则,而且会穿透几层“包装”:

    match cmd0.as_deref() {
        Some("rm") if rm_args_include_force_option(&command[1..]) => {
            Some(DangerousCommandMatch::ForcedRm)
        }

        // For sudo <cmd>, simply check <cmd>.
        Some("sudo") => {
            dangerous_command_match_with_depth(&command[1..], wrapper_depth + 1, platform)
        }

        // Skip environment assignments before checking the command run by env.
        Some("env") => dangerous_command_match_for_env(command, wrapper_depth, platform),

        // A trap action is shell source stored in the first operand.
        Some("trap") => dangerous_command_match_for_trap(command, wrapper_depth, platform),

        // ...
        _ => None,
    }

(codex-rs/shell-command/src/command_safety/is_dangerous_command.rs:132)

rm 只要带 -f、--force 或 -rf 这样含 f 的组合短选项就算数;bash -lc 里的脚本用宽松档展开,所以藏在管道、if、for、$(...) 里的 rm -rf 也跑不掉。包装嵌套超过 8 层(MAX_DANGEROUS_COMMAND_WRAPPER_DEPTH)直接当作危险,失败时关门而不是放行。Windows 另有一套规则:Remove-Item -Force、del /f、rd /s /q 这类强制删除,以及用 Start-Process、ShellExecute、浏览器程序打开 URL 的命令。

判断结果只在执行策略里用。codex-rs/core/src/exec_policy.rs 先把 bash -lc 用严格档拆成多条命令,逐条匹配 Starlark 规则(规则本身见执行策略);拆不开就按整条 argv 匹配。没有规则命中的命令,才轮到这段兜底逻辑:

    // If the command is flagged as dangerous or we have no sandbox protection,
    // we should never allow it to run without approval.
    //
    // We prefer to prompt the user rather than outright forbid the command,
    // but if the user has explicitly disabled prompts, we must
    // forbid the command.
    if dangerous_command_match.is_some() || windows_managed_fs_restrictions_without_sandbox_backend
    {
        return match approval_policy {
            AskForApproval::Never => Decision::Forbidden,
            AskForApproval::OnRequest
            | AskForApproval::UnlessTrusted
            | AskForApproval::Granular(_) => Decision::Prompt,
        };
    }

(codex-rs/core/src/exec_policy.rs:793)

被拒绝时模型看到的理由是 rm -f style commands are not permitted. Use a safer approach。不危险的未命中命令则看审批策略与沙箱:never 直接放行、靠沙箱兜底;untrusted 一律询问;on-request 同样放行,只有在受限沙箱里主动申请越过沙箱时才询问。换句话说,0.158.0 的内置逻辑里没有“哪些命令天生安全”的白名单,安全感主要来自沙箱,想免审批的命令要靠规则明确放行。

用哪个 shell

shell_detect.rs 决定默认 shell:Windows 上是 PowerShell,找不到就退回 cmd.exe;Unix 上读 passwd 里登记的登录 shell(用线程安全的 getpwuid_r),不认识或找不到时,macOS 依次试 zsh、bash,其它系统依次试 bash、zsh,最后兜底 /bin/sh。模型通过 shell 参数指定的路径只用来判断类型,可执行文件仍由 Codex 自己去找,函数名 get_shell_by_model_provided_path 的注释写得明白。

shell 快照:把 rc 文件的效果缓存下来

命令默认以登录 shell 启动,每次都要执行一遍用户的 rc 文件,慢且可能有副作用。shell 快照(codex-rs/core/src/shell_snapshot.rs)的做法是:环境建好时在后台启动一次捕获,用登录 shell 执行一段脚本,先加载 ~/.bashrc 或 ~/.zshrc,再把 shopt -p、declare -f(函数)、set -o 选项、别名与导出的变量写成一个可 source 的文件,放在 CODEX_HOME/shell_snapshots/ 下,文件名是线程 ID 加一个时间戳。捕获限时 10 秒,写完还要用 set -e 再 source 一遍验证,通过了才改名生效;PowerShell 与 cmd 不支持。快照是异步的,命令执行时只“看一眼”它好了没有,没好就照常跑,不会为它等待。

有了快照,maybe_wrap_shell_lc_with_snapshot 把 bash -lc '<命令>' 改写成一个非登录的 bash -c 脚本:

    let rewritten_script = if override_exports.is_empty() {
        format!("if . '{snapshot_path}' >/dev/null 2>&1; then :; fi\n\n{run_original}")
    } else {
        format!(
            "{override_captures}\n\nif . '{snapshot_path}' >/dev/null 2>&1; then :; fi\n\n{override_exports}\n\n{run_original}"
        )
    };

(codex-rs/core/src/tools/runtimes/mod.rs:506)

先静默 source 快照,失败也不影响后续;再把 CODEX_THREAD_ID 等运行时变量与用户在 shell_environment_policy.set 里明确要求的值重新导出,免得被快照里的旧值盖掉;最后 exec 原来的 shell 以 -c 跑命令。Windows 上不做这层包装。快照文件在对应对象释放时删除,没删干净的,下次创建快照时会顺手清理:找不到所属线程、或者线程的会话文件超过 3 天没动过,都会被删。启用了网络代理的凭据代管时,快照改为在命令自己的沙箱里按需捕获(codex-rs/core/src/shell_snapshot_sandbox.rs);还在开发中的 shell_snapshot_v2 则把捕获挪到 exec-server 一侧。

和《从 LLM 到 Coding Agent》对照

权限系统讲的第一层是“工具自分类”,靠工具自报读写属性决定要不要问人。Codex 对 shell 命令没有走“自分类”,因为同一个工具里可以塞进任何命令:它把分类拆成两件事,语义摘要只服务展示,安全判断则落在执行策略与沙箱上,而且只在“证明安全”时要求完全看懂命令,“发现危险”时宁可多报。那一篇强调的 fail-closed 在这里也看得到:看不懂的命令,摘要退化成 Unknown、规则按整条命令匹配,包装嵌套太深就直接当作危险。


上一篇:执行命令 · shell 与 unified exec · 下一篇:apply_patch · Codex 的文件编辑格式

本页目录