具体问题与边界
从 argv 参数到子进程结果,校验、Policy、Approval、执行、超时、取消和截断的真实顺序是什么?
ShellTool 负责输入与策略;ExecutionEnvironment 负责进程;CancellationToken 决定中断;ToolRouter 负责结果配对。 本节以官方 Codex 锁定提交为事实基线,以 Mini Codex 的可执行断言检验 Python 设计;二者不是逐行翻译关系。
协议、类型与状态所有权
| 对象 | 创建/所有者 | 生命周期与作用域 | 是否持久化 |
|---|---|---|---|
| argv/cwd/timeout | ToolCall arguments | 单次调用 | 随 ToolCall 持久化 |
| ExecDecision | ExecPolicy | 单次命令 | 否 |
| Approval decision | ApprovalPolicy/Host | 单次动作 | Mini 不单独持久化 |
| subprocess | ExecutionEnvironment | 直到 exit/kill/reap | 否 |
| stdout/stderr | ExecutionEnvironment | 调用结果 | 编码进 ToolResult |
正常路径
- 参数先验证为非空字符串数组;Mini 不先拼 Shell 字符串,从而避免第二次 shell parse 改写 argv 边界。
- cwd 通过 WorkspacePolicy.resolve;这只限制启动目录,不能限制命令自己访问哪里。
- ExecPolicy 对 argv prefix 产生 allow/prompt/forbid。forbid 直接形成错误结果;prompt 才询问 Approval;allow 跳过人工询问。
- ExecutionEnvironment 用
create_subprocess_exec创建进程,并并发等待 communicate、取消事件与 timeout。 - 正常退出收集 returncode;timeout kill+wait 并返回 124;Interrupt kill+wait 后抛 TurnCancelled。输出在环境层统一截断。
调用链与等待点
1. 参数先验证为非空字符串数组;Mini 不先拼 Shell 字符串,从而避免第二次 shell parse 改写 argv 边界
→ 2. cwd 通过 WorkspacePolicy.resolve;这只限制启动目录,不能限制命令自己访问哪里
→ 3. ExecPolicy 对 argv prefix 产生 allow/prompt/forbid
→ 4. ExecutionEnvironment 用 `create_subprocess_exec` 创建进程,并并发等待 communicate、取消事件与 timeout
→ 5. 正常退出收集 returncode;timeout kill+wait 并返回 124;Interrupt kill+wait 后抛 TurnCancelled
每个 await 都是状态可被取消、外部副作用可能已经发生或其他任务能够推进的边界。正文因此同时写清输入形状、所有者、成功产物和失败残留,而不是只列方法名。
Python 风格伪代码
async def shell(arguments, ctx):
argv = require_string_array(arguments["command"])
cwd = ctx.workspace_policy.resolve(arguments.get("cwd", "."))
timeout = require_positive_number(arguments.get("timeout_seconds", 30))
decision = exec_policy.decide(argv)
if decision == FORBID:
return ToolExecution("forbidden", is_error=True)
if decision == PROMPT and not await ctx.approval.approve(argv, cwd):
return ToolExecution("rejected", is_error=True)
process = await create_subprocess_exec(*argv, cwd=cwd, pipes=True)
io_task = create_task(process.communicate())
cancel_task = create_task(ctx.cancellation.wait())
done = await wait_first(io_task, cancel_task, timeout)
if cancel_task in done:
kill_and_reap(process); raise TurnCancelled()
if io_task not in done:
kill_and_reap(process); return timeout_result(124)
return encode_result(process.returncode, truncate(stdout), truncate(stderr))
伪代码表达顺序和责任。真实可运行实现没有把万能调用当作未解释黑箱;对应模块见 Mini 导航。
失败、取消与恢复
| 故障点 | 已发生/残留状态 | 处理与模型可见结果 |
|---|---|---|
| argv 类型错误 | 不创建进程 | 错误 ToolResult,模型可修参数 |
| cwd 越界 | WorkspaceViolation | Router 编成 tool failed Result |
| Policy forbid/Approval deny | 没有副作用 | 模型看到拒绝原因 |
| timeout | 进程被 kill 并 wait | exit_code 124 与错误文本 |
| Interrupt | 进程被 kill/reap | Router 补 aborted,Turn owner Aborted |
| 输出过大 | 内存/Prompt 膨胀 | 在执行边界截断并标明移除量 |
必须保持的不变量
任何进程终点都必须明确收集或 kill+reap;拒绝、超时与普通非零退出不能消失成无 Result 的 ToolCall。
设计取舍
argv 模式舍弃管道、重定向等 Shell 语法,换取清晰参数边界。官方支持更多 shell backend、sandbox escalation 与平台差异;Mini 的 Local adapter 不隔离恶意命令。
“为什么”的表述是从源码状态机、调用顺序和测试反推的工程解释;源码未声明的动机不写成官方承诺。
测试与复现实验
cd examples/mini-codex
uv run pytest -q -k 'test_approval_rejection_becomes_model_visible_result or test_exec_policy_uses_longest_argv_prefix or test_interrupt_produces_one_aborted_terminal_event'
uv run mypy src
关键断言:
test_approval_rejection_becomes_model_visible_resulttest_exec_policy_uses_longest_argv_prefixtest_interrupt_produces_one_aborted_terminal_event
官方源码导航
- codex-rs/core/src/tools/handlers/shell.rs:Shell handler 参数与执行生命周期
- codex-rs/core/src/tools/runtimes/shell.rs:Policy、Approval、Sandbox 与执行
- codex-rs/core/src/tools/handlers/shell/shell_command.rs:命令请求形状
- codex-rs/core/src/tools/handlers/shell_tests.rs:拒绝、错误与输出行为
Mini Codex 对照
src/mini_codex/tools/shell.py:输入/Policy/Approval/结果编码src/mini_codex/execution/environment.py:timeout/cancel/kill/reap/truncatesrc/mini_codex/policy/exec_policy.py:argv prefix 决策
本节边界
已经证明:任何进程终点都必须明确收集或 kill+reap;拒绝、超时与普通非零退出不能消失成无 Result 的 ToolCall。
评论
登录后即可评论