首页
/ Codex execpolicy-legacy:基于 Starlark 策略语言的命令安全分类引擎

Codex execpolicy-legacy:基于 Starlark 策略语言的命令安全分类引擎

2026-09-06 22:30:11作者:卓艾滢Kingsley

本篇技术指南围绕仓库中的 codex-execpolicy-legacy crate 展开,系统讲解其"对一条 execv(3) 命令做安全分类"的设计:从 safe / match / forbidden / unverified 四态模型、CLI 检查命令与退出码,到 Starlark 编写的策略文件格式、默认策略规则全集,再到源码层的校验流程与"策略自带单元测试"机制。读完本文,你可以独立读懂并编写 .policy 规则,并理解 Codex 在执行代理命令前如何用这套引擎做白名单校验。

一、定位:把命令分类,而不是返回布尔值

该 crate 位于 codex-rs/execpolicy-legacy/,README 开篇即说明:新版的前缀规则引擎已迁移到 codex-execpolicy crate,这里保留的是最初的 execpolicy 实现(见 README.md)。

它的目标是:将一条拟执行的 execv(3) 命令分类为四种状态之一:

  • safe:命令可以安全执行(带限定条件,见下文);
  • match:命令匹配了策略中的规则,但调用方需要结合它将要写入的文件,自行判断是否安全执行;
  • forbidden:命令被明确禁止执行;
  • unverified:无法判定安全性,交由用户决策。

之所以不直接返回布尔值,原因在于:判断一条命令是否"安全"往往需要 execv() 参数之外的上下文。例如,如果你信任一个自主软件代理在你的源码树里写文件,那么 /bin/cp foo bar 是否"安全"取决于调用进程的 getcwd(3),以及 foobar 相对于 getcwd() 解析后的 realpath。因此校验器返回的是结构化结果,由客户端据此判定这条 execv() 调用是否安全。

另一个关键语义在源码中同样明确:ValidExec 的注释写道,"safety" 并不保证命令会执行成功——cat /Users/mbolin/code/codex/README.md 在系统允许代理读取该目录下任何文件时可被判为 "safe",但若 README.md 不存在,运行时仍会失败(只是代理没有读取任何未授权的文件)。

二、快速上手:CLI 检查命令

crate 同时是一个可执行目标(src/main.rs),用 clap 定义了如下参数:

  • check 子命令:把剩余参数视为 execv(3) 的输入,形如 program + args...
  • check-json 子命令:接收一个 JSON 对象字符串,要求含 "program"(str)与 "args"(list[str])字段;
  • --require-safe:若命令未通过策略检查,则以非零码退出,但仍向 stdout 打印可解析的 JSON;
  • --policy / -p:指定自定义 .policy 文件路径,用于临时测试;未指定时加载内置默认策略。

内置默认策略通过 include_str! 在编译期嵌入(src/lib.rs):

const DEFAULT_POLICY: &str = include_str!("default.policy");

pub fn get_default_policy() -> starlark::Result<Policy> {
    let parser = PolicyParser::new("#default", DEFAULT_POLICY);
    parser.parse()
}

示例 1:safe 结果

检查 ls -l foo

cargo run -p codex-execpolicy-legacy -- check ls -l foo | jq

退出码为 0,stdout 输出:

{
  "result": "safe",
  "match": {
    "program": "ls",
    "flags": [
      {
        "name": "-l"
      }
    ],
    "opts": [],
    "args": [
      {
        "index": 1,
        "type": "ReadableFile",
        "value": "foo"
      }
    ],
    "system_path": ["/bin/ls", "/usr/bin/ls"]
  }
}

两点值得注意:

  • foo 被标记为 ReadableFile,调用方应把它相对 getcwd() 解析并做 realpath(它可能是符号链接),再判断读取它是否安全;
  • 虽然指定的可执行文件是 ls"system_path" 给出 /bin/ls/usr/bin/ls 作为更可信的替代——避免使用 $PATH 中碰巧排在第一位的 ls。只要主机上存在其中之一,就推荐用绝对路径作为 execv(3) 的第一个参数。

示例 2:match 结果

