English 书架

第 10 章:Shell、Exec Server 与文件系统工具

阅读契约: 用本章把 shell execution 读成受治理的副作用,而不是一个裸 subprocess。请同时跟踪四个 owner:handler 负责塑形请求,exec policy 负责决定 approval requirement,orchestrator 负责选择 sandbox attempt,exec-server 负责 process 与 filesystem 的 placement。读完后,你应该能解释为什么 process_id 是逻辑协议句柄而不是 OS pid,为什么 approval 不等于 sandbox,以及为什么远端文件系统写入必须携带 sandbox context。

Shell 与文件系统执行边界:展示 tool calls、exec policy、approval、sandbox selection、exec-server process、filesystem handler 和有序输出
Shell 和文件系统访问之所以可控,是因为命令会经过 request shaping、policy、approval、sandbox selection、executor placement、ordered output 和 filesystem mediation。

源码边界: 本章只有在链接到固定 Codex commit 或本章源码地图的 files、types、functions、tests、schemas、request/event shapes 时,才把说法视为 verified source。像 runtime、owner、placement boundary、contract 这类架构归纳是从可见 anchors 得出的 surrounding contract inference,不是对 OpenAI 服务内部的断言。

第 9 章解释了 Codex 如何暴露工具,同时不让模型凭空获得工具权限。本章沿着第一个真正把这件事落到运行时的工具家族继续往下看:shell 和 filesystem access。从模型能看到的表面看,shellexec_command 很像一句“运行这个命令”。但在 runtime 里,这句话会变成一个受治理的请求,其权限被分散到 parsing、policy、approval、sandboxing、process management、output sequencing 和 filesystem mediation 中。

核心心智模型是:Codex 不是在运行“一条 shell 命令”;它是在接收一个 side-effect request,并逐步证明这个副作用可以在哪里发生。 一条命令最终可能变成本机 child process、一个保留给后续 stdin 的 PTY、一个经过 sandbox transform 的 execution request,也可能变成针对远端 executor 的 exec-server request。这些不是后端实现细节,而是在定义哪个文件系统是权威、哪些输出可以 replay、哪个 permission profile 必须随操作一起传递。

一、Shell 是受治理的请求

Codex 有多个 shell 相邻入口,是因为不同客户端需要不同的交互形状。经典 shell 工具返回一次捕获结果;exec_command 可以让进程保持存活,并在之后接受 write_stdin;还有一些 shell surface 用于兼容、host-local execution 或 shell-aware command routing。关键不是入口有多少,而是这些入口本身都不是权限边界。

1.1 Request Surface 不是权限边界

入口表面需要什么仍然必须治理什么
shell一条 command、cwd、timeout、可选 approval hintcommand classification、approval、sandbox、output shaping
exec_commandcommand、selected environment、可选 TTYprocess identity、output buffering、stdin continuation、retry
write_stdin和已有 process 交互process liveness、stdin support、output polling、exit cleanup
filesystem helpers对 selected workspace 执行 read/write/remove/copyexecutor placement 与 sandbox context

这层拆分能避免一种误导性的 API 叙事。Tool name 只说明客户端想怎样交互;它并不说明哪个 filesystem 是权威、是否需要 prompt、后续 stdin write 是否还能找到同一个 runtime object。

1.2 Handler Shaping 建立第一份 Contract

经典 shell handler 展示了第一层翻译。ShellHandler::to_exec_params 并不启动进程,而是把模型请求塑形成 ExecParams:里面有 cwd resolution、timeout policy、capture policy、environment、network、sandbox permissions 和 justification。

ExecParams {
    command: params.command.clone(),
    cwd: turn_context.resolve_path(params.workdir.clone()),
    expiration: params.timeout_ms.into(),
    capture_policy: ExecCapturePolicy::ShellTool,
    env: create_env(&turn_context.shell_environment_policy, Some(thread_id)),
    sandbox_permissions: params.sandbox_permissions.unwrap_or_default(),
    // additional fields omitted
}

