上一篇从 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

之中。

本篇目标

阅读完成后,你应该能够:

  1. 跟踪 shell_commandexec_command 的完整执行链。
  2. 区分 Approval Policy、Exec Policy、Permission Profile 和 Sandbox Policy。
  3. 解释 NeverOnRequestUnlessTrustedGranular 的真实语义。
  4. 说明显式规则、启发式判断与危险命令检测的优先级。
  5. 理解为什么审批通过不等于解除沙箱。
  6. 解释 ApprovedForSession 的精确缓存范围。
  7. 说明 Additional Permissions 如何只扩大单次命令的沙箱权限。
  8. 跟踪 macOS Seatbelt、Linux Bubblewrap/Seccomp/Landlock 和 Windows Sandbox。
  9. 解释 Sandbox Denial 如何识别,以及何时允许第二次执行。
  10. 区分 Immediate 与 Deferred Network Approval。
  11. 理解 Unified Exec 的后台进程、session_idwrite_stdin
  12. 建立审批策略、命令类别与实际执行边界的决策矩阵。

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,
}

DeniedAbort 都阻止执行,但上层交互含义不同:

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

而是一条分层的安全状态机。

核心结论如下:

  1. shell_command 面向一次性 Shell,Unified Exec 面向可持续终端会话。
  2. 两者都在执行前合并 Environment、CWD、Sticky Permission 和 Inline Permission。
  3. Shell 中的 apply_patch 会被拦截并进入结构化 Patch Runtime。
  4. Approval Policy 决定是否允许询问,Exec Policy 决定具体命令结果。
  5. Permission Profile 定义能力边界,Sandbox Runtime 负责平台强制执行。
  6. Exec Policy 会解析 bash -lc 或 PowerShell Body,不能只检查外层 Shell。
  7. 多个命令段和多个 Rule Match 都取最严格 Decision。
  8. Explicit Rule 优先于 Safe/Dangerous Heuristic。
  9. UnlessTrusted 只自动批准窄 Safe Command 集合。
  10. OnRequest 对普通命令默认依赖沙箱,而不是逐条询问。
  11. Never 表示不询问,不表示忽略 Forbidden 或 Managed Sandbox。
  12. Granular 将 Rule Prompt 与 Sandbox Prompt 分别控制。
  13. Heuristic Allow 跳过审批但保留沙箱。
  14. 只有所有命令段被显式 Allow Rule 覆盖,才可能首次绕过沙箱。
  15. Prefix Rule Amendment 经过宽泛前缀黑名单和全命令覆盖验证。
  16. PermissionRequest Hook、Guardian 和用户审批形成分层裁决。
  17. ApprovedForSession 只缓存精确 Approval Key。
  18. Environment、CWD、TTY 和 Permission 变化都会使旧 Approval Key 失效。
  19. WithAdditionalPermissions 在沙箱内做最小增量授权。
  20. RequireEscalated 请求无沙箱执行,但不能覆盖 Denied Read。
  21. Workspace Write 默认保护 .git.agents.codex
  22. macOS 使用动态 Seatbelt SBPL,从 deny default 开始开放能力。
  23. Linux 默认使用 Bubblewrap Filesystem + Seccomp,而不是单独依赖 Landlock。
  24. Linux ProxyOnly 使用隔离 Network Namespace 与受控 Bridge。
  25. Windows Legacy Backend 使用 Restricted Token 与 ACL,但不支持完整 Restricted Read。
  26. Windows Elevated Backend通过 Sandbox Account、ACL、WFP 和 Runner IPC 扩展能力。
  27. 无法表达权限时,各平台实现选择拒绝,而不是静默无沙箱运行。
  28. Sandbox Denial 依赖错误类型、Exit Code 和输出关键字共同识别。
  29. Normal Failure、Timeout 和 Signal 不会自动进入权限升级。
  30. 第二次执行需要同时满足 Runtime、Approval Policy 和 Permission Profile 条件。
  31. Strict Auto Review 对无沙箱 Retry 必须重新 Review。
  32. Shell Network Approval 是 Immediate,Unified Exec 是 Deferred。
  33. Network Approval Key 精确到 Environment、Host、Protocol 和 Port。
  34. Unified Exec 在 Initial Yield 前就把活进程存入 Process Store。
  35. yield_time_ms 只控制本次等待,不等于终止进程。
  36. write_stdin 同时承担 PTY 输入、Ctrl-C 和后台轮询。
  37. Turn Interrupt 不自动终止后台终端,显式清理使用 CleanBackgroundTerminals
  38. 命令审批、沙箱权限和网络审批共同决定真实副作用边界。

下一篇将继续分析 Codex 的扩展系统,比较 MCP、Skills、Plugins 与 Hooks 分别在什么
阶段加载,如何进入模型上下文,以及它们如何跨越配置、工具和生命周期边界。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