回到 cp 的例子,因为规则中匹配到 ARG_WFILE(可写文件),返回的是 match 而非 safe

cargo run -p codex-execpolicy-legacy -- check cp src1 src2 dest | jq
{
  "result": "match",
  "match": {
    "program": "cp",
    "flags": [],
    "opts": [],
    "args": [
      {
        "index": 0,
        "type": "ReadableFile",
        "value": "src1"
      },
      {
        "index": 1,
        "type": "ReadableFile",
        "value": "src2"
      },
      {
        "index": 2,
        "type": "WriteableFile",
        "value": "dest"
      }
    ],
    "system_path": ["/bin/cp", "/usr/bin/cp"]
  }
}

调用方若想放行该命令,应解析 JSON 挑出 WriteableFile 类型的参数,判断这些路径是否允许写入。

示例 3:forbidden 结果

策略也可以声明"命中即禁止"的规则。例如不希望代理任何情况下运行 applied deploy,于是定义:

define_program(
    program="applied",
    args=["deploy"],
    forbidden="Infrastructure Risk: command contains 'applied deploy'",
    should_match=[
        ["deploy"],
    ],
    should_not_match=[
        ["lint"],
    ],
)

规则要成为 forbidden,必须用关键字参数 forbidden 给出原因,该原因会被原样包含在输出中:

cargo run -p codex-execpolicy-legacy -- check applied deploy | jq
{
  "result": "forbidden",
  "reason": "Infrastructure Risk: command contains 'applied deploy'",
  "cause": {
    "Exec": {
      "exec": {
        "program": "applied",
        "flags": [],
        "opts": [],
        "args": [
          {
            "index": 0,
            "type": {
              "Literal": "deploy"
            },
            "value": "deploy"
          }
        ],
        "system_path": []
      }
    }
  }
}

退出码对照

main.rs 中定义了三个专用退出码,仅在指定 --require-safe 时生效,未指定时非 safe 结果仍以 0 退出:

分类 --require-safe --require-safe
safe 0 0
match(含可写文件) 0 12
forbidden 0 14
unverified(校验失败) 0 13

三、Starlark 策略语言:.policy 文件格式

默认策略定义在 crate 内的 src/default.policy。之所以选用 Starlark 而非 JSON/YAML,是因为它在不牺牲安全性与可复现性的前提下支持"宏"(列表拼接、变量复用等)。底层实现采用 starlark-rust

policy_parser.rs 看,解析器向策略文件注入了以下内建函数与全局常量:

内建函数policy_builtins 模块):

  • define_program(...):定义一条程序规则,支持的参数包括 programsystem_pathoption_bundlingcombined_formatoptionsargsforbiddenshould_matchshould_not_match
  • flag(name):声明一个不带值的选项(如 -r);
  • opt(name, type, required=...):声明一个带值的选项(如 head -n 5 中的 -n),required=True 时该选项必须出现;
  • forbid_substrings(strings):把任意参数中出现指定子串的命令判为 forbidden;
  • forbid_program_regex(regex, reason):程序名命中正则即 forbidden。

全局参数匹配器常量policy_parser.rs#L46-L59):

常量 含义
ARG_OPAQUE_VALUE 不透明值:无法确定其含义,但确定不是文件路径
ARG_RFILE 一个可被读取的文件(或目录)
ARG_WFILE 恰好一个将被写入的文件
ARG_RFILES 一个或多个可读文件
ARG_RFILES_OR_CWD 可读文件,或允许省略(回退到当前目录)
ARG_POS_INT 正整数(如 head -n 的取值)
ARG_SED_COMMAND 经专门解析器(sed_command.rs)校验过的"已知安全" sed 命令
ARG_UNVERIFIED_VARARGS 类型未知的可变参数(见下文 might_write_file 的保守处理)

规则语义示例

README 中的 cp 规则:

define_program(
    program="cp",
    options=[
        flag("-r"),
        flag("-R"),
        flag("--recursive"),
    ],
    args=[ARG_RFILES, ARG_WFILE],
    system_path=["/bin/cp", "/usr/bin/cp"],
    should_match=[
        ["foo", "bar"],
    ],
    should_not_match=[
        ["foo"],
    ],
)