这个小对象已经是一个 contract。命令还没有被信任,但 runtime 已经知道它属于哪个 working directory、environment policy、capture mode 和 sandbox request。handler 后面的 handle 会把塑形后的请求交给 run_exec_like,由共享 shell execution path 继续处理 environment 与 approval。handler 是入口,不是最终授权者。

如果跳过这一步,后续 policy code 就只能从未结构化字符串里猜 cwd、environment、network 和 sandbox intent。Codex 的做法是先把副作用变成 typed request,再讨论它能不能运行。

二、Policy 先于 Sandbox

一个常见误读是把 sandbox 当作第一道防线。Codex 源码里的顺序更细:先把 command 变得更结构化,再匹配 policy,再转换成 approval requirement,最后才由 orchestrator 选择 sandbox attempt。

命令执行梯:从 tool call 经过 parsing、exec policy、approval gate、sandbox attempt、retry 到 result record
Exec policy 先决定命令是 forbidden、needs approval 还是可以 skip approval,然后 runtime 才选择并执行 sandbox attempt。

共享 shell path 会先在 run_exec_like 中抽取 command 并应用 environment facts。真正有意义的治理发生在 command policy 层:解析或 lowering 常见 shell 形式,匹配 prefix rules,在必要时参考 host executable metadata,并在没有规则命中时回退到保守 heuristics。

2.1 Policy 生成 Approval Shape

核心转换在 ExecPolicyManager::create_exec_approval_requirement_for_command。简化后,源码把 policy decision 映射成三种 approval shape:

match evaluation.decision {
    Decision::Forbidden => ExecApprovalRequirement::Forbidden { /* reason */ },
    Decision::Prompt => ExecApprovalRequirement::NeedsApproval { /* reason */ },
    Decision::Allow => ExecApprovalRequirement::Skip { /* bypass_sandbox */ },
}

这个 shape 很重要,因为 allow 不总是“无 sandbox 自由运行”。源码会在每个 parsed command segment 都被 exec policy 显式 allow 时,才计算出 bypass_sandbox。所以一条命令可以安全到无需提示,但仍然在当前 sandbox 下执行。反过来,如果当前 approval policy 不允许对某类命令发起 prompt,一个 prompt decision 也可能变成 forbidden。

Policy resultRuntime 含义常见误读
Forbidden执行前停止并返回 policy reason。“是 sandbox 拦住了。”其实进程不一定会启动。
NeedsApproval在尝试副作用前询问 hooks、guardian 或用户。“审批通过就是全权限。”它只授权这条 operation path。
Skip这个 request shape 不需要 prompt。“所有控制都跳过了。”除非 policy 显式允许 bypass,否则 sandbox 仍可生效。

2.2 Fallback 保持保守

render_decision_for_unmatched_command 这条 fallback path 也很保守:它会区分 known-safe commands 和 dangerous commands,并把 approval policy 与 sandbox kind 一起纳入判断。所以本章必须把 command classification 和 process execution 分开讲。

这种设计的成本是:一个陌生但无害的命令也可能需要 approval。这是有意为之的边界,而不是缺陷;fallback 保护的是“未知副作用不会因为写在 shell 语法里就自动可信”这个不变量。

三、Orchestrator 拥有 Attempts

工具拿到 approval requirement 后,会进入共享的 ToolOrchestrator。Orchestrator 负责把 approval、network approval、sandbox selection、guardian review 和 retry semantics 组合起来。它不是 spawn 外面的一层薄包装。

3.1 Attempt 是 Runtime State

用形状描述,这条链路是:

tool request
  -> exec approval requirement
  -> hook / guardian / user decision when needed
  -> initial sandbox attempt
  -> runtime run
  -> optional retry without sandbox when policy permits
  -> tool output or structured denial

