codex cli 源码教程 | 第十二篇:命令执行、审批与跨平台沙箱
上一篇从 ToolRouter 出发,分析了模型如何看到工具、工具调用如何路由到
Handler,以及并行工具为什么仍按调用顺序回填结果。
但当模型调用:
shell_command
exec_command
apply_patch
真正的副作用并不会立即发生。
命令还要经过:
参数解析
Environment 选择
Additional Permissions 合并
Exec Policy 求值
PermissionRequest Hook
Guardian 或用户审批
Sandbox 选择
Network Approval 注册
进程启动
输出采集
Sandbox Denial 识别
受控升级与第二次执行
这条链路同时回答三个安全问题:
是否允许执行?
允许在什么权限边界内执行?
失败后是否允许扩大权限重试?
三者不能混为一谈。
例如:
Approved
只表示审批通过,不必然表示无沙箱执行。
又如:
Exec Policy = Allow
只有当所有解析出的命令段都被显式 allow 规则覆盖时,才可能让第一次执行直接绕过
文件系统沙箱。
如果权限配置中存在 denied-read 路径,即使命令已经获得批准,也不能丢掉沙箱,因为
denied-read 只能由沙箱强制执行。
本篇将沿两条主链展开:
shell_command
-> ShellRuntime
-> execute_env
exec_command
-> UnifiedExecRuntime
-> UnifiedExecProcessManager
-> PTY / Pipe / Remote Exec Server
然后把它们汇合到共同的:
ExecPolicyManager
ToolOrchestrator
SandboxManager
NetworkApprovalService
之中。
本篇目标
阅读完成后,你应该能够:
- 跟踪
shell_command与exec_command的完整执行链。 - 区分 Approval Policy、Exec Policy、Permission Profile 和 Sandbox Policy。
- 解释
Never、OnRequest、UnlessTrusted与Granular的真实语义。 - 说明显式规则、启发式判断与危险命令检测的优先级。
- 理解为什么审批通过不等于解除沙箱。
- 解释
ApprovedForSession的精确缓存范围。 - 说明 Additional Permissions 如何只扩大单次命令的沙箱权限。
- 跟踪 macOS Seatbelt、Linux Bubblewrap/Seccomp/Landlock 和 Windows Sandbox。
- 解释 Sandbox Denial 如何识别,以及何时允许第二次执行。
- 区分 Immediate 与 Deferred Network Approval。
- 理解 Unified Exec 的后台进程、
session_id与write_stdin。 - 建立审批策略、命令类别与实际执行边界的决策矩阵。
1. 本篇源码地图
命令入口
| 文件 | 职责 |
|---|---|
| handlers/shell.rs | Legacy Shell 共享执行入口 |
| handlers/shell/shell_command.rs | shell_command 参数解析与 Shell argv 构造 |
| handlers/unified_exec/exec_command.rs | exec_command、Environment 与后台会话入口 |
| handlers/unified_exec/write_stdin.rs | 写入或轮询 Unified Exec 会话 |
| handlers/apply_patch.rs | 拦截 Shell 中的 apply_patch 并复用安全链 |
Runtime 与编排
| 文件 | 职责 |
|---|---|
| runtimes/shell.rs | Shell 审批键、网络审批与实际执行 |
| runtimes/unified_exec.rs | Unified Exec 审批、沙箱与进程启动 |
| runtimes/apply_patch.rs | Patch 审批键与沙箱重试 |
| tools/orchestrator.rs | 审批、沙箱选择、拒绝识别和二次执行 |
| tools/sandboxing.rs | Approval/Sandbox Runtime Trait 与共享策略 |
策略与执行
| 文件 | 职责 |
|---|---|
| exec_policy.rs | 命令规则加载、求值与策略修订 |
| execpolicy/src/policy.rs | Prefix Rule 匹配和严格度聚合 |
| execpolicy/src/parser.rs | Starlark Rule Parser |
| shell-command/src/command_safety | Safe/Dangerous Command 启发式 |
| exec.rs | 子进程、超时、输出采集和拒绝分类 |
| sandboxing/src/denial.rs | Sandbox Denial 启发式识别 |
权限与网络
| 文件 | 职责 |
|---|---|
| protocol/src/models.rs | Permission Profile 与 Additional Permissions |
| protocol/src/permissions.rs | 文件系统与网络权限求值 |
| protocol/src/protocol.rs | Approval Policy 与 Review Decision |
| tools/network_approval.rs | Host 级网络审批状态机 |
| sandboxing/src/policy_transforms.rs | 权限合并和有效权限计算 |
Unified Exec
| 文件 | 职责 |
|---|---|
| unified_exec/process_manager.rs | Process Store、PTY/Pipe、轮询与清理 |
| unified_exec/process.rs | 统一进程对象与输出 Buffer |
| unified_exec/async_watcher.rs | 后台退出监听和事件终态 |
| exec-server/src | Remote Environment 执行后端 |
跨平台沙箱
| 文件 | 职责 |
|---|---|
| sandboxing/src/manager.rs | 平台选择与 Command Transform |
| sandboxing/src/seatbelt.rs | macOS Seatbelt Policy 生成 |
| sandboxing/src/landlock.rs | Linux Helper argv 生成 |
| linux-sandbox/src/linux_run_main.rs | Linux Sandbox 两阶段启动 |
| linux-sandbox/src/bwrap.rs | Bubblewrap Mount Namespace |
| linux-sandbox/src/landlock.rs | Seccomp 与 Legacy Landlock |
| sandboxing/src/windows.rs | Windows Backend 能力判断 |
| windows-sandbox-rs/src | Token、ACL、WFP、PTY 和 Elevated Runner |
完整主链可以先压缩为:
Model Tool Call
-> Handler
-> Additional Permission Resolution
-> ExecPolicyManager
-> ToolOrchestrator
-> PermissionRequest Hook
-> Guardian / User Approval
-> SandboxManager
-> NetworkApprovalService
-> ToolRuntime
-> Denial Classification
-> Optional Retry
-> Exec Output
-> Tool Result
2. 先区分四层安全策略
命令执行最容易出现的误解,是把所有安全配置都叫作“审批”。
实际上至少有四层。
| 层次 | 核心问题 | 典型类型 |
|---|---|---|
| Approval Policy | 是否允许发起某类审批 | AskForApproval |
| Exec Policy | 这条命令应 Allow、Prompt 还是 Forbidden | Policy |
| Permission Profile | 进程能读写和联网到哪里 | PermissionProfile |
| Sandbox Runtime | 当前平台如何强制执行权限 | SandboxType |
它们的关系不是覆盖,而是组合:
Exec Policy 决定是否需要批准
Approval Policy 决定该批准能否出现
Permission Profile 定义执行边界
Sandbox Runtime 把边界落实到操作系统
因此:
Allow != Full Access
Approved != Unsandboxed
Sandbox Denied != User Denied
3. 两条命令入口为何并存
Codex 当前保留两套 Shell 风格工具:
shell_command
exec_command + write_stdin
shell_command 更接近一次性命令:
输入完整 Shell 字符串
等待命令结束
返回聚合输出
Unified Exec 更接近终端会话:
启动命令
等待一小段 yield time
返回已有输出
进程未结束时返回 session_id
后续用 write_stdin 继续交互或轮询
两者在 Tool Surface 上可以互相替代,但在执行模型上并不相同。
4. ShellCommandHandler 先把字符串降低为 argv
shell_command 的模型参数主要是:
command
workdir
timeout_ms
login
sandbox_permissions
additional_permissions
justification
prefix_rule
Handler 不把 command 直接传给 execve,而是根据当前用户 Shell 构造 argv:
pub(super) fn base_command(
shell: &Shell,
command: &str,
use_login_shell: bool,
) -> Vec<String> {
shell.derive_exec_args(command, use_login_shell)
}
例如,在类 Unix Shell 中通常会形成:
/bin/zsh -lc "<command>"
或:
/bin/bash -lc "<command>"
审批系统看到的是这个最终 argv,但 Exec Policy 会进一步尝试解析其中的 Shell Body。
5. Login Shell 是受配置约束的
login=true 不是模型可以无条件开启的能力。
if !allow_login_shell && login == Some(true) {
return Err(FunctionCallError::RespondToModel(
"login shell is disabled by config; ...".to_string(),
));
}
原因是 Login Shell 可能加载:
.profile
.bash_profile
.zprofile
其他 Shell Startup Script
这些脚本本身可能改变:
PATH
代理变量
命令 Alias
启动副作用
所以它属于执行语义,而不是单纯的 UI 选项。
6. Shell Handler 的共享入口是 run_exec_like
ShellCommandHandler::handle_call 完成参数解析后,会进入:
async fn run_exec_like(
args: RunExecLikeArgs,
) -> Result<FunctionToolOutput, FunctionCallError>
它的主要步骤是:
1. 取得 Environment FileSystem
2. 合并已批准的 Turn/Session Permission
3. 校验 Additional Permissions
4. 拦截 apply_patch
5. 生成 ExecApprovalRequirement
6. 构造 ShellRequest
7. 调用 ToolOrchestrator
8. 发布 Begin/End Event
9. 格式化 Tool Output
这里开始,Legacy Shell 与普通子进程执行进入统一安全边界。
7. Unified Exec 先选择 Environment
exec_command 支持多 Environment:
Local Environment
Remote Exec Server Environment
其他附加 Environment
Handler 会通过:
resolve_tool_environment(
&step_context.environments,
environment_id,
)
选择目标。
这一步必须早于:
CWD 解析
Shell 选择
Permission Path 归一化
Approval Key 构造
Network Approval Attribution
因为相同命令在不同 Environment 中不是同一个安全请求。
8. PathUri 让远端路径保留目标系统语义
Unified Exec 使用:
pub cwd: PathUri
而 Legacy Shell 主要使用:
pub cwd: AbsolutePathBuf
PathUri 可以表达:
file:///Users/alice/project
file:///C:/repo
并保留 Path Convention。
本地沙箱仍需要本机路径,因此 Handler 会同时检查:
to_abs_path()
infer_path_convention() == native()
如果目标是远端 Environment 且本地无需构造平台沙箱,可以继续保留 Foreign Path;
否则必须拒绝在错误平台语义下解释路径。
9. Unified Exec 分配 Process ID 在审批之前
exec_command 会先调用:
let process_id =
manager.allocate_process_id().await;
生产模式使用随机区间:
1000..100000
测试模式使用确定性递增 ID。
如果后续参数校验、Patch 拦截或进程启动失败,必须调用:
release_process_id(process_id)
否则 Reserved ID 会泄漏,Process Store 也可能保留无效状态。
10. Shell 字符串与审批命令不是两份独立事实
Unified Exec 会保留两个表示:
hook_command
command: Vec<String>
hook_command 用于:
PreToolUse / PostToolUse
PermissionRequest Hook
用户可读展示
command 用于:
Exec Policy
Approval Key
Sandbox Transform
实际启动
Hook 可以改写输入后,Handler 会重新解析 Tool Payload,从而避免展示命令与执行命令失配。
11. Shell 中的 apply_patch 会被提前拦截
模型有时会把 apply_patch 作为 Shell 命令调用。
Codex 不会让它直接成为普通子进程:
shell_command / exec_command
-> intercept_apply_patch
-> parse + verify
-> ApplyPatchRuntime
-> ToolOrchestrator
这样可以保留:
文件级审批
Patch Delta
Diff Tracker
Patch Event
路径级权限检查
部分成功记录
而不是把 Patch 降级为一个不可观察的 Shell 副作用。
12. Additional Permissions 不是完整 Permission Profile 替换
单次命令可以请求:
pub struct AdditionalPermissionProfile {
pub network: Option<NetworkPermissions>,
pub file_system: Option<FileSystemPermissions>,
}
它是 Partial Overlay,而不是新建一个完整权限域。
典型请求是:
在当前工作区权限上
额外允许写 /tmp/generated
或额外允许网络
对应:
sandbox_permissions = with_additional_permissions
additional_permissions = ...
13. 三种 SandboxPermissions 语义不同
pub enum SandboxPermissions {
UseDefault,
RequireEscalated,
WithAdditionalPermissions,
}
| 值 | 语义 |
|---|---|
UseDefault |
使用 Turn 的原始 Permission Profile |
RequireEscalated |
请求无文件系统沙箱执行 |
WithAdditionalPermissions |
仍在沙箱内,但扩大本次命令的权限 |
WithAdditionalPermissions 的目标是:
最小增量授权
而不是:
因为缺少一个目录权限,所以解除全部沙箱
14. Additional Permissions 先经过结构校验
normalize_and_validate_additional_permissions 会拒绝:
Feature 未启用
不是 OnRequest 且权限未预批准
缺少 additional_permissions
请求内容为空
单独提供 additional_permissions 却未指定对应 sandbox_permissions
Glob Permission 也有额外限制:
Glob Pattern 只允许 Deny
不能用一个宽泛 Glob 临时授予读写权限。
15. 已批准权限可以自动应用到后续命令
Session 和 Turn 可以保存 Sticky Permission Grant。
每次 Shell 命令前都会合并:
显式 Additional Permissions
Session Granted Permissions
Turn Granted Permissions
如果当前有效请求完全落在已批准 Grant 中:
permissions_preapproved = true
后续命令不需要再次发起同一权限范围的授权。
但命令本身仍要经过正常 Exec Policy:
权限已批准 != 命令已批准
16. RequireEscalated 不会被 Sticky Grant 偷换
apply_granted_turn_permissions 对:
SandboxPermissions::RequireEscalated
直接返回:
permissions_preapproved = false
原因是一次目录或网络 Grant 不能自动升级成:
未来任意命令都可无沙箱执行
这是权限范围保持的重要边界。
17. Exec Policy 的输入是 ExecApprovalRequest
Shell 与 Unified Exec 最终都会构造:
pub(crate) struct ExecApprovalRequest<'a> {
pub command: &'a [String],
pub approval_policy: AskForApproval,
pub permission_profile: PermissionProfile,
pub windows_sandbox_level: WindowsSandboxLevel,
pub sandbox_permissions: SandboxPermissions,
pub prefix_rule: Option<Vec<String>>,
}
这说明命令决策不仅依赖 argv。
它还需要知道:
当前是否允许询问
当前是否真的有平台沙箱
命令是否主动请求权限覆盖
模型是否给出可持久化 Prefix 建议
18. Shell Body 会先拆成多个 Plain Command
Exec Policy 会尝试:
parse_shell_lc_plain_commands(command)
例如:
cd repo && rg TODO | head -20
可能被拆成:
["cd", "repo"]
["rg", "TODO"]
["head", "-20"]
规则必须对所有实际命令段共同求值,而不是只检查最外层:
bash
否则任何 Prefix Rule 都可能被 bash -lc 绕过。
19. 复杂 Shell 解析会进入保守路径
如果完整 Plain Command Parser 失败,Codex 还会尝试:
parse_shell_lc_single_command_prefix(command)
这类结果会标记:
used_complex_parsing = true
随后产生两个保守效果:
不把 Safe Command 启发式直接视为可信
不自动推导 Exec Policy Amendment
因为只看到命令前缀,不足以证明整个 Shell Body 安全。
20. Windows PowerShell 有单独的 Lowering
在 Windows 上,Exec Policy 还会尝试:
parse_powershell_command_into_plain_commands
Lowering 后的命令会标记:
ExecPolicyCommandOrigin::PowerShell
随后使用 PowerShell 专用:
Safe Command 判断
Dangerous Command 判断
不能把 Bash Token 规则机械套到 PowerShell AST 上。
21. Prefix Rule 是 Token 级匹配
规则使用 Starlark 风格:
prefix_rule(
pattern = ["git", "status"],
decision = "allow",
)
它匹配:
git status
git status --short
但不匹配:
git -C repo status
因为 Token 顺序不同。
规则不是对子字符串做正则搜索。
22. Prefix Rule 支持 Token Alternatives
一个 Pattern Token 可以是一组候选值:
prefix_rule(
pattern = ["cargo", ["test", "check"]],
decision = "allow",
)
它同时匹配:
cargo test
cargo check
但仍保持 Token 边界。
这比:
字符串 starts_with
更不容易把相似命令误判为已批准命令。
23. Absolute Executable 可以回退到 Basename Rule
Exec Policy 默认启用:
MatchOptions {
resolve_host_executables: true,
}
因此:
/usr/bin/git status
可以回退匹配:
prefix_rule(
pattern = ["git", "status"],
decision = "allow",
)
也可以使用:
host_executable(
name = "git",
paths = ["/usr/bin/git"],
)
把 Basename Fallback 限定到明确路径。
24. 多个匹配取最严格 Decision
Decision 的顺序是:
Allow < Prompt < Forbidden
Evaluation::from_matches 对所有命中取:
matched_rules
.iter()
.map(RuleMatch::decision)
.max()
因此:
一个 Allow + 一个 Prompt = Prompt
一个 Allow + 一个 Forbidden = Forbidden
不要把 Prefix Rule 理解成“最后一条命中获胜”。
25. 多命令 Shell 也取全局最严格结果
对:
safe_read && dangerous_write
Codex 会分别匹配两个命令,再合并全部 Rule Match。
只要任意命令段为:
Forbidden
整个 Tool Call 就是:
Forbidden
这避免部分命令借助 Shell Chain 混入已批准前缀。
26. 无规则命中时才进入启发式
每个命令段的顺序是:
Exact Rule
-> Host Executable Basename Rule
-> Unmatched Command Heuristics
只要显式规则命中,就不会再用 Safe/Dangerous Heuristic 替换它。
因此:
prefix_rule(
pattern = ["ls"],
decision = "prompt",
)
可以强制让通常安全的 ls 进入审批。
27. Known Safe Command 是窄 Safelist
通用 Safelist 包含部分只读或低副作用命令,例如:
cat
grep
head
ls
pwd
rg
stat
tail
wc
whoami
但不是只看程序名。
例如:
find -delete
rg --pre <command>
base64 --output=<file>
git diff --output=<file>
会因为参数可产生副作用而退出 Safelist。
28. Git Safe 判断会跳过危险变体
Git 只允许明确的只读子命令:
status
log
diff
show
branch 的只读形态
并拒绝可能改写执行环境或写文件的选项:
-C
-c
--exec-path
--git-dir
--work-tree
--output
--ext-diff
--textconv
因此“Git 命令”本身不是一个足够窄的安全类别。
29. Dangerous Command 是独立检测
通用危险命令检测会识别:
rm -f
rm -rf
sudo rm -rf
bash -lc 中的危险命令段
Windows 还会使用专用 PowerShell 危险命令检测。
它的目标不是覆盖所有危险程序,而是识别一组不能仅依赖默认流程放行的高风险形态。
未知命令不等于危险命令:
Unknown
Dangerous
是两个不同状态。
30. AskForApproval 有四种策略
pub enum AskForApproval {
UnlessTrusted,
OnRequest,
Granular(GranularApprovalConfig),
Never,
}
它们控制:
何时允许出现审批
未匹配命令如何处理
Sandbox Denial 后能否请求升级
其中 OnRequest 是默认值。
旧配置名:
on-failure
只是兼容 Alias,不能据此推断当前实现会对所有 Sandbox Failure 自动弹窗。
31. UnlessTrusted 的核心是 Safe Command Safelist
无显式规则命中时:
Simple Known Safe Command
-> Allow
其他命令
-> Prompt
前提是:
不是 Complex Parsing
所以:
ls
可能自动通过,而复杂 Shell 拼接即使以 ls 开头也不会获得同样待遇。
32. OnRequest 默认依赖沙箱,而不是每条命令都询问
在 Managed Restricted 文件系统中,无显式规则命中的普通命令:
UseDefault
-> Allow
-> 在沙箱内执行
只有以下情况会触发 Prompt:
危险命令
显式 Prompt Rule
命令请求 Sandbox Override
因此 OnRequest 的正常路径是:
先让沙箱执行
而不是先询问所有写命令
33. Never 是“不询问”,不是“全部允许”
对于普通未匹配命令:
Never
-> Allow
-> 依赖沙箱限制
对于危险命令:
Managed Permission Profile
-> Forbidden
但如果用户明确选择:
PermissionProfile::Disabled
PermissionProfile::External
危险命令可被视为已由外部信任选择处理。
Never 的关键语义是:
不向用户发起交互式升级请求
34. Granular 把审批类别拆开
pub struct GranularApprovalConfig {
pub sandbox_approval: bool,
pub rules: bool,
pub skill_approval: bool,
pub request_permissions: bool,
pub mcp_elicitations: bool,
}
对命令执行最关键的是:
sandbox_approval
rules
如果 Prompt 来自显式 Exec Policy Rule:
检查 rules
如果 Prompt 来自危险命令或 Sandbox Override:
检查 sandbox_approval
对应开关为 false 时,不是跳过审批继续执行,而是:
Forbidden
35. ExecApprovalRequirement 是编排器的统一输入
Exec Policy 最终输出:
pub(crate) enum ExecApprovalRequirement {
Skip {
bypass_sandbox: bool,
proposed_execpolicy_amendment:
Option<ExecPolicyAmendment>,
},
NeedsApproval {
reason: Option<String>,
proposed_execpolicy_amendment:
Option<ExecPolicyAmendment>,
},
Forbidden {
reason: String,
},
}
这一步把:
Rule Engine
Heuristics
Approval Policy
折叠成 ToolOrchestrator 能理解的三态。
36. Allow 只有两种不同执行含义
如果命令只是被启发式允许:
Skip {
bypass_sandbox: false
}
它会跳过审批,但仍使用正常沙箱。
如果每个解析出的命令段都被显式 Allow Rule 覆盖:
Skip {
bypass_sandbox: true
}
它才表达:
该命令前缀被策略视为完全可信
这是 Exec Policy 中最需要谨慎使用的能力。
37. 显式 Allow 必须覆盖所有命令段
对:
git status && custom-script
即使:
git status
命中显式 Allow,也不能让整个调用绕过沙箱。
bypass_sandbox 的计算要求:
commands.iter().all(...)
每个命令段都必须至少命中一个显式 Allow Rule。
38. Prompt Reason 只引用显式 Prompt Rule
如果 Prompt 来自启发式,Reason 可以为空。
如果 Prompt 来自规则:
prefix_rule(
pattern = ["deploy"],
decision = "prompt",
justification = "需要访问生产环境",
)
Codex 会优先选择最具体的 Prompt Prefix,并生成:
`deploy ...` requires approval: 需要访问生产环境
具体度只用于选择展示理由,不改变多规则取最严格结果的原则。
39. Forbidden Reason 也选择最具体前缀
例如:
prefix_rule(
pattern = ["git", "reset", "--hard"],
decision = "forbidden",
justification = "请改用可恢复操作",
)
最终错误会包含:
`git reset --hard ...` rejected: 请改用可恢复操作
这让策略不只负责拒绝,还能向 Agent 提供替代路径。
40. 模型可以建议 Prefix Rule Amendment
Tool 参数可以携带:
prefix_rule
但 Codex 不会盲目持久化。
建议必须满足:
不为空
不属于禁止的宽泛前缀
当前没有显式 Policy Rule 命中
加入该规则后可 Allow 所有解析命令段
不是 Complex Parsing Fallback
最终 Amendment 仍需要用户通过:
ApprovedExecpolicyAmendment
41. 解释器和通用 Shell 前缀不会被建议持久化
禁止建议列表包括:
python
python -c
bash -lc
sh -c
zsh -lc
pwsh -Command
git
sudo
node -e
osascript
原因是这些前缀本身可以承载任意后续逻辑。
持久化:
allow ["python", "-c"]
等价于给任意 Python 代码创建长期绕过入口。
42. Policy Amendment 同时更新文件与内存
批准后,Codex 会把规则追加到:
$CODEX_HOME/rules/default.rules
随后更新内存中的:
ArcSwap<Policy>
更新过程由:
Semaphore(1)
串行化,避免两个审批同时改写规则文件。
内存更新前还会再次检查当前策略,避免重复添加已经生效的 Allow Rule。
43. Policy 只从有效且可信的配置层加载
加载顺序遍历:
Lowest Precedence
-> Highest Precedence
但禁用或不可信的 Project Layer 不会进入规则集合。
Requirements 中的 Exec Policy 还会作为 Overlay 合入。
对 Prefix Rule 而言,多个同时命中的结果仍取最严格 Decision,所以不能依赖文件顺序把Forbidden 静默降级为 Allow。
44. Rule Parse Error 采用降级而不是完全启动失败
load_exec_policy_with_warning 遇到 Rule Parse Error 时:
保留 Requirements Policy
返回 Warning
但目录读取等基础 I/O 错误仍会向上返回。
这一区分保证:
用户规则写错时可见且可恢复
强制 Requirements 不会因此丢失
45. ToolOrchestrator 是安全主状态机
核心签名是:
pub async fn run<Rq, Out, T>(
&mut self,
tool: &mut T,
req: &Rq,
tool_ctx: &ToolCtx,
turn_ctx: &TurnContext,
approval_policy: AskForApproval,
) -> Result<OrchestratorRunResult<Out>, ToolError>
where
T: ToolRuntime<Rq, Out>;
它不关心具体是:
Shell
Unified Exec
Apply Patch
只依赖 ToolRuntime 提供审批、沙箱和执行行为。
46. ToolRuntime 组合三个职责
Approvable
Sandboxable
run(...)
可以概括为:
pub(crate) trait ToolRuntime<Req, Out>:
Approvable<Req> + Sandboxable
{
fn network_approval_spec(...) -> Option<...>;
fn sandbox_cwd(...) -> Option<&PathUri>;
async fn run(...) -> Result<Out, ToolError>;
}
ToolOrchestrator 负责流程,Runtime 负责工具特有细节。
47. Orchestrator 的八步主流程
1. 取得 ExecApprovalRequirement
2. 执行首次审批
3. 计算 Sandbox Override
4. 选择 Initial Sandbox
5. 注册 Network Approval
6. 执行 Runtime
7. 分类 Sandbox Denial
8. 必要时审批并进行第二次执行
任何普通错误都不会自动进入重试。
只有明确的:
SandboxErr::Denied
才会进入升级判断。
48. PermissionRequest Hook 优先于 Guardian 和用户
当 Runtime 提供:
PermissionRequestPayload
审批顺序是:
PermissionRequest Hook
-> Guardian Review
-> User Approval
Hook 可以返回:
Allow
Deny { message }
None
None 才继续后续审批路径。
这使组织级静态规则可以在 UI 弹窗前直接做决定。
49. Strict Auto Review 会跳过 PermissionRequest Hook
Strict Auto Review 下:
Skip Requirement
也会进入 Guardian Review。
此时 Orchestrator 设置:
evaluate_permission_request_hooks = false
避免同一安全请求同时被 Hook 和 Strict Guardian 重复裁决。
这是一条特定模式下的例外,不能简单概括为 Hook 永远先执行。
50. Guardian Review ID 与 Tool Call ID 分离
ApprovalCtx 同时保存:
call_id
guardian_review_id
两者分别代表:
正在执行的 Tool Item
一次具体 Review 生命周期
同一个 Tool Call 可能因为 Sandbox Retry 产生第二次 Review。
如果复用同一 ID,拒绝消息、Override 和 App Server 通知会相互混淆。
51. 审批结果不只有 Approved 和 Denied
pub enum ReviewDecision {
Approved,
ApprovedExecpolicyAmendment { ... },
ApprovedForSession,
NetworkPolicyAmendment { ... },
Denied,
TimedOut,
Abort,
}
Denied 与 Abort 都阻止执行,但上层交互含义不同:
Denied
-> Agent 可以继续尝试其他方案
Abort
-> 等待用户下一条输入
52. ApprovedForSession 使用精确 Approval Key
Approval Cache 内部是:
pub(crate) struct ApprovalStore {
map: HashMap<String, ReviewDecision>,
}
Key 先序列化为 JSON。
只有:
ReviewDecision::ApprovedForSession
会写入缓存。
一次性 Approved 不会被未来请求复用。
53. Shell Approval Key 包含完整安全上下文
Shell Key 包含:
Environment ID
Canonical Command
CWD
Sandbox Permissions
Additional Permissions
因此:
同一命令 + 不同目录
同一命令 + 不同 Environment
同一命令 + 更大的权限请求
都不是同一个缓存项。
54. Unified Exec Key 还包含 TTY
Unified Exec 额外保存:
tty
原因是:
非交互 Pipe
交互式 PTY
具有不同的输入能力和进程行为。
批准一个只输出结果的 Pipe Command,不应自动批准同一命令的交互式会话形态。
55. Command 会在进入 Approval Key 前规范化
Runtime 使用:
canonicalize_command_for_approval
目的不是改变实际执行,而是让等价 Wrapper 形式拥有稳定审批身份。
但 Key 仍保留:
完整 Token
CWD
Environment
Permission
TTY
不会退化成危险的“只按程序名缓存”。
56. Apply Patch 按 Environment 和文件分别缓存
Patch Key 是:
pub(crate) struct ApplyPatchApprovalKey {
environment_id: String,
path: PathUri,
}
一个 Patch 修改多个文件时,会生成多个 Key。
只有全部 Key 都已:
ApprovedForSession
才跳过新审批。
如果用户批准本次 Patch 为 Session Scope,每个文件 Key 会分别写入缓存,未来修改其中子集也可复用。
57. 首次 Sandbox Override 有两个来源
pub(crate) enum SandboxOverride {
NoOverride,
BypassSandboxFirstAttempt,
}
可能绕过首次沙箱的来源是:
所有命令段被显式 Allow Rule 覆盖
SandboxPermissions::RequireEscalated
WithAdditionalPermissions 默认仍留在沙箱中。
58. Denied Read 会阻止无沙箱执行
关键判断是:
pub(crate) fn unsandboxed_execution_allowed(
policy: &FileSystemSandboxPolicy,
) -> bool {
!policy.has_denied_read_restrictions()
}
如果 Permission Profile 明确拒绝读取:
.env
secret/
凭据文件
无沙箱执行会让这些 Deny 失效。
因此,即使:
RequireEscalated
Exec Policy Explicit Allow
也不能绕过沙箱。
59. RequireEscalated 在 Denied Read 下会降为 UseDefault
Runtime 调用:
sandbox_permissions_preserving_denied_reads
当:
RequireEscalated + Denied Read
实际 Launch Permission 会变成:
UseDefault
这不是忽略用户拒绝读取的策略,而是优先保持更严格的安全不变量。
60. PermissionProfile 明确谁负责隔离
pub enum PermissionProfile {
Managed {
file_system: ManagedFileSystemPermissions,
network: NetworkSandboxPolicy,
},
Disabled,
External {
network: NetworkSandboxPolicy,
},
}
三种 Enforcement 分别是:
Managed
-> Codex 构造平台沙箱
Disabled
-> Codex 不应用外层文件系统沙箱
External
-> 文件系统隔离由调用者负责
External 不是“没有安全边界”,而是边界所有权在 Codex 之外。
61. 文件系统权限由 Entry 组合
pub struct FileSystemSandboxEntry {
pub path: FileSystemPath,
pub access: FileSystemAccessMode,
}
Access Mode 是:
Read
Write
Deny
Path 可以是:
绝对路径
Glob Pattern
Special Path
Special Path 包括:
:root
:minimal
:project_roots
:tmpdir
:slash_tmp
62. 同路径冲突优先级是 Deny > Write > Read
源码明确说明:
deny beats write
write beats read
同时,路径匹配会优先更具体的 Entry。
因此可以表达:
工作区整体可写
但 .git 只读
某个更深层生成目录重新可写
不同平台 Runtime 必须尽量保持同一语义。
63. Workspace Write 会保护项目元数据
默认 Workspace Write 不等于整个仓库任意写。
以下顶层元数据会受到保护:
.git
.agents
.codex
其中 .git 还要处理:
普通目录
Worktree 指针文件
Submodule gitdir
Bare Repository
如果确实要修改这些路径,需要显式更窄的 Write Entry 或专用受控流程。
64. ReadDenyMatcher 对非法 Glob Fail Closed
运行时 Deny Matcher 遇到非法 Pattern 时:
invalid_pattern = true
随后:
is_read_denied(...) = true
也就是:
配置拼写错误不会静默放宽读取权限
而用于 Sandbox 启动前展开路径的调用者可以选择返回显式错误。
65. Additional Permissions 只合并到 Restricted Policy
effective_file_system_sandbox_policy 对:
FileSystemSandboxKind::Restricted
追加 Permission Entry。
对:
Unrestricted
ExternalSandbox
保留原 Policy。
因为已经无限制的文件系统无需再“扩大”,External 的强制执行也不归本地 Overlay 所有。
66. 网络权限是 Restricted 或 Enabled
pub enum NetworkSandboxPolicy {
Restricted,
Enabled,
}
Additional Permission 中:
network.enabled = true
可以把本次命令的有效网络策略扩大为 Enabled。
但如果 Turn 启用了 Managed Network Proxy,命令仍可能经过:
Domain Policy
Inline Network Approval
Execution Attribution
Enabled 不等于绕过组织级 Managed Network。
67. SandboxManager 先判断是否需要沙箱
pub fn should_sandbox(
&self,
file_system_policy: &FileSystemSandboxPolicy,
network_policy: NetworkSandboxPolicy,
pref: SandboxablePreference,
has_managed_network_requirements: bool,
) -> bool
Auto 模式下,只要满足任一条件就需要平台沙箱:
存在 Managed Network Requirement
网络受限且不是 External Sandbox
Restricted 文件系统没有 Full Disk Write
Runtime 也可以指定:
Require
Forbid
68. 平台选择是编译目标驱动的
macOS
-> MacosSeatbelt
Linux
-> LinuxSeccomp
Windows 且 Sandbox Enabled
-> WindowsRestrictedToken
其他情况
-> None
注意:
sandbox_requested = true
与:
selected SandboxType != None
不是同一事实。
某个平台可能需要沙箱,但当前 Host 没有可用实现。
69. Sandbox Transform 延迟到执行边界
编排层尽量保留:
PathUri
PermissionProfile
SandboxType
Network Context
只有真正启动本机进程时才转换为:
Native Path
Concrete argv
Environment Variables
Platform Wrapper
这对 Remote Exec Server 很重要,因为远端路径不能由本机提前错误解析。
70. macOS 使用 sandbox-exec 与动态 SBPL
平台 Wrapper 是:
/usr/bin/sandbox-exec
-p <generated-policy>
-DKEY=VALUE
--
<original-command>
Codex 动态拼接:
Base Policy
Read Policy
Write Policy
Deny Read Policy
Network Policy
Platform Default Read Roots
而不是为每种权限组合维护一份静态模板。
71. Seatbelt Base Policy 从 deny default 开始
基础策略第一条是:
(deny default)
随后只开放运行所需能力,例如:
process-exec
process-fork
same-sandbox signal
部分 sysctl
PTY
必要的 Mach Service
受限 IPC
文件读写和网络访问由后续动态段补充。
72. Seatbelt 文件权限分别生成 Read 与 Write Rule
如果 Full Disk Read:
(allow file-read*)
否则根据 Readable Root 生成:
subpath + 参数化 Root
Write 同理。
Writable Root 下的:
Read-Only Subpath
Protected Metadata Name
会作为排除项重新收紧。
73. Seatbelt Denied Read 同时限制探测式写操作
Unreadable Glob 会转换为锚定正则,并生成:
deny file-read*
deny file-write-unlink
原因是只拒绝读取还不够。
如果仍允许 Unlink,进程可能通过删除结果、错误差异或存在性行为探测被保护路径。
74. Seatbelt 网络策略可限制到本地代理端口
Managed Network 激活时,动态策略可只开放:
Loopback Proxy Port
必要 DNS
受控 Unix Socket
可选 Local Bind
如果 Managed Network 被要求,但没有可用 Proxy Endpoint:
Fail Closed
不会自动退化成 Full Network。
75. Linux 的 SandboxType 名称不等于只有 Seccomp
枚举名是:
LinuxSeccomp
但当前默认实现组合了:
Bubblewrap
Mount Namespace
User Namespace
PID Namespace
可选 Network Namespace
Seccomp
PR_SET_NO_NEW_PRIVS
Filesystem 默认由 Bubblewrap 实现,Seccomp 主要负责网络与危险 Syscall 限制。
76. Linux 通过 codex-linux-sandbox 自调用
Core 会把原命令包装为:
codex-linux-sandbox
--sandbox-policy-cwd <cwd>
--command-cwd <cwd>
--permission-profile <json>
[--use-legacy-landlock]
[--allow-network-for-proxy]
--
<command>
Helper 解析 Permission Profile 后再构建具体隔离层。
这样 Core 不需要直接拼接所有 Bubblewrap 参数。
77. Linux 默认是两阶段启动
默认路径:
Outer Helper
-> 构造 Bubblewrap Filesystem View
-> 进入 Namespace
-> 再次进入 Inner Helper
-> 应用 no_new_privs + Seccomp
-> exec 最终命令
顺序很重要。
如果先应用:
PR_SET_NO_NEW_PRIVS
某些依赖 Setuid 的 Bubblewrap 部署可能无法创建 Namespace。
78. Bubblewrap 默认把文件系统设为只读
核心思路是:
Read Baseline
-> --ro-bind
Writable Root
-> --bind
Read-Only Carveout
-> 再次 --ro-bind
Unreadable Path
-> Mask
Mount 顺序必须保证更窄的限制最后生效。
79. Restricted Read 可以从空文件系统开始
如果策略没有读取整个 /:
--tmpfs /
然后只挂载:
Explicit Readable Roots
Platform Minimal Roots
Writable Roots
必要 Device
这与“整盘只读,再限制写入”不同。
它可以真正表达:
看不到未授权路径
80. Linux 网络有三种 Namespace 模式
FullAccess
Isolated
ProxyOnly
| 模式 | Network Namespace | 典型用途 |
|---|---|---|
FullAccess |
保留 Host Network | Network Enabled 且无 Managed Proxy |
Isolated |
--unshare-net |
完全限制外部网络 |
ProxyOnly |
--unshare-net + Bridge |
只允许走 Managed Proxy |
ProxyOnly 还会在 Inner Stage 激活代理路由。
81. Seccomp 不只限制网络
Linux Filter 还会拒绝:
ptrace
process_vm_readv
process_vm_writev
io_uring_setup
io_uring_enter
io_uring_register
Restricted Network 下还会限制:
connect
bind
listen
accept
sendto
部分 socket family
命中规则时返回:
EPERM
82. AF_UNIX 与 Proxy Routed 使用不同规则
Restricted 模式保留有限:
AF_UNIX
以支持本地进程 IPC。
Proxy Routed 模式允许:
AF_INET
AF_INET6
在隔离 Network Namespace 内访问本地 Bridge,同时限制其他 Socket Family。
因此 Network Sandbox 不是简单地把 socket() 全部禁掉。
83. Legacy Landlock 只支持较窄能力
Legacy 路径使用:
Landlock Filesystem
Seccomp Network
但源码明确拒绝:
Restricted Read-Only Access
因为 Legacy Landlock 路径主要表达:
全盘可读
有限 Root 可写
复杂 Restricted Read 需要 Bubblewrap Filesystem View。
84. WSL1 无法承载需要 Bubblewrap 的策略
以下情况需要 Bubblewrap:
ProxyOnly Network
非 Legacy Landlock 且文件系统不是 Full Disk Write
如果检测到 WSL1:
Wsl1UnsupportedForBubblewrap
Codex 会返回明确错误,而不是静默无沙箱执行。
85. Windows 有 Restricted Token 和 Elevated Backend
外层 SandboxType 仍叫:
WindowsRestrictedToken
但内部有两套 Backend:
Legacy Restricted Token
Elevated Logon-User Sandbox
选择规则是:
WindowsSandboxLevel::Elevated
或 Managed Proxy 强制执行
-> Elevated
否则
-> Legacy
86. Legacy Windows Backend 使用受限 Token 与 Capability SID
Token 创建基于:
CreateRestrictedToken
DISABLE_MAX_PRIVILEGE
LUA_TOKEN
WRITE_RESTRICTED
Capability SID
最终通过:
CreateProcessAsUserW
启动进程。
Workspace Write 由受限 Token 与目录 ACL 共同表达。
87. Legacy Backend 不能强制复杂 Denied Read
WRITE_RESTRICTED Token 的 Restricting SID 主要参与写权限检查。
因此 Legacy Backend 会拒绝:
Restricted Read Root
Denied Read Override
无法表达的 Split Writable Root
Read-Only Carveout 下重新开放的 Writable Descendant
它不会因为当前 Backend 表达不了就退化为 Unsandboxed。
错误信息明确包含:
refusing to run unsandboxed
88. Elevated Windows Backend 支持更完整权限
Elevated Backend 使用专用 Sandbox Account 和 Command Runner IPC。
它可以接收:
Read Roots Override
Write Roots Override
Deny Read Paths
Deny Write Paths
Proxy Enforcement
TTY / Pipe
Private Desktop
Managed Proxy 需要基于 Sandbox Identity 的 WFP 规则,因此即使配置为默认
Restricted Token,网络代理模式也会选择 Elevated Backend。
89. Windows Denied Read 使用 ACL 并处理 Reparse Point
Denied Read 计划会同时保留:
Lexical Path
已存在路径的 Canonical Target
避免通过 Reparse Point 的另一种拼写读取同一对象。
对于尚不存在的 Denied Path,Elevated Setup 会先创建路径再应用 Deny ACE,防止命令:
先创建被拒绝路径
再读取其中内容
90. Windows ACL 修改失败会回滚本次变更
apply_deny_read_acls 记录本次已添加的 ACE。
任一路径失败时:
撤销本次已经添加的 ACE
返回错误
避免一次失败的 One-Shot Sandbox 在主机文件系统留下部分 ACL 状态。
91. Windows 还使用 WFP、Job Object 与 Private Desktop
完整 Windows 隔离不只有 Token:
WFP
-> 按 Sandbox Account 限制网络
Job Object
-> KILL_ON_JOB_CLOSE
Private Desktop
-> 可选隔离 GUI/Desktop 交互
ConPTY / Pipe
-> 统一终端 I/O
这也是为什么 Windows Sandbox 不能简单类比成 Unix 文件权限。
92. 三平台能力对照
| 能力 | macOS | Linux 默认 | Linux Legacy | Windows Legacy | Windows Elevated |
|---|---|---|---|---|---|
| 文件系统基线 | Seatbelt SBPL | Bubblewrap Mount | Landlock | Token + ACL | Sandbox User + ACL |
| 限制写入 Root | 是 | 是 | 是 | 是 | 是 |
| Restricted Read | 是 | 是 | 否 | 否 | 是 |
| Denied Read Glob | 动态正则 | 预展开并 Mask | 否 | 否 | 以解析后的路径/ACL 实现 |
| 网络完全隔离 | Seatbelt | Net Namespace + Seccomp | Seccomp | 有限 | WFP |
| Managed Proxy Only | Loopback/Socket Rule | NetNS Bridge | 否 | 否 | WFP + Proxy |
| PTY | Host PTY + Seatbelt | Host PTY + Namespace | Host PTY | ConPTY | Runner + ConPTY |
| 失败时静默无沙箱 | 否 | 否 | 否 | 否 | 否 |
“支持”仍受具体 Policy Shape、Host 能力和 Feature 配置约束。
93. SandboxAttempt 保存一次执行的完整边界
SandboxAttempt 包含:
SandboxType
sandbox_requested
PermissionProfile
Exec Server PermissionProfile
Managed Network Flag
Sandbox CWD
Workspace Roots
Linux Helper Path
Legacy Landlock Flag
Windows Sandbox Level
Network Denial Cancellation Token
Network Proxy
首次执行与第二次执行会构造两个不同 Attempt。
Runtime 不应自行猜测当前是不是升级重试,只读取 Attempt 中的实际边界。
94. ShellRuntime 把 Attempt 转成 ExecRequest
Shell Runtime 的末端调用链是:
build_sandbox_command
-> SandboxAttempt::env_for
-> SandboxManager::transform
-> ExecRequest
-> execute_env
在此之前还会:
恢复 Shell Snapshot
应用 Package PATH Prepend
处理 PowerShell UTF-8
禁用 Elevated Windows PowerShell Profile
合并 Network Cancellation
审批链与 Shell 兼容性逻辑保持分离。
95. execute_env 同时处理超时与取消
pub enum ExecExpiration {
Timeout(Duration),
DefaultTimeout,
Cancellation(CancellationToken),
TimeoutOrCancellation {
timeout: Duration,
cancellation: CancellationToken,
},
}
默认 Shell Timeout 是:
10 秒
网络拒绝也会通过 Cancellation Token 合入 Expiration。
这使运行中的命令可以在网络策略拒绝后及时终止。
96. 进程输出有硬上限
Legacy Exec 对:
stdout
stderr
aggregated output
设置保留字节上限。
实时 Delta 事件也有数量上限:
MAX_EXEC_OUTPUT_DELTAS_PER_CALL = 10000
此外,子进程退出后读取 Pipe 有 Drain Timeout,避免孙进程继承 FD 导致 Agent 永久等待。
这些限制既是稳定性保护,也是内存保护。
97. Timeout、Signal 与 Denied 是不同错误
finalize_exec_result 按顺序分类:
Signal
Timeout
Sandbox Denied
Normal Output
因此:
命令超时
不会因为输出中包含 permission denied 就被误当成可升级的 Sandbox Denial。
只有正常完成错误分类后,才进入 Denial Heuristic。
98. Sandbox Denial 使用保守启发式
拒绝识别要求:
SandboxType != None
Exit Code != 0
然后搜索:
operation not permitted
permission denied
read-only file system
seccomp
sandbox
landlock
failed to write file
Linux 还识别:
128 + SIGSYS
99. Exit Code 2、126、127 不会单独触发升级
如果没有 Sandbox Keyword,以下 Quick Reject Code:
2
126
127
不会被猜测为 Sandbox Denial。
它们常见于:
参数错误
命令不可执行
命令不存在
对这类错误自动解除沙箱既无帮助,也会扩大风险。
100. Sandbox Denial 只是重试候选
首次执行返回:
SandboxErr::Denied
后,还要依次检查:
Network Decision 是否可归因
Runtime 是否允许 escalate_on_failure
Approval Policy 是否允许无沙箱审批
Denied Read 是否允许无沙箱
是否存在可审批的 Network Context
任一关键条件不满足,就把原始 Denial 返回模型。
101. OnRequest 不会对普通文件系统拒绝自动升级
Shell Runtime 使用默认:
wants_no_sandbox_approval(OnRequest) = false
所以普通:
写工作区外目录失败
不会在 Sandbox Denial 后自动弹出“无沙箱重试”。
Agent 应主动重新发起一个带明确:
RequireEscalated
WithAdditionalPermissions
justification
的请求。
这比失败后隐式扩大权限更可审计。
102. OnRequest 对 Network Context 有窄例外
如果 Denial 携带可解析的:
NetworkApprovalContext
且默认审批需求允许 Prompt,OnRequest 可以进入网络审批。
这不是一般无沙箱升级,而是:
针对具体 Host/Protocol 的网络策略裁决
授权范围比解除整个文件系统沙箱窄得多。
103. Strict Auto Review 的首次批准不覆盖无沙箱重试
Strict Review 对首次 Attempt 的批准只覆盖:
当前 Sandbox 边界
如果首次执行被拒绝,准备无沙箱重试时:
必须新建 Guardian Review
即使 Tool Call ID 没变,也不能把第一次批准解释成对更大权限的批准。
104. 已经审批过的命令可能跳过重复询问
非 Strict 模式下,如果:
already_approved = true
且 Denial 不是 Network Context,Runtime 可以跳过第二次 Approval Prompt。
这是因为首次审批可能本来就针对:
RequireEscalated
或明确的命令执行请求。
但:
Network Context
Strict Auto Review
仍要求新的窄范围裁决。
105. Denied Read 下第二次执行仍保持沙箱
如果:
unsandboxed_execution_allowed = false
但存在可解决的 Network Context,Retry 可以继续。
此时:
retry_sandbox_requested = true
retry_sandbox = 平台沙箱
也就是只改变网络策略,不丢弃 Denied Read。
这是“受控升级”而不是“全面解除”。
106. ToolOrchestrator 最多进行两次 Attempt
当前结构是:
Initial Attempt
Optional Retry Attempt
第二次失败不会继续无限升级。
这样可以避免:
审批循环
权限不断扩大
重复副作用
不可预测重试
对有副作用的 Tool,固定最多两次尤其重要。
107. Apply Patch 需要保留部分成功 Delta
Patch Runtime 内部保存:
committed_delta: AppliedPatchDelta
如果 Patch 部分文件已经应用,随后某个文件因 Sandbox Denial 失败:
已提交 Delta 不能丢失
第二次 Attempt 必须知道前一次已经改变了哪些文件,避免 UI、History 和 Diff Tracker
错误地认为 Patch 全部未发生。
108. Network Approval 先注册执行归属
begin_network_approval 为每次 Attempt 创建:
registration_id
environment_id
turn_id
command
trigger
cancellation_token
在 Linux Sandbox 下,还创建 Execution-Scoped Proxy:
execution_id
attribution_token
这样并行命令的网络请求可以归因到正确 Tool Call。
109. Host Approval Key 不只包含 Host
Environment ID
Lowercase Host
Protocol
Port
共同构成:
HostApprovalKey
因此批准:
https://api.example.com:443
不会自动批准:
http://api.example.com:80
也不会跨 Environment 复用。
110. 并发相同 Host 请求会合并等待
pending_host_approvals 保存:
HostApprovalKey
-> PendingHostApproval
第一个请求成为 Owner 并发起审批。
后续相同 Key:
等待同一个 Notify
复用同一个 Decision
避免并行 Tool Call 为同一 Host 弹出多个审批。
111. Network Approval 也有 Session Cache
服务保存:
session_approved_hosts
session_denied_hosts
ApprovedForSession 会加入 Approved Set。
持久化 Deny Network Policy Amendment 会加入 Denied Set。
两个 Set 更新时会互相移除相同 Key,避免同一 Host 同时处于 Allow 和 Deny。
112. 网络审批只在 Managed Profile 中开放
Inline Network Approval 要求:
PermissionProfile::Managed
AskForApproval != Never
存在 Active Turn Context
请求可归因到 Environment
不满足时直接:
Deny
External 或 Disabled Profile 不会借助该流程假装由 Codex 管理其网络边界。
113. 网络审批同样支持 Hook、Guardian 和用户
网络 Miss 的顺序是:
Session Cache
PermissionRequest Hook
Guardian
User Approval
Policy Amendment Persistence
Hook Payload 会使用类似:
network-access https://host:port
的描述。
网络规则可以持久化为:
network_rule(
host = "api.example.com",
protocol = "https",
decision = "allow",
)
114. Shell 使用 Immediate Network Approval
ShellRuntime 返回:
NetworkApprovalMode::Immediate
原因是 Legacy Shell Tool Call 等待命令执行结束。
Runtime 返回后可以立即:
finish_call
检查网络拒绝结果
注销 Registration
Tool Call 生命周期与进程生命周期基本一致。
115. Unified Exec 使用 Deferred Network Approval
UnifiedExecRuntime 返回:
NetworkApprovalMode::Deferred
因为:
exec_command Tool Call 已返回
后台进程仍可能继续联网
Network Registration 必须跟随 Process Entry,而不是跟随首次 Tool Call Future。
116. 网络拒绝会取消后台进程
Deferred Approval 保存:
registration_id
cancellation_token
finish_outcome
execution_proxy
Network Service 记录 Denied Outcome 时:
写入 call_outcomes
取消 Cancellation Token
Process Manager 监听该 Token,并终止相关进程。
117. 进程退出后还等待 100ms 的迟到拒绝
后台进程退出与 Proxy 上报 Blocked Request 可能存在竞态。
Process Manager 会等待:
LATE_NETWORK_DENIAL_GRACE_PERIOD = 100ms
再完成 Deferred Approval。
这样可以捕获:
进程刚退出
Network Denial 事件稍后到达
的情况。
118. Unified Exec Runtime 最终返回 UnifiedExecProcess
安全链执行成功后,不是立刻返回完整文本,而是返回:
UnifiedExecProcess
Process Manager 再负责:
启动 Streaming Output
等待 Initial Yield
决定是否存入 Process Store
构造 ExecCommandToolOutput
这让审批/沙箱逻辑与会话 I/O 逻辑保持分层。
119. 本地 Unified Exec 支持 PTY 与 Pipe
本地启动分为:
tty = true
-> spawn_process_with_inherited_fds
tty = false
-> spawn_process_no_stdin_with_inherited_fds
非 TTY 进程默认没有可写 Stdin。
write_stdin 对非 TTY 只允许特殊:
Ctrl-C
其他输入返回:
StdinClosed
120. Remote Environment 走 Exec Server
如果:
environment.is_remote()
Process Manager 调用:
environment
.get_exec_backend()
.start(exec_server_params)
Remote Exec Server 接收:
argv
PathUri CWD
Env Policy
TTY
Sandbox Context
Managed Network Context
而不是让本机先把远端命令包装成本机 Seatbelt 或 Bubblewrap。
121. 初始等待前就把活进程放入 Store
exec_command 启动进程后,如果进程仍存活,会在等待 Yield Time 前调用:
store_process
原因是 Turn 可能在等待期间被 Interrupt。
如果 Process Store 尚未持有 Arc:
最后一个 Arc 被释放
后台进程对象终止
就会破坏“Turn 中断不自动杀死后台终端”的协议语义。
122. Yield Time 不是进程 Timeout
yield_time_ms
只表示:
本次 Tool Call 最多等待多久收集初始输出
到期后:
进程仍存活
-> 返回 session_id
它不等于:
终止后台进程
真正的进程 Expiration 由 Runtime 的 ExecExpiration 控制。
123. write_stdin 同时承担写入和轮询
参数是:
session_id
chars
yield_time_ms
max_output_tokens
语义是:
chars 非空
-> 写入 PTY 或发送 Ctrl-C
chars 为空
-> 只轮询后续输出
这避免为:
poll_process
send_input
interrupt_process
分别增加多个 Tool。
124. write_stdin 不重复执行 PreToolUse
write_stdin 只是已有命令的 Transport。
原始 exec_command 已经执行过:
Bash PreToolUse
所以 write_stdin 的:
pre_tool_use_payload = None
当轮询观察到原进程最终完成时,它可以发出匹配原命令的 PostToolUse。
125. Empty Poll 只有进程仍存活时才发交互事件
空 chars 不是实际终端输入。
因此:
空 Poll + 进程已退出
-> 不发 TerminalInteraction
空 Poll + 进程仍存活
-> 发等待状态
非空输入
-> 即使导致进程退出也发交互事件
这样 UI 不会把普通 Poll 误展示成用户输入。
126. Process Store 有上限和 LRU 淘汰
当后台进程达到上限时,Manager 优先淘汰:
已退出且非保护状态的最旧进程
否则选择:
非保护状态的最旧进程
淘汰后会:
注销 Network Approval
终止 Process
异步清理发生在释放 Store Lock 之后,避免持锁执行外部等待。
127. Turn Interrupt 与 CleanBackgroundTerminals 不同
协议明确区分:
Interrupt
-> 终止当前 Turn
-> 不自动终止后台终端
CleanBackgroundTerminals
-> 主动终止该 Thread 的全部后台进程
这就是 Process Store 必须独立于当前 Turn Future 存活的原因。
128. 命令安全决策流程图
Tool Call
|
v
Parse + Resolve Environment/CWD
|
v
Merge Sticky + Inline Permissions
|
v
Intercept apply_patch?
| yes -> ApplyPatchRuntime
| no
v
Parse command segments
|
v
Exact Rule / Host Executable Rule?
| yes -> strictest Decision
| no -> safe/dangerous heuristics
v
ExecApprovalRequirement
| Forbidden -> return rejection
| NeedsApproval
| -> Hook -> Guardian/User
| Skip
v
Can first attempt bypass sandbox?
| explicit allow / require escalated
| denied-read forces sandbox
v
Select platform sandbox
|
v
Register network approval
|
v
Run tool
| success -> return output/session
| normal error -> return error
| SandboxErr::Denied
v
Check escalation + policy + denied-read
| disallowed -> return original denial
| allowed -> optional fresh approval
v
Second attempt
|
v
Return final result
129. Managed Restricted 下的审批决策矩阵
下表假设:
没有预先命中的 Session Approval Cache
PermissionProfile = Managed Restricted
命令未被 Sticky Permission 预批准
| 命令类别 | Never | OnRequest | UnlessTrusted | Granular |
|---|---|---|---|---|
| Simple Known Safe | Allow + Sandbox | Allow + Sandbox | Allow + Sandbox | Allow + Sandbox |
| 普通未知命令 | Allow + Sandbox | Allow + Sandbox | Prompt | Allow + Sandbox |
| Dangerous Command | Forbidden | Prompt | Prompt | Prompt 或 Forbidden |
| Explicit Allow Rule | Allow,可尝试 Bypass | Allow,可尝试 Bypass | Allow,可尝试 Bypass | Allow,可尝试 Bypass |
| Explicit Prompt Rule | Forbidden | Prompt | Prompt | 由 rules 决定 |
| Explicit Forbidden Rule | Forbidden | Forbidden | Forbidden | Forbidden |
| RequireEscalated | Handler 拒绝主动请求 | Prompt | Handler 拒绝主动请求 | Handler 拒绝主动请求 |
| WithAdditionalPermissions | 不允许新请求 | Prompt | Handler 拒绝主动请求 | 当前 Handler 仅允许预批准范围 |
Bypass 仍受:
Denied Read
平台 Sandbox 能力
Managed Network
约束。
130. 常见 Permission Profile 行为矩阵
| 场景 | Read Only | Workspace Write | Danger Full Access | External |
|---|---|---|---|---|
| 读取允许 Root | 允许 | 允许 | 允许 | 外部负责 |
| 写工作区 | 拒绝 | 允许,但保护元数据 | 允许 | 外部负责 |
| 写工作区外 | 拒绝 | 拒绝 | 允许 | 外部负责 |
| 读取 Denied Path | 拒绝 | 拒绝 | 允许,不携带 Deny Entry | 外部负责 |
| 默认网络 | Restricted | Restricted | Enabled | Profile 指定 |
| Codex 构造 FS Sandbox | 是 | 是 | 通常否 | 否 |
实际 Root 来自最终物化后的 Permission Entry,不能只凭 Profile 名称判断。
131. 四类实践命令应该验证什么
只读命令
pwd
rg --files
git status --short
验证:
UnlessTrusted 下是否命中 Safelist
是否仍在平台沙箱内
是否产生 Approval Event
工作区写入
printf 'ok\n' > .codex-sandbox-test
验证:
Workspace Write 成功
Tool Event 正常结束
没有无沙箱重试
工作区外写入
printf 'blocked\n' > /var/tmp/codex-outside-test
验证:
首次执行被平台沙箱拒绝
OnRequest 不自动升级普通文件系统 Denial
模型收到原始拒绝输出
网络访问
curl -I https://example.com
验证:
Network Policy
Proxy Attribution
Host/Protocol/Port Approval Key
Immediate 或 Deferred 生命周期
132. 不要用破坏性命令验证危险检测
危险命令识别可以通过单元测试或 Exec Policy Check 验证:
cargo test -p codex-shell-command \
command_safety
不要在真实工作区执行:
rm -rf
git reset --hard
来观察是否弹出审批。
安全系统测试本身也要遵循最小副作用原则。
133. 使用 execpolicy check 验证规则
创建临时 Rule 文件后,可以运行:
cargo run -p codex-execpolicy -- \
check \
--rules /tmp/tutorial.rules \
--pretty \
git status --short
关注输出:
matchedRules
matchedPrefix
decision
resolvedProgram
justification
对绝对可执行路径测试时增加:
--resolve-host-executables
134. Exec Policy 测试重点
建议覆盖:
显式 Allow
显式 Prompt
显式 Forbidden
多个命令段取最严格
Absolute Executable Fallback
Complex Shell 不推导 Amendment
Banned Prefix 不生成 Amendment
Granular Rules Disabled
Never + Prompt Rule
对应测试模块:
cargo test -p codex-core \
exec_policy::tests
135. Orchestrator 测试重点
建议覆盖:
首次 Sandbox Success
Normal Failure 不升级
Sandbox Denied 且策略禁止升级
Denied Read 保持 Sandbox
Strict Auto Review 二次 Review
Network Context 窄范围重试
ApprovedForSession Cache
PermissionRequest Hook Allow/Deny
相关单元测试分布在:
tools/sandboxing_tests.rs
tools/orchestrator.rs 的测试依赖
session/tests/guardian_tests.rs
136. 平台测试要按 Host 选择
macOS:
cargo test -p codex-sandboxing \
seatbelt
Linux:
cargo test -p codex-linux-sandbox
Windows:
cargo test -p codex-windows-sandbox
跨平台 CI 应分别验证真实平台 Runtime。
仅在 macOS 上检查生成的 Windows argv,不能替代 ACL、Token 和 WFP 集成测试。
137. Unified Exec 测试重点
cargo test -p codex-core \
unified_exec
重点断言:
Process ID 分配与释放
Initial Yield 前存入 Store
后台输出轮询
PTY 输入
Pipe Ctrl-C
Process Exit Event
Network Denial Cancellation
100ms Late Denial Grace
LRU Prune Cleanup
138. 常见误区一:Approved 就是 Full Access
错误理解:
用户点了批准
-> 命令无沙箱执行
真实情况:
用户批准命令
-> Orchestrator 继续
-> Sandbox Override 再独立计算
-> Permission Profile 仍然生效
尤其是:
普通 NeedsApproval
通常仍在沙箱内执行。
139. 常见误区二:OnRequest 会在任何失败后询问
真实实现只对:
主动 Sandbox Override
危险命令
显式 Prompt Rule
窄 Network Context
进入审批。
普通文件系统 Sandbox Denial 会直接回给模型,让 Agent 重新提出更明确、范围更小的权限请求。
140. 常见误区三:Linux 沙箱就是 Landlock
当前默认是:
Bubblewrap Filesystem
Seccomp Network/Syscall
Landlock 是:
Legacy/Backup Path
且不支持完整 Restricted Read。
阅读枚举名 LinuxSeccomp 或旧文件名时,要继续跟踪 linux_run_main 的真实分支。
141. 常见误区四:Windows Restricted Token 能表达所有权限
Legacy Restricted Token 对写隔离有效,但不能可靠表达所有:
Deny Read
Restricted Read Root
复杂 Split Policy
Managed Proxy
这些能力需要 Elevated Backend。
无法表达时源码选择拒绝启动,而不是扩大权限。
142. 常见误区五:Network Enabled 等于直连互联网
Managed Network 存在时:
Enabled
仍可能只表示:
允许通过受控 Proxy 发起网络
Domain Allowlist、Inline Approval、WFP、Seatbelt Loopback Rule 或 Linux Proxy Bridge
仍可能限制目标。
143. 练习一:建立 Exec Policy 决策矩阵
为以下命令分别设置:
Allow
Prompt
Forbidden
No Match
命令:
ls
git status
python -c ...
rm -rf /tmp/example
在四种 Approval Policy 下记录:
ExecApprovalRequirement
bypass_sandbox
是否出现审批
不要实际执行破坏性命令,只测试 Policy Evaluation。
144. 练习二:验证显式 Allow 与启发式 Allow
先不配置规则运行:
git status
再配置:
prefix_rule(
pattern = ["git", "status"],
decision = "allow",
)
断言两者都跳过审批,但:
Heuristic Allow
-> bypass_sandbox = false
Explicit Allow
-> bypass_sandbox = true
145. 练习三:验证 Denied Read 不可被 Escalation 丢弃
构造 Permission Profile:
:root = write
secret.env = deny
再请求:
RequireEscalated
断言:
SandboxOverride = NoOverride
Launch SandboxPermissions = UseDefault
secret.env 仍不可读
146. 练习四:验证 Approval Cache 精度
批准:
Environment A
CWD /repo
Command cargo test
TTY false
UseDefault
并选择:
ApprovedForSession
随后逐项改变:
Environment
CWD
Command
TTY
Additional Permissions
确认任一关键字段改变都不会命中旧缓存。
147. 练习五:验证 Network Host Key
依次请求:
https://example.com:443
http://example.com:80
https://example.com:8443
批准第一个为 Session Scope。
确认后两个请求仍需要独立决定。
再在第二个 Environment 中访问第一个目标,确认 Environment Scope 也生效。
148. 练习六:验证 Unified Exec 后台生命周期
启动一个短期后台任务:
for i in 1 2 3; do
echo "$i"
sleep 1
done
设置较短:
yield_time_ms
确认:
exec_command 返回 session_id
write_stdin 空输入获取后续输出
最终响应不再返回 session_id
Process Store 清理完成
149. 练习七:验证 Immediate 与 Deferred Network Approval
分别通过:
shell_command
exec_command
启动同一网络请求。
确认:
Shell Runtime 返回后 Registration 已完成
Unified Exec 后台存活时 Registration 仍存在
进程退出后等待 Late Denial Grace
150. 推荐测试命令
Exec Policy:
cargo test -p codex-core \
exec_policy::tests
权限与编排:
cargo test -p codex-core \
tools::sandboxing
Network Approval:
cargo test -p codex-core \
network_approval
Unified Exec:
cargo test -p codex-core \
unified_exec
Shell Command Safety:
cargo test -p codex-shell-command
如果仓库使用 Nextest:
cargo nextest run -p codex-core \
-E 'test(/exec_policy|approval|sandbox|unified_exec/)'
151. 本篇小结
Codex 的命令执行不是:
模型输出字符串
-> std::process::Command
而是一条分层的安全状态机。
核心结论如下:
shell_command面向一次性 Shell,Unified Exec 面向可持续终端会话。- 两者都在执行前合并 Environment、CWD、Sticky Permission 和 Inline Permission。
- Shell 中的
apply_patch会被拦截并进入结构化 Patch Runtime。 - Approval Policy 决定是否允许询问,Exec Policy 决定具体命令结果。
- Permission Profile 定义能力边界,Sandbox Runtime 负责平台强制执行。
- Exec Policy 会解析
bash -lc或 PowerShell Body,不能只检查外层 Shell。 - 多个命令段和多个 Rule Match 都取最严格 Decision。
- Explicit Rule 优先于 Safe/Dangerous Heuristic。
UnlessTrusted只自动批准窄 Safe Command 集合。OnRequest对普通命令默认依赖沙箱,而不是逐条询问。Never表示不询问,不表示忽略 Forbidden 或 Managed Sandbox。Granular将 Rule Prompt 与 Sandbox Prompt 分别控制。- Heuristic Allow 跳过审批但保留沙箱。
- 只有所有命令段被显式 Allow Rule 覆盖,才可能首次绕过沙箱。
- Prefix Rule Amendment 经过宽泛前缀黑名单和全命令覆盖验证。
- PermissionRequest Hook、Guardian 和用户审批形成分层裁决。
ApprovedForSession只缓存精确 Approval Key。- Environment、CWD、TTY 和 Permission 变化都会使旧 Approval Key 失效。
WithAdditionalPermissions在沙箱内做最小增量授权。RequireEscalated请求无沙箱执行,但不能覆盖 Denied Read。- Workspace Write 默认保护
.git、.agents和.codex。 - macOS 使用动态 Seatbelt SBPL,从
deny default开始开放能力。 - Linux 默认使用 Bubblewrap Filesystem + Seccomp,而不是单独依赖 Landlock。
- Linux ProxyOnly 使用隔离 Network Namespace 与受控 Bridge。
- Windows Legacy Backend 使用 Restricted Token 与 ACL,但不支持完整 Restricted Read。
- Windows Elevated Backend通过 Sandbox Account、ACL、WFP 和 Runner IPC 扩展能力。
- 无法表达权限时,各平台实现选择拒绝,而不是静默无沙箱运行。
- Sandbox Denial 依赖错误类型、Exit Code 和输出关键字共同识别。
- Normal Failure、Timeout 和 Signal 不会自动进入权限升级。
- 第二次执行需要同时满足 Runtime、Approval Policy 和 Permission Profile 条件。
- Strict Auto Review 对无沙箱 Retry 必须重新 Review。
- Shell Network Approval 是 Immediate,Unified Exec 是 Deferred。
- Network Approval Key 精确到 Environment、Host、Protocol 和 Port。
- Unified Exec 在 Initial Yield 前就把活进程存入 Process Store。
yield_time_ms只控制本次等待,不等于终止进程。write_stdin同时承担 PTY 输入、Ctrl-C 和后台轮询。- Turn Interrupt 不自动终止后台终端,显式清理使用
CleanBackgroundTerminals。 - 命令审批、沙箱权限和网络审批共同决定真实副作用边界。
下一篇将继续分析 Codex 的扩展系统,比较 MCP、Skills、Plugins 与 Hooks 分别在什么
阶段加载,如何进入模型上下文,以及它们如何跨越配置、工具和生命周期边界。
更多推荐

所有评论(0)