其含义是:

  • cp 允许使用的 flag 仅限 -r-R--recursive(flag 指不带参数的选项);
  • args 中的 ARG_RFILES 表示期望一个或多个"可读文件"参数;
  • 末尾的 ARG_WFILE 表示恰好一个"可写文件"参数;
  • should_match / should_not_match 是把单元测试直接内嵌进规则定义的轻量手段:前者是应当匹配本规则的 execv(3) 参数示例,后者是不应匹配的示例。这些示例在 .policy 文件加载时就会被验证。

README 同时提醒:.policy 语言仍在演进,需要不断扩展表达力,既能覆盖所有希望视为"安全"的命令,又不让不安全命令混过。

四、默认策略规则全景

default.policy 覆盖了九个程序(printenvsed 各有两条变体规则),文件头部的 docstring 就是 define_program() 参数的权威说明。逐条梳理:

  1. ls:flag 限 -1-a-l;参数为 ARG_RFILES_OR_CWD(允许 ls 无参列当前目录);system_path/bin/ls/usr/bin/ls
  2. cat:flag 限 -b-n-t;参数 ARG_RFILESshould_not_match 显式排除了空参数(cat 无参会读 stdin,不适合当前使用场景)和 -l(不自动批准建议性锁)。
  3. cp:如上所述,ARG_RFILES + ARG_WFILE,因此它天然返回 match 而非 safe
  4. headopt("-c", ARG_POS_INT)opt("-n", ARG_POS_INT),参数 ARG_RFILES
  5. printenv(两条规则,利用同一程序名可注册多个 spec 的机制区分场景):
    • 无参数版本:打印全部环境变量,should_match=[[]]should_not_match=[["PATH"]]
    • 单参数版本:打印指定变量,参数为 ARG_OPAQUE_VALUE(变量名不是文件路径),should_match=[["PATH"]]should_not_match=[[], ["PATH", "HOME"]]
  6. pwd:flag 限 -L-P,不接受位置参数(注意它通常是 shell 内建命令)。
  7. rg:选项面很宽——-A/-B/-C/-d/--max-depth/-m/--max-countARG_POS_INT-g/--globARG_OPAQUE_VALUE;flag 含 -n-i-l--files--files-with-matches--files-without-match;位置参数为 ARG_OPAQUE_VALUE(搜索模式)加 ARG_RFILES_OR_CWD。其 system_path 特意留空,源码中的 TODO 备注了原因(可能期望使用宿主环境自带的 rg)。
  8. sed(两条规则):由于 GNU sed 的 e flag 可执行替换串为 shell 命令(policy 文件中给出了 sed 's/y/echo hi/e' 的 PoC 注释),sed 很难被"常规"方式安全化,于是实现了专属的 ARG_SED_COMMAND,只接受"已知安全"的 sed 命令。flag 只保留 -n-u刻意不支持 -i(就地修改)与 -f(脚本文件)
    • 不带 -e:第一个位置参数必须是合法 sed 命令,其后是可读文件;
    • -eopt("-e", ARG_SED_COMMAND, required=True),其余位置参数一律视为可读文件。
  9. which:flag 限 -a-s;参数为 ARG_RFILESwhich 的参数是程序名,这里按文件路径语义处理);should_not_match 排除空参数。

Starlark 的"宏"能力在默认策略里直接体现:common_sed_flags 列表在两条 sed 规则间复用,printenv_system_path 变量在两条 printenv 规则间复用。

五、源码层的校验流程

5.1 Policy::check:三道闸门

policy.rs 中的 Policy::check 按顺序执行:

  1. 禁止程序名正则:遍历 forbidden_program_regexes,程序名命中即返回 Forbidden { cause: Forbidden::Program, .. }
  2. 禁止子串:任一参数命中 forbidden_substrings_pattern 即返回 Forbidden { cause: Forbidden::Arg, .. },reason 固定为 arg \{arg}` contains forbidden substring`;
  3. 按名查找程序规则programsMultiMap<String, ProgramSpec>,同一程序可挂多条规则;逐条调用 ProgramSpec::check,第一条成功即返回;全部失败则返回最后一条错误(默认是 NoSpecForProgram,即 unverified)。