第一次 attempt 会根据 turn 的 permission profile 和 sandbox policy 在 orchestrator.rs 中构造。如果 sandbox 拒绝这次 attempt,后续分支会检查 tool 是否允许 escalation、当前 approval policy 是否允许 no-sandbox retry、是否存在 network-denial approval context,然后才会再次请求审批或直接返回 denial。第二次 attempt 是显式的:只有这些检查通过后,源码才会创建一个 SandboxType::None 的新 SandboxAttempt

3.2 Retry 不是静默 Fallback

这就是为什么 approval 和 sandboxing 不能混在一起:

边界源码 owner回答的问题
approval requirementexec policy 与 tool runtime这个副作用需要审批、禁止,还是可以跳过审批?
approval decisionhooks、guardian、cached approval 或 user request是否有授权决策批准这个 request?
sandbox attemptToolOrchestratorSandboxAttempt这次 attempt 应受哪些 filesystem/network 限制?
retryToolOrchestratorsandbox denial 后,是否允许 no-sandbox retry?

更底层的 sandbox transform 在 SandboxAttempt::env_for 完成。这里 sandbox manager 会用当前 sandbox type、cwd、permission profile 和 network setting 转换 ExecRequest。也就是在这里,一个受治理的请求才变成可执行环境。

这个 failure boundary 很关键:命令已经被 approval,并不表示 sandbox denial 一定会被绕过。Approval 回答 runtime 是否可以尝试副作用;sandbox attempt 回答这次尝试受到怎样的约束。

四、Unified Exec 增加 Process Identity

exec_command 相比简单 shell 工具有一个关键新增概念:逻辑 process session。命令可能输出一段后立即返回,也可能继续运行。Runtime 必须持有 session、限制 output buffers、发出 events,并允许后续 write_stdin 和同一个 process 交互。

Unified exec 生命周期:从 exec_command 经过 cwd 与 environment resolution、process id allocation、process manager、output chunks、result snapshot、write_stdin 到 alive or exited state
exec_command 创建 managed process session;write_stdin 后续通过逻辑 process id 定位这个 session,而不是定位某个 OS pid。

4.1 Handler 绑定 Environment、Cwd 与 Process Id

unified exec handler 会先解析 selected environment、cwd、executor filesystem、shell command、TTY mode、yield time、output token limits 和 permission request,再调用 process manager。源码中可见的 handoff 发生在 ExecCommandHandler::handle:它先解析 environment 并分配 process id,再构造 request。这个形状可以在 ExecCommandRequest 里看到:

pub(crate) struct ExecCommandRequest {
    pub command: Vec<String>,
    pub hook_command: String,
    pub process_id: i32,
    pub yield_time_ms: u64,
    pub max_output_tokens: Option<usize>,
    pub cwd: AbsolutePathBuf,
    pub environment: Arc<Environment>,
    pub network: Option<NetworkProxy>,
    pub tty: bool,
    pub sandbox_permissions: SandboxPermissions,
    pub additional_permissions: Option<AdditionalPermissionProfile>,
    pub additional_permissions_preapproved: bool,
    pub justification: Option<String>,
    pub prefix_rule: Option<Vec<String>>,
}

handler 会通过 UnifiedExecProcessManager::allocate_process_id 分配 process id,然后把 request 交给 exec_command。这个 manager 会通过 orchestrated sandbox path 打开 process,启动 output streaming,在 initial yield wait 之前存储 live sessions,并返回 ExecCommandToolOutput:里面有 raw output、已知时的 exit code、wall time、chunk id,以及可能仍然存活的 process_id

4.2 Manager 拥有 Continuation

所以 write_stdin 不是第二个命令启动器。WriteStdinRequest 携带 process id、input、yield time 和 output limit。manager 的 write_stdin 会为已有 process 准备 handles,只在 stdin 支持时写入 input,轮询 bounded output,刷新 process state,并清理已经退出的 sessions。

这也是 UI 必须尊重的边界。如果 tool result 返回了 process_id,说明这个 process 仍然是 managed runtime object;如果只返回 exit_code,它已经变成完成态输出。把 process_id 当作 OS pid 是错的;后面的 protocol 会把这点写得更明确。

