让 AI 在你的机器上执行命令,这件事的风险是显而易见的。Codex 的安全设计试图在"能用"和"安全"之间找到一个不太难受的平衡点。读它的源码,会发现这个平衡是通过三个层次的机制叠加实现的:审批策略、规则引擎、平台沙箱。三者各司其职,又互相配合。
本文直接从代码出发,分析这三层防线的具体实现。
一、第一层:AskForApproval 审批策略
最外层的控制是一个枚举,定义在 codex-rs/protocol/src/protocol.rs:
pub enum AskForApproval {
/// 只有"已知安全"的只读命令才自动放行,其余全部询问用户
UnlessTrusted,
/// 已废弃:全部自动放行,依赖沙箱保护;失败时再升级给用户
OnFailure,
/// 模型自己决定何时询问用户(默认值)
#[default]
OnRequest,
/// 细粒度控制:分别配置不同类型的审批流程
Granular(GranularApprovalConfig),
/// 永不询问用户;失败直接返回给模型,不升级
Never,
}
五种模式覆盖了从"完全信任"到"完全不信任"的整个区间。默认值是 OnRequest,意思是把决定权交给模型——模型认为有必要时才问用户。
UnlessTrusted 是最保守的非交互模式:只有通过 is_known_safe_command() 检查的命令才自动放行,其他全部要求人工确认。这个模式适合对机器控制权有较高要求的场景。
Granular 模式最有意思,它允许对不同类型的审批请求分别开关:
pub struct GranularApprovalConfig {
/// 沙箱命令审批(含 with_additional_permissions 和 require_escalated)
pub sandbox_approval: bool,
/// execpolicy 规则触发的 prompt
pub rules: bool,
/// skill 脚本执行的审批
#[serde(default)]
pub skill_approval: bool,
/// request_permissions 工具触发的审批
#[serde(default)]
pub request_permissions: bool,
/// MCP elicitation 弹窗
pub mcp_elicitations: bool,
}
这个设计的意图是:不同的使用场景对"哪类操作需要人工介入"有不同的答案。比如在 CI 环境里,你可能希望关闭 sandbox_approval(让命令自动执行),但保留 mcp_elicitations(MCP 工具调用时仍然要求确认)。粒度控制让这种差异化配置成为可能,而不是只能在"全问"和"全不问"之间二选一。
当审批策略和规则引擎产生冲突时,有明确的优先级规则。在 exec_policy.rs 里:
pub(crate) fn prompt_is_rejected_by_policy(
approval_policy: AskForApproval,
prompt_is_rule: bool,
) -> Option<&'static str> {
match approval_policy {
AskForApproval::Never => Some(PROMPT_CONFLICT_REASON),
AskForApproval::OnFailure | AskForApproval::OnRequest | AskForApproval::UnlessTrusted => None,
AskForApproval::Granular(granular_config) => {
if prompt_is_rule {
if !granular_config.allows_rules_approval() {
Some(REJECT_RULES_APPROVAL_REASON)
} else {
None
}
} else if !granular_config.allows_sandbox_approval() {
Some(REJECT_SANDBOX_APPROVAL_REASON)
} else {
None
}
}
}
}
逻辑很清晰:Never 模式下所有 prompt 都被拒绝;Granular 模式下根据 prompt 来源(规则触发还是沙箱触发)分别判断;其他模式不拦截 prompt。
二、第二层:ExecPolicy 规则引擎
审批策略决定"是否询问用户",规则引擎决定"命令本身是否被允许执行"。这是两个正交的维度。
规则的基本结构
规则引擎的核心是前缀匹配。每条规则由一个命令前缀(PrefixPattern)和一个决策(Decision:Allow / Prompt / Forbidden)组成:
pub struct PrefixRule {
pub pattern: PrefixPattern,
pub decision: Decision,
pub justification: Option<String>,
}
pub struct PrefixPattern {
pub first: Arc<str>, // 第一个 token,固定字符串
pub rest: Arc<[PatternToken]>, // 后续 token,可以是固定字符串或候选集合
}
pub enum PatternToken {
Single(String),
Alts(Vec<String>), // 允许多个候选值,如 ["-c"|"--command"]
}
匹配逻辑是前缀匹配:规则的 pattern 只需匹配命令的前 N 个 token,后续 token 不影响匹配结果。这意味着一条 allow git 规则会放行所有以 git 开头的命令,包括 git push --force。
规则按程序名(第一个 token)索引,存储在 MultiMap<String, RuleRef> 里。查找时先精确匹配第一个 token,找到候选规则集合后再逐条检查后续 token。
决策聚合
一条命令可能匹配多条规则。最终决策取所有匹配规则中优先级最高的那个。优先级顺序是 Forbidden > Prompt > Allow——只要有一条规则禁止,命令就被禁止:
fn from_matches(matched_rules: Vec<RuleMatch>) -> Self {
let decision = matched_rules.iter().map(RuleMatch::decision).max();
// ...
Self { decision, matched_rules }
}
这里用了 max(),前提是 Decision 实现了 Ord,且 Forbidden > Prompt > Allow。这是一个"最严格规则优先"的设计,和防火墙的 deny-override 语义一致。
启发式回退
当没有任何规则匹配时,规则引擎不会直接放行,而是调用一个启发式函数 render_decision_for_unmatched_command()。这个函数综合考虑多个因素:
- 命令是否在"已知安全"列表里(
is_known_safe_command) - 命令是否被判定为危险(
command_might_be_dangerous) - 当前的审批策略(
AskForApproval) - 沙箱类型(受限沙箱还是无限制环境)
在 OnRequest 模式 + 受限沙箱的组合下,非危险命令会被直接放行(Decision::Allow),因为沙箱本身提供了保护;但如果命令请求了沙箱覆盖权限(sandbox_permissions.requests_sandbox_override()),则需要用户确认。
被禁止的前缀建议
规则引擎还有一个细节:当用户批准某条命令时,系统会尝试自动生成一条 allow 规则,方便下次跳过询问。但有一批命令前缀被明确列入黑名单,不会被自动建议:
static BANNED_PREFIX_SUGGESTIONS: &[&[&str]] = &[
&["python3"],
&["python3", "-c"],
&["bash"],
&["bash", "-lc"],
&["sh", "-c"],
&["zsh"],
&["node", "-e"],
&["perl", "-e"],
&["sudo"],
&["env"],
&["osascript"],
// ... 完整列表还包括 python, py, pypy, ruby, php, lua,
// powershell, pwsh 及其各种调用形式
];
这些都是"可以执行任意代码的解释器入口"。如果允许自动建议 allow python3,等于允许了所有 Python 代码,这显然不是用户批准某次具体命令时的意图。被禁止的前缀建议列表阻止了这类过度宽泛的规则被静默添加。
注意 git 也在这个列表里——git 可以通过 hooks 执行任意代码,把它加入 allow 前缀同样过于宽泛。
三、第三层:平台沙箱
前两层是"软"控制:审批策略和规则引擎决定是否执行命令,但一旦命令被放行,它在系统上的行为没有任何约束。第三层是"硬"控制:通过操作系统级别的沙箱机制,限制进程实际能做什么。
Codex 在三个平台上使用不同的沙箱技术,统一通过 SandboxManager 管理:
pub enum SandboxType {
None,
MacosSeatbelt,
LinuxSeccomp,
WindowsRestrictedToken,
}
macOS:Seatbelt(sandbox-exec)
macOS 的沙箱基于 Apple 的 Seatbelt 机制,通过 /usr/bin/sandbox-exec 执行。Codex 明确硬编码了这个路径:
pub const MACOS_PATH_TO_SEATBELT_EXECUTABLE: &str = "/usr/bin/sandbox-exec";
注释里解释了原因:只信任 /usr/bin 下的 sandbox-exec,防止攻击者在 PATH 里注入恶意版本。如果 /usr/bin/sandbox-exec 被篡改,攻击者已经有了 root 权限,这是另一个问题。
沙箱策略写在 seatbelt_base_policy.sbpl 文件里,用 Scheme 语法描述。基础策略的核心原则是"默认拒绝":
(version 1)
; start with closed-by-default
(deny default)
; child processes inherit the policy of their parent
(allow process-exec)
(allow process-fork)
(allow signal (target same-sandbox))
从 (deny default) 开始,然后逐条添加允许项。这是正确的安全设计方向——默认拒绝比默认允许更安全,因为遗漏的允许项只会导致功能缺失,而遗漏的拒绝项会导致安全漏洞。
文件系统访问权限在运行时动态生成,根据 SandboxPolicy(ReadOnly、WorkspaceWrite 等)计算出具体的允许路径列表,注入到策略字符串里。
Linux:bubblewrap 和 Landlock
Linux 上使用两种机制的组合:
bubblewrap:基于 Linux user namespace 的容器化工具,通过 bind mount 构建一个隔离的文件系统视图。沙箱进程只能看到被显式挂载进来的目录。Codex 把它封装成一个独立的辅助程序 codex-linux-sandbox:
pub const CODEX_LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
pub fn create_linux_sandbox_command_args_for_permission_profile(
command: Vec<String>,
command_cwd: &Path,
permission_profile: &PermissionProfile,
sandbox_policy_cwd: &Path,
use_legacy_landlock: bool,
allow_network_for_proxy: bool,
) -> Vec<String> {
// 把 permission_profile 序列化成 JSON,作为参数传给 sandbox helper
let permission_profile_json = serde_json::to_string(permission_profile)...;
let mut linux_cmd = vec![
"--sandbox-policy-cwd", sandbox_policy_cwd,
"--command-cwd", command_cwd,
"--permission-profile", permission_profile_json,
];
// ...
}
权限描述以 JSON 形式传递给辅助程序,由辅助程序在内部解析并构建 bubblewrap 参数。这个设计把"权限描述"和"沙箱实现"解耦——主进程只需要描述想要什么权限,具体怎么实现交给辅助程序。
Landlock:Linux 内核 5.13 引入的文件系统访问控制机制,不依赖 user namespace,可以在不需要 root 权限的情况下限制进程的文件系统访问。Codex 支持 --use-legacy-landlock 参数,在不支持 bubblewrap 的环境(如 WSL1)下回退到纯 Landlock 模式。
WSL1 的情况值得注意:WSL1 不支持 user namespace,因此 bubblewrap 无法工作。代码里有明确的检测和错误提示:
pub(crate) const WSL1_BWRAP_WARNING: &str = concat!(
"Codex's Linux sandbox uses bubblewrap, which is not supported on WSL1 ",
"because WSL1 cannot create the required user namespaces. ",
"Use WSL2 for sandboxed shell commands."
);
沙箱选择逻辑
SandboxManager::select_initial() 决定使用哪种沙箱:
pub fn select_initial(&self, file_system_policy, network_policy, pref, ...) -> SandboxType {
match pref {
SandboxablePreference::Forbid => SandboxType::None,
SandboxablePreference::Require => get_platform_sandbox(...),
SandboxablePreference::Auto => {
if should_require_platform_sandbox(file_system_policy, network_policy, ...) {
get_platform_sandbox(...)
} else {
SandboxType::None
}
}
}
}
Auto 模式下,只有当权限配置需要沙箱保护时才启用沙箱。如果用户配置了 DangerFullAccess 或 ExternalSandbox(表示已经在外部沙箱里运行),则跳过平台沙箱。
四、三层防线的协作方式
三层防线不是简单的串联,而是有复杂的交互:
规则引擎可以绕过沙箱。当 ExecPolicy 规则明确 Allow 某条命令时,bypass_sandbox 标志会被设置为 true,命令直接执行,不经过平台沙箱。这是性能优化:如果命令已经被明确信任,再套一层沙箱只是增加开销。
Decision::Allow => ExecApprovalRequirement::Skip {
bypass_sandbox: commands.iter().all(|command| {
exec_policy
.matches_for_command_with_options(command, None, &match_options)
.iter()
.any(|rule_match| {
is_policy_match(rule_match) && rule_match.decision() == Decision::Allow
})
}),
// ...
},
注意条件:只有当所有解析出的命令片段都被明确的规则(PrefixRuleMatch,不是启发式)Allow 时,才绕过沙箱。启发式允许的命令仍然走沙箱。
审批策略可以把 Prompt 变成 Forbidden。规则引擎说"需要询问用户",但如果审批策略是 Never 或 Granular 且对应的开关关闭,prompt 会被直接转化为 Forbidden。这让无人值守的自动化场景成为可能:明确告诉系统"不要等我,遇到需要询问的命令直接拒绝"。
沙箱策略影响启发式决策。在 render_decision_for_unmatched_command() 里,受限沙箱下的非危险命令会被直接放行,而无沙箱环境下同样的命令可能需要 prompt。沙箱的存在改变了风险评估,进而影响了规则引擎的启发式判断。
五、设计上的取舍
这套设计有几个值得注意的取舍:
规则是前缀匹配,不是精确匹配。allow cargo build 会同时允许 cargo build --release 和 cargo build --target x86_64-unknown-linux-musl,但也会允许 cargo build && rm -rf /——不过后者在命令解析阶段会被拆分成两条命令,rm -rf / 会单独被规则引擎评估。命令解析(parse_shell_lc_plain_commands)是安全的关键前提。
规则可以在运行时追加。用户批准某条命令时,系统可以把对应的前缀规则写入 ~/.codex/rules/default.rules,下次同类命令自动放行。这是用户体验的优化,但也意味着规则库会随使用积累而膨胀。BANNED_PREFIX_SUGGESTIONS 列表的存在,是防止这个机制被滥用的安全阀。
沙箱保护 .codex 和 .git/hooks。在 WorkspaceWrite 模式下,工作目录是可写的,但 WritableRoot 结构会把某些子路径标记为只读:
pub struct WritableRoot {
pub root: AbsolutePathBuf,
pub read_only_subpaths: Vec<AbsolutePathBuf>, // .codex, .git/hooks 等
pub protected_metadata_names: Vec<String>,
}
这防止了一类攻击:模型通过修改 .git/hooks 或 .codex/rules 来提升自己的权限。即使在"可以写工作目录"的模式下,这些路径仍然受保护。
六、总结
Codex 的执行安全模型可以用一句话概括:策略决定是否询问,规则决定是否允许,沙箱决定能做什么。
三层防线的设计思路:
- AskForApproval:控制人机交互的频率,从"全问"到"全不问",粒度可细化到不同类型的操作
- ExecPolicy:前缀匹配的规则引擎,支持 Allow / Prompt / Forbidden 三种决策,启发式兜底,规则可动态追加
- 平台沙箱:macOS 用 Seatbelt(
deny default的 SBPL 策略),Linux 用 bubblewrap + Landlock,统一通过SandboxManager抽象
这套设计的核心假设是:不同的使用场景需要不同的安全级别,没有一个配置能适合所有人。所以它提供了足够多的旋钮,让用户根据自己的信任模型来调整。代价是复杂性——理解这三层如何交互,需要读相当多的代码。