因此 Forbidden 枚举的三个变体 Program / Arg / Execprogram.rs#L75-L88)分别对应这三种禁止来源;applied deploy 的例子命中的是第三种——规则本身匹配成功,但 spec 带 forbidden 字段。

5.2 ProgramSpec::check:选项与位置参数的匹配

program.rs#L94-L195 的匹配逻辑:

  • 逐参数扫描:以 - 开头的参数必须命中 allowed_options,否则报 UnknownOptionFlag 型选项立即记入 matched_flagsValue 型选项则把下一个参数消费为其值(若下一个参数又以 - 开头,报 OptionFollowedByOptionInsteadOfValue);裸 -- 分隔符明确不支持(DoubleDashNotSupportedYet);
  • 扫描结束后若某个选项还"欠"一个值,报 OptionMissingValue
  • 位置参数交给 resolve_observed_args_with_patternsargs 中的匹配器序列对齐,数量或类型不符时报 NotEnoughArgsUnexpectedArguments 等错误(完整错误清单见 error.rs);
  • required=True 的选项若缺席,报 MissingRequiredOptions
  • 全部通过后组装 ValidExec { program, flags, opts, args, system_path },若 spec 带 forbidden 字段则升级为 MatchedExec::Forbidden,否则为 MatchedExec::Match

5.3 safematch 的分界:might_write_files

CLI 层(main.rs#L97-L125)在拿到 Match 后调用 ValidExec::might_write_files() 决定输出 safe 还是 match。该方法(valid_exec.rs#L33-L36)检查所有选项值与位置参数的 ArgType

  • WriteableFileUnknown 类型返回 true(保守:未知类型按可能写文件处理);
  • LiteralOpaqueNonFilePositiveIntegerReadableFileSedCommand 返回 false

这也解释了示例 1 与示例 2 的差异:ls -l foo 的参数全是 ReadableFile,输出 safecp ... dest 含一个 WriteableFile,输出 match

六、策略完整性的验证

README 声明 "default.policy 的完整性通过单元测试验证"。机制分两层:

  1. 加载时自检:每条规则的 should_match / should_not_match 示例在策略解析后被逐条执行——Policy::check_each_good_list_individually 要求正例必须匹配成功,check_each_bad_list_individually 要求负例必须匹配失败(policy.rs#L88-L102),任何违规都收集为 PositiveExampleFailedCheck / NegativeExamplePassedCheck 报告;
  2. 按程序的测试套件tests/all.rs 聚合了 tests/suite/ 下的模块——good.rsbad.rs(通用正反例),以及针对具体程序的 cp.rshead.rsls.rspwd.rsliteral.rssed.rs,外加 parse_sed_command.rs 验证 sed 命令解析器。

这种"规则即测试"的设计意味着:修改 default.policy 添加一条规则时,只要同时写对 should_match/should_not_match,规则的正确性就与策略文件同生命周期维护,无需另写测试。

七、小结与延伸阅读

execpolicy-legacy 的核心价值可以浓缩为三点:

  • 结构化判定:不回答"能不能跑",而是回答"命令匹配了哪条规则、每个参数是什么类型、用哪个绝对路径执行更可信",把写文件等高风险决策留给了解上下文的调用方;
  • 策略即代码:用 Starlark 编写规则,既有宏的复用能力,又保持求值安全与可复现,且规则自带正反例验证;
  • 防御纵深:程序名正则、参数子串黑名单、逐选项白名单、专属 SedCommand 解析器(封杀 GNU sed e flag 的提权路径)层层设卡。

延伸阅读路径:

需要说明的适用前提:option_bundling(如 -al 合并写法)与 combined_format--option=value 写法)在 default.policy 头部被标注为 PLANNED,属于规划中的参数;当前策略语言仍处于演进状态,新增规则时应以现有内建函数签名(见第五、四节源码引用)为准。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.13 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
897
5.8 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
529
593
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
915
1.83 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.58 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.35 K
1.46 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.01 K
515
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
547
388