五、exec-server 把 Placement 变成 Protocol

exec-server crate 负责把 placement 做成 protocol,而不是散落的一堆特殊分支。它的 protocol file 命名了用于 start、read、write、terminate、接收 output notifications 和执行 filesystem operations 的 JSON-RPC methods。同一套 client shape 可以面对本地 executor,也可以面对远端 executor。

Codex core 与 executor 之间的 exec-server RPC 边界:展示 process API start/read/write、output、stdin、exit、sequence chunks 和本地或远端 placement 图标
exec-server 用 JSON-RPC contract 包住 process placement:Codex core 发送 process 与 filesystem request,executor 拥有 process,output 以 sequenced chunks 返回。

5.1 Process 与 Filesystem Method 共用一层 Contract

这些 method constants 很小,但很关键。protocol.rs 把 process 和 filesystem methods 放在同一个协议面:

pub const INITIALIZE_METHOD: &str = "initialize";
pub const INITIALIZED_METHOD: &str = "initialized";
pub const EXEC_METHOD: &str = "process/start";
pub const EXEC_READ_METHOD: &str = "process/read";
pub const EXEC_WRITE_METHOD: &str = "process/write";
pub const EXEC_TERMINATE_METHOD: &str = "process/terminate";
pub const EXEC_OUTPUT_DELTA_METHOD: &str = "process/output";
pub const EXEC_EXITED_METHOD: &str = "process/exited";
pub const EXEC_CLOSED_METHOD: &str = "process/closed";
pub const FS_READ_FILE_METHOD: &str = "fs/readFile";
pub const FS_WRITE_FILE_METHOD: &str = "fs/writeFile";
pub const FS_CREATE_DIRECTORY_METHOD: &str = "fs/createDirectory";
pub const FS_GET_METADATA_METHOD: &str = "fs/getMetadata";
pub const FS_READ_DIRECTORY_METHOD: &str = "fs/readDirectory";
pub const FS_REMOVE_METHOD: &str = "fs/remove";
pub const FS_COPY_METHOD: &str = "fs/copy";
pub const HTTP_REQUEST_METHOD: &str = "http/request";
pub const HTTP_REQUEST_BODY_DELTA_METHOD: &str = "http/request/bodyDelta";

5.2 Output 是有序协议状态

process 参数也澄清了一个常见误会。ExecParams 说明 process_id 是“client-chosen logical process handle scoped to this connection/session”,并且明确不是 OS pid。Output 也不是 blob:ProcessOutputChunk 携带 seqstreamchunkReadResponse 返回 chunks、next_seq、exit state、closed state 和 failure。

client 侧也镜像了这个协议。ExecServerClientexecreadwriteterminate 和 filesystem operations 的 request methods。远端 process adapter 在 remote_process.rs 中先 register session,再发送 client.exec(params),并通过 session 实现 readwriteterminate。因此本地与远端 placement 可以共享 process semantics,而不需要 shell handler 知道 child process 实际在哪里。

六、Filesystem Operations 也经过 Executor Boundary

Filesystem access 遵循同样的 placement rule。如果 selected executor 拥有 workspace,那么 reads、writes、copies、removals 和 metadata checks 都必须经过这个 executor。否则就会出现一种危险错位:remote command 修改一个文件系统,而 patch 或 file read 却误触另一个文件系统。

Executor filesystem boundary:sandbox context 经过分组 filesystem operations,进入 permission profile 后面的 local 或 remote workspace
Executor filesystem calls 会把 sandbox context 带到真正拥有 workspace 的文件系统,因此 local 和 remote operations 共享同一个 permission boundary。

6.1 Server Handler 应用 Sandbox Context

server-side handler 把可用操作列得很清楚。FileSystemHandler 包装一个 ExecutorFileSystem,并暴露 read_filewrite_filecreate_directoryget_metadataread_directoryremovecopy。远端 client path 会在 forwarding 时保留 sandbox context。下面是 RemoteFileSystem 的简化片段:

