Appearance
Human Control 如何跨进程工作:HITL、Interrupt、Approval 与 Resume
前置:下游故障后怎么继续服务:Circuit Breaker 与 Graceful Degradation
核心问题:人工审批跨小时后由另一个进程继续时,Approval 状态、权限和待执行动作怎样保持一致且可审计?
模型准备向 100 个收件人发送邮件,但这个动作需要人确认。Runtime 不能占住一个线程等几小时,也不能只记一句“等待批准”:它要在副作用发生前 Interrupt,持久化 pending action;用户可以 approve、reject 或 edit;服务即使重启,也能从 Resumable Run State 找回同一动作版本并重新校验后继续。
Human-in-the-Loop
HITL 的关键不是“弹一个确认框”,而是把人的决策变成可持久化、可审计、可恢复的 Runtime 节点。
HITL 把人作为 Runtime 的显式决策节点,用于高风险、歧义或需要授权的动作。它要支持持久化等待、超时、身份校验和审计。
Interrupt
Interrupt 是 Runtime 在某个可恢复边界暂停当前 Run,并把“继续执行所需的外部输入”暴露给调用方的控制事件。它要求保存足够 RunState,使进程退出后仍能 resume。
Interrupt 前的代码可能在 resume 时重新运行,因此前置副作用必须幂等或置于安全边界之后。
Approval / Reject / Edit
Interrupt 只是“Runtime 暂停,等待外部决策”;Approval / Reject / Edit 才是用户返回的结构化决定。
text
proposed action v3
→ interrupt + durable approval request
→ user edits amount 1000 → 500
→ new action v4
→ 重新 validation / authorization
→ execute v4特别是 Edit,不能把修改后的参数偷偷套到原已批准 action 上。它实际上产生了一个新的候选动作,应重新经过 schema、policy 和必要的风险检查,并记录谁修改了什么。
🔍 源码对照:Pending Approval 是 Runtime Gate,不是 Prompt 建议
精确定位
- 文件:
source/host/runner/auto-review-gate.ts - Symbol:
createAutoReviewGate()→assertNoPendingApproval()→shellApprovalState() - 深入索引:Code Atlas:05-reliability-approval
ts
function assertNoPendingApproval() {
const pending = controller.getPendingApprovals()
if (pending.length > 0)
throw new PendingApprovalError()
}
function shellApprovalState(surface) {
return {
getIdentity: () => `${surface}:${generation[surface]}`,
markSideEffectStart: () => generation[surface]++,
}
}对照理解
- “等待审批”不是 Prompt 里的提醒,而是 Runtime 可查询的 pending state;存在 pending approval 时,新的 side effect 直接被 Gate 拒绝。
shellApprovalState()还为 side-effect generation 建立 identity,说明 Approval 必须绑定具体执行状态,而不是笼统绑定 Tool 名。- 这正是 Resume 时需要重新验证 action identity 的原因:旧批准不能无条件套到新参数或新 generation。
源码说明
auto-review-gate.ts 可以直接看到多个受控 surface,以及 assertNoPendingApproval():只要已有 pending approval,就阻止新的 side effect 开始。这说明 Approval 不是“模型问用户一句话”,而是会改变 Runtime 可执行集合的控制状态。
工程边界
Auto-review 的分类模型、surface 名称和 UX 属于 Grok Bot;通用 invariant 是 pending approval 必须 durable、可关联原 action,并在 resume 时重新验证环境。
Shadow Harness 对照
- ApprovalRecord 保存 action digest、risk、requested_at、decision/version,而不是只保存 yes/no。
- resume 后重新 resolve capability/path/version,避免批准旧意图却执行新内容。
验证
Action A 进入 WAITING_APPROVAL 后尝试启动 Action B side effect;B 必须被 gate 阻止。重启后批准 A,再验证 action digest 未变化才允许执行。
Long-running Approval
审批可能持续几分钟甚至几天,因此等待期间不能一直占着线程、数据库事务、HTTP connection 或 sandbox lease。
正确形态更像:
text
Runtime 提交 approval_request + checkpoint
→ Run = WAITING_APPROVAL
→ Worker 释放
...
审批事件到达
→ 校验 request 是否仍有效
→ 重新检查权限/环境
→ 新 Worker resume这也是为什么 approval 必须是 durable state,而不是内存里的 Future。如果审批期间 Tool schema、权限或目标资源已经变化,恢复时还要重新验证。
Resumable Run State
可恢复状态只保存继续执行所需的 durable facts,例如 current node、completed attempts、pending approval、approved action version 和 checkpoint reference;socket、lock、coroutine、provider client 等瞬态资源在 resume 时重新建立。
可恢复状态必须可序列化、版本化并包含继续执行所需的事实,而不是保存 Python coroutine/对象引用。
Failure Modes
等待审批时占住进程或丢失 Resume State
症状: Runtime 用阻塞 Future 等用户数小时,重启后 pending action 消失。
为什么危险: 资源占用且无法知道批准的是哪个动作版本。
处理: approval boundary 持久化 RunState/action hash/record,释放计算;恢复时重新授权。
Resume 时执行了未经批准的旧参数
症状: 用户把 recipient 从 100 人改成 3 人并批准,但 Runtime resume 后仍执行原始 ToolCall。
为什么危险: Approval 只存在于 UI 层,没有成为 durable execution fact。
处理: 持久化 approved action version/hash;resume 时重新读取并授权最终参数,旧 proposal 只能保留审计记录。
如何验证
| Case | 断言 |
|---|---|
| 等待审批时占住进程或丢失 Resume State | 创建 approval 后终止进程再恢复,同一 action 可 approve/reject 且不重复执行。 |
| Resume 使用旧参数 | 用户 edit 后重启服务,恢复执行必须使用 approved action version,原始 proposal 不得产生副作用。 |
关联源码
Codex 源码对照:Approval 是 Runtime State,不是一个 bool
要回答的问题
一次高风险 ToolCall 需要人工批准时,Runtime 至少要保存什么 pending identity?为什么 approved=true 不足以支持安全 resume?
精确定位
Tag: rust-v0.150.1
File:
codex-rs/core/src/state/turn.rscodex-rs/core/src/exec_policy.rscodex-rs/core/src/tools/sandboxing.rs
Symbols:
TurnState::insert_pending_approval(...)TurnState::remove_pending_approval(...)TurnState::clear_pending_waiters(...)ExecApprovalRequirementExecPolicyManager::create_exec_approval_requirement_for_command(...)ApprovalStorewith_cached_approval(...)
最小调用链
text
command + approval policy + permission profile
→ ExecPolicyManager::create_exec_approval_requirement_for_command(...)
→ ExecApprovalRequirement
├─ Skip
├─ NeedsApproval
└─ Forbidden
→ when interaction is required:
active TurnState keeps pending approval keyed by identity
→ ReviewDecision arrives
→ remove/resolve pending waiter
→ execution may continue under current permission/sandbox policySession-scoped cached approval 是另一条路径:
text
approval keys
→ ApprovalStore lookup
→ ApprovedForSession?
→ skip prompt or fetch ReviewDecision
→ optionally cache per keyObservable Fact
TurnState 有 pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,并提供 insert/remove/clear pending waiter 的操作;这说明“等待 approval”本身是 active-turn Runtime state,而不是模型上下文中的一句文字。
另一方面,tools/sandboxing.rs::ApprovalStore 可以按序列化 key 缓存 ApprovedForSession。也就是说 Codex 区分 当前 pending interaction 和 可复用的 session approval cache,二者 lifecycle 不同。
Design Inference
安全 HITL / resume 的 invariant 是:approval 必须绑定具体 action identity、request scope、policy/permission context 与生命周期。 Runtime 需要知道“批准的是哪个 pending side effect”,而不是只保存一个无上下文布尔值。
Codex-specific Trade-off
Codex rust-v0.150.1 使用 oneshot::Sender<ReviewDecision> 维护进程内 pending waiter,并用序列化 approval key 做 session cache。这些具体数据结构属于实现选择;跨进程 durable resume 是否以及如何重建 waiter,需要由更高层 persistence contract 决定。
Shadow Harness Delta
PendingApproval至少记录approval_id / run_id / tool_call_id / action_digest / permission_scope / policy_version / created_at / status。- 内存 waiter 与 durable approval record 分开:进程重启后重建 waiter,但不能丢 action identity。
- resume 前重新校验 tool definition、policy version、environment identity 与 action digest;发生变化则旧 approval 失效或重新请求。
- 测试
approval requested → crash → resume:不得重复副作用,也不得把旧批准套到另一个 command。
不要过度推导
TurnState.pending_approvals 能证明 Codex 把 pending approval 当 Runtime state,但不能单凭这个内存 HashMap 证明 Codex 的完整跨进程 durable approval 方案。更不能从 ApprovedForSession 推出“审批后永久拥有权限”。
完整映射见 OpenAI Codex Source Map。
自测
- 审批请求发出后进程重启。Approval state 必须保存哪些身份和 pending action,才能避免恢复后执行错动作?
- 用户批准的是“这个具体副作用”还是“未来任何类似动作”?为什么 approval token 必须绑定明确范围?