Skip to content
预计 1 分钟右侧目录可快速定位

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 建议 ​

精确定位

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:

Symbols:

  • TurnState::insert_pending_approval(...)
  • TurnState::remove_pending_approval(...)
  • TurnState::clear_pending_waiters(...)
  • ExecApprovalRequirement
  • ExecPolicyManager::create_exec_approval_requirement_for_command(...)
  • ApprovalStore
  • with_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 policy

Session-scoped cached approval 是另一条路径:

text
approval keys
→ ApprovalStore lookup
→ ApprovedForSession?
→ skip prompt or fetch ReviewDecision
→ optionally cache per key

Observable 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 ​

  1. PendingApproval 至少记录 approval_id / run_id / tool_call_id / action_digest / permission_scope / policy_version / created_at / status。
  2. 内存 waiter 与 durable approval record 分开:进程重启后重建 waiter,但不能丢 action identity。
  3. resume 前重新校验 tool definition、policy version、environment identity 与 action digest;发生变化则旧 approval 失效或重新请求。
  4. 测试 approval requested → crash → resume:不得重复副作用,也不得把旧批准套到另一个 command。

不要过度推导 ​

TurnState.pending_approvals 能证明 Codex 把 pending approval 当 Runtime state,但不能单凭这个内存 HashMap 证明 Codex 的完整跨进程 durable approval 方案。更不能从 ApprovedForSession 推出“审批后永久拥有权限”。

完整映射见 OpenAI Codex Source Map。

自测 ​

  1. 审批请求发出后进程重启。Approval state 必须保存哪些身份和 pending action,才能避免恢复后执行错动作?
  2. 用户批准的是“这个具体副作用”还是“未来任何类似动作”?为什么 approval token 必须绑定明确范围?

Sources ​

AI Engineering · Agent · Harness · Backend Systems