self.client.get().await?
    .fs_read_file(FsReadFileParams {
        path: path.clone(),
        sandbox: remote_sandbox_context(sandbox),
    })
    .await?;

6.2 Executor 拥有 Workspace

这里真正的 owner 不是“Codex 当前运行的机器”,而是本轮或本次操作选择的 executor environment。这也是为什么核心 shell path 会在 run_exec_like 中解析 executor filesystem,以及为什么 patch interception 可以在退回 opaque shell execution 之前处理。文件编辑也是副作用;第 11 章会把 patch path 作为自己的协议来讲。

七、不要混在一起的概念

本章的实用价值,主要是把相邻概念分开。一旦混在一起,故障就很难定位。

不要混在一起更好的区分为什么重要
command text 与 command policyraw shell text 会先 parse 或 lower,再做 policy decision一个字符串可能隐藏多个 command segment 或 shell semantics
approval 与 sandboxingapproval 授权一条 side-effect path;sandbox 限制一次 attempt经过审批的命令仍可能在 sandbox 内运行
process_id 与 OS pidprocess_id 是 Codex/exec-server logical handleremote process 与 retained session 需要协议身份
output blob 与 output streamoutput 有 seq、stream、exit、closed 和 failure stateUI replay 与 write_stdin polling 依赖顺序
local files 与 executor filesfilesystem operations 面向 selected executorremote workspace 不能和 client machine 混淆

贯穿本章的不变量很简单:副作用只能作为结构化请求穿过权限边界。 Shell execution、process continuation 和 filesystem mutation 都在用不同方式维护这个不变量。

应用到实践

  1. 先塑形,再 spawn。 先把 tool arguments 转成 typed execution requests,不要把任何文本直接当成可运行事实。
  2. 让 policy 决定 approval shape。 不要用 sandbox denial 代替显式的 allow、prompt 或 forbid decision。
  3. 把 sandbox attempt 当成 runtime state。 无 sandbox retry 是第二次受治理 attempt,不是静默 fallback。
  4. 保持 process identity 的逻辑性。 process_id 属于 Codex/exec-server session contract,不是 OS pid。
  5. 把 filesystem work 送到 executor。 Reads、writes、copies、removals 必须面向拥有 selected workspace 的文件系统,并携带 sandbox context。

第 11 章会从 shell stream 收窄到一条文件编辑路径:patch。这个分离不是形式主义。它让 Codex 可以把 edit 当作结构化 mutation 来审查和应用,而不是把编辑藏进任意 shell 文本。

源码地图

概念源码锚点
Shell handler request shapingcodex-rs/core/src/tools/handlers/shell/shell_handler.rs
Shared shell execution pathcodex-rs/core/src/tools/handlers/shell.rs
Exec policy approval conversioncodex-rs/core/src/exec_policy.rs
Unmatched command fallbackcodex-rs/core/src/exec_policy.rs
Tool orchestrator approval and attempt flowcodex-rs/core/src/tools/orchestrator.rs
Sandbox denial retry branchcodex-rs/core/src/tools/orchestrator.rs
Sandbox attempt transformcodex-rs/core/src/tools/sandboxing.rs
Unified exec handler request bindingcodex-rs/core/src/tools/handlers/unified_exec/exec_command.rs
Unified exec request shapecodex-rs/core/src/unified_exec/mod.rs
Unified exec process managercodex-rs/core/src/unified_exec/process_manager.rs
Unified exec runtime adaptercodex-rs/core/src/tools/runtimes/unified_exec.rs
Exec-server protocol methods and process outputcodex-rs/exec-server/src/protocol.rs
Exec-server client callscodex-rs/exec-server/src/client.rs
Remote process adaptercodex-rs/exec-server/src/remote_process.rs
Executor filesystem handlercodex-rs/exec-server/src/server/file_system_handler.rs
Remote filesystem sandbox forwardingcodex-rs/exec-server/src/remote_file_system.rs