Codex execpolicy-legacy:基于 Starlark 策略语言的命令安全分类引擎
本篇技术指南围绕仓库中的 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),以及 foo、bar 相对于 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(...):定义一条程序规则,支持的参数包括program、system_path、option_bundling、combined_format、options、args、forbidden、should_match、should_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 覆盖了九个程序(printenv 与 sed 各有两条变体规则),文件头部的 docstring 就是 define_program() 参数的权威说明。逐条梳理:
ls:flag 限-1、-a、-l;参数为ARG_RFILES_OR_CWD(允许ls无参列当前目录);system_path为/bin/ls、/usr/bin/ls。cat:flag 限-b、-n、-t;参数ARG_RFILES。should_not_match显式排除了空参数(cat无参会读 stdin,不适合当前使用场景)和-l(不自动批准建议性锁)。cp:如上所述,ARG_RFILES+ARG_WFILE,因此它天然返回match而非safe。head:opt("-c", ARG_POS_INT)与opt("-n", ARG_POS_INT),参数ARG_RFILES。printenv(两条规则,利用同一程序名可注册多个 spec 的机制区分场景):- 无参数版本:打印全部环境变量,
should_match=[[]]、should_not_match=[["PATH"]]; - 单参数版本:打印指定变量,参数为
ARG_OPAQUE_VALUE(变量名不是文件路径),should_match=[["PATH"]]、should_not_match=[[], ["PATH", "HOME"]]。
- 无参数版本:打印全部环境变量,
pwd:flag 限-L、-P,不接受位置参数(注意它通常是 shell 内建命令)。rg:选项面很宽——-A/-B/-C/-d/--max-depth/-m/--max-count取ARG_POS_INT,-g/--glob取ARG_OPAQUE_VALUE;flag 含-n、-i、-l、--files、--files-with-matches、--files-without-match;位置参数为ARG_OPAQUE_VALUE(搜索模式)加ARG_RFILES_OR_CWD。其system_path特意留空,源码中的 TODO 备注了原因(可能期望使用宿主环境自带的rg)。sed(两条规则):由于 GNU sed 的eflag 可执行替换串为 shell 命令(policy 文件中给出了sed 's/y/echo hi/e'的 PoC 注释),sed很难被"常规"方式安全化,于是实现了专属的ARG_SED_COMMAND,只接受"已知安全"的 sed 命令。flag 只保留-n、-u,刻意不支持-i(就地修改)与-f(脚本文件):- 不带
-e:第一个位置参数必须是合法 sed 命令,其后是可读文件; - 带
-e:opt("-e", ARG_SED_COMMAND, required=True),其余位置参数一律视为可读文件。
- 不带
which:flag 限-a、-s;参数为ARG_RFILES(which的参数是程序名,这里按文件路径语义处理);should_not_match排除空参数。
Starlark 的"宏"能力在默认策略里直接体现:common_sed_flags 列表在两条 sed 规则间复用,printenv_system_path 变量在两条 printenv 规则间复用。
五、源码层的校验流程
5.1 Policy::check:三道闸门
policy.rs 中的 Policy::check 按顺序执行:
- 禁止程序名正则:遍历
forbidden_program_regexes,程序名命中即返回Forbidden { cause: Forbidden::Program, .. }; - 禁止子串:任一参数命中
forbidden_substrings_pattern即返回Forbidden { cause: Forbidden::Arg, .. },reason 固定为arg \{arg}` contains forbidden substring`; - 按名查找程序规则:
programs是MultiMap<String, ProgramSpec>,同一程序可挂多条规则;逐条调用ProgramSpec::check,第一条成功即返回;全部失败则返回最后一条错误(默认是NoSpecForProgram,即 unverified)。
因此 Forbidden 枚举的三个变体 Program / Arg / Exec(program.rs#L75-L88)分别对应这三种禁止来源;applied deploy 的例子命中的是第三种——规则本身匹配成功,但 spec 带 forbidden 字段。
5.2 ProgramSpec::check:选项与位置参数的匹配
program.rs#L94-L195 的匹配逻辑:
- 逐参数扫描:以
-开头的参数必须命中allowed_options,否则报UnknownOption;Flag型选项立即记入matched_flags,Value型选项则把下一个参数消费为其值(若下一个参数又以-开头,报OptionFollowedByOptionInsteadOfValue);裸--分隔符明确不支持(DoubleDashNotSupportedYet); - 扫描结束后若某个选项还"欠"一个值,报
OptionMissingValue; - 位置参数交给
resolve_observed_args_with_patterns与args中的匹配器序列对齐,数量或类型不符时报NotEnoughArgs、UnexpectedArguments等错误(完整错误清单见 error.rs); required=True的选项若缺席,报MissingRequiredOptions;- 全部通过后组装
ValidExec { program, flags, opts, args, system_path },若 spec 带forbidden字段则升级为MatchedExec::Forbidden,否则为MatchedExec::Match。
5.3 safe 与 match 的分界:might_write_files
CLI 层(main.rs#L97-L125)在拿到 Match 后调用 ValidExec::might_write_files() 决定输出 safe 还是 match。该方法(valid_exec.rs#L33-L36)检查所有选项值与位置参数的 ArgType:
WriteableFile或Unknown类型返回true(保守:未知类型按可能写文件处理);Literal、OpaqueNonFile、PositiveInteger、ReadableFile、SedCommand返回false。
这也解释了示例 1 与示例 2 的差异:ls -l foo 的参数全是 ReadableFile,输出 safe;cp ... dest 含一个 WriteableFile,输出 match。
六、策略完整性的验证
README 声明 "default.policy 的完整性通过单元测试验证"。机制分两层:
- 加载时自检:每条规则的
should_match/should_not_match示例在策略解析后被逐条执行——Policy::check_each_good_list_individually要求正例必须匹配成功,check_each_bad_list_individually要求负例必须匹配失败(policy.rs#L88-L102),任何违规都收集为PositiveExampleFailedCheck/NegativeExamplePassedCheck报告; - 按程序的测试套件:tests/all.rs 聚合了
tests/suite/下的模块——good.rs、bad.rs(通用正反例),以及针对具体程序的cp.rs、head.rs、ls.rs、pwd.rs、literal.rs、sed.rs,外加parse_sed_command.rs验证 sed 命令解析器。
这种"规则即测试"的设计意味着:修改 default.policy 添加一条规则时,只要同时写对 should_match/should_not_match,规则的正确性就与策略文件同生命周期维护,无需另写测试。
七、小结与延伸阅读
execpolicy-legacy 的核心价值可以浓缩为三点:
- 结构化判定:不回答"能不能跑",而是回答"命令匹配了哪条规则、每个参数是什么类型、用哪个绝对路径执行更可信",把写文件等高风险决策留给了解上下文的调用方;
- 策略即代码:用 Starlark 编写规则,既有宏的复用能力,又保持求值安全与可复现,且规则自带正反例验证;
- 防御纵深:程序名正则、参数子串黑名单、逐选项白名单、专属
SedCommand解析器(封杀 GNU sedeflag 的提权路径)层层设卡。
延伸阅读路径:
- 策略文件本体:codex-rs/execpolicy-legacy/src/default.policy
- CLI 入口与退出码:codex-rs/execpolicy-legacy/src/main.rs
- 策略解析与内建函数:codex-rs/execpolicy-legacy/src/policy_parser.rs
- 参数类型定义:codex-rs/execpolicy-legacy/src/arg_type.rs
- 测试套件:codex-rs/execpolicy-legacy/tests/
- 新版前缀规则引擎所在 crate:
codex-rs/execpolicy/(README 指明迁移方向)
需要说明的适用前提:option_bundling(如 -al 合并写法)与 combined_format(--option=value 写法)在 default.policy 头部被标注为 PLANNED,属于规划中的参数;当前策略语言仍处于演进状态,新增规则时应以现有内建函数签名(见第五、四节源码引用)为准。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00