跳转到主要内容
有些工具调用太重要而不能盲目放行,又太有用而不能直接禁止——一次生产数据库 写入、一次电汇、一次对真实数据的 *.delete。对于那些,你想要一个人在循环 中:挂起该调用,让一名人工查看,然后只在得到一个”是”之后才继续。那正是 pending_approval 判定 所做的。 本页端到端地涵盖人工介入智能体审批流程:一个被挂起的调用如何浮现、一名 审查者如何从控制台或一个 webhook 解决它,以及智能体如何重新提交获批的 调用。关于该判定在规则语法中的位置,参见 防火墙规则;关于围绕它的策略模型,参见 防火墙概览

1. 一个被挂起的调用是什么样

当一条规则解析为 pending_approval 时,引擎入队一条审批记录,而该 调用不会到达工具。中继返回 HTTP 400error.codefirewall_approval_pending;智能体将轮询的审批 id 携带在人类可读的 error.message 中:
{
  "error": {
    "code": "firewall_approval_pending",
    "message": "tool \"db.write\" held for approval (…) — resolve approval 507f1f77bcf86cd799439011 and retry with header X-OrcaRouter-Firewall-Approval"
  }
}
结构化的 error.metadata(存在时)携带该判定的原因细节——reason_codefactorsrisk_score——而非审批 id。从消息中解析出该 id,或从下面的 SDK 辅助函数获取它。 挂起是即时的——没有内联的长轮询阻塞你的请求。智能体拿回该 id,该调用 在服务端以 pending 状态被驻留,而解决带外发生。
一个被挂起的调用被记录为一个判定为 pending_approval 的防火墙事件,所以它 在 事件日志 中可与 deny 事件并排过滤 ——你总能看到什么被挂起,以及通过审批记录看到什么被解决。

2. 一个具体示例

编写一条把对一个生产连接的任何写入挂起以供人工的规则:
{
  "label": "hold prod db writes",
  "tool_name_glob": "db.write",
  "verdict": "pending_approval",
  "args_match_json": "{\"clauses\":[{\"path\":\"$.connection\",\"op\":\"eq\",\"value\":\"prod\"}]}"
}
现在是生命周期:
1

智能体调用该工具

智能体对 prod 发出 db.write。该规则匹配,引擎挂起该调用,中继返回 400 firewall_approval_pending,带一个 approval_id
2

一名人工(或你的系统)审查

一名审查者解决该审批——在控制台中或通过一个签名 webhook 回调(参见 §3)。
3

智能体轮询直到解决

智能体轮询该审批 id,直到它的状态不再是 pending(参见 §4)。
4

智能体携带审批头重新提交

approved 时,智能体把完全相同的调用重新发出一次,携带一个一次性的 X-OrcaRouter-Firewall-Approval 头。引擎认领该审批并放那一次调用通过。

3. 解决一个审批

有两种方式把一个 pending 审批变成 approvedrejected。两者共享一个 首个决策生效保证——第一个落地的解决被原子地应用,而任何更晚的解决 (或一个重复)是一个返回 200 的幂等空操作。
Approvals 标签以最旧优先列出挂起的挂起项,每一个带有工具名和一行 “Held because…” 点名触发的策略和规则子句。(原始调用参数不存储在审批 记录上——只有工具名、来源和一个参数哈希——所以审查者从工具加匹配到的 子句来决定。)一名审查者用以下方式解决一个:
PATCH /api/workspace/firewall/approvals/:id
{ "decision": "approved", "reason": "verified change ticket #4821" }
decision 必须是 approvedrejected。这条路由是 UserAuth (审查者的控制台会话)并门控到 Developer+——你审查者的身份就是 授权,所以不涉及共享密钥。解决被写入工作区审计日志。
要把审批接入一个外部系统(一个 Slack 审批、一个工单工作流),为工作区 配置一个审批 webhook 密钥,然后把决策 POST 回来:
POST /api/v1/firewall/approvals/:id/callback
{ "decision": "approved", "reason": "auto-approved by change-control bot" }
该回调通过 HMAC-SHA256 认证:把 X-Orca-Signature: sha256=<hex> 头 设为以你工作区的审批 webhook 密钥为键的 <approval_id>\n<raw_body> 的 HMAC。该 id 是签名材料的一部分,所以一个被捕获的签名无法对一个不同的 审批重放。没有配置一个密钥时,回调驱动的解决被拒绝——改用控制台 PATCH 来解决。
为无人值守运行配置一条审批 webhook 拒绝路径是安全的默认:如果没有人工 解决一个挂起,该调用就单纯驻留,而智能体继续轮询。一个被挂起的调用绝不会 静默地变成一个允许。

4. 轮询,然后重新提交

智能体一侧是一个轮询循环,后跟一次重新提交。 用一个 firewall-gateway-scoped 令牌轮询审批状态:
GET /api/v1/firewall/approvals/:id
这条路由需要一个带 firewall-gateway scope 的令牌(与用于 /evaluate 和 MCP 网关的同一个专用 网关密钥);一个 普通中继密钥得到 403。它返回审批文档——等到 stateapprovedrejected 而非 pending。一个跨工作区或未知的 id 返回 404,绝不向另一个 租户透露它存在。 一旦状态是 approved重新提交:把同样的工具调用重新发出,携带一个 一次性头中的审批 id:
X-OrcaRouter-Firewall-Approval: 507f1f77bcf86cd799439011
引擎原子地认领该审批——一次性。携带它的第一次重新提交被放行那一次; 同一个头的一次重放发现该审批已被消费,会被再次挂起,而非允许。一个 rejected 审批永不可认领,所以智能体应该把拒绝当作一个终端的 deny, 另选一条路径。
OrcaRouter MCP SDK 的 HITL 辅助函数为你运行这个轮询-然后-重新提交循环:当 evaluate 返回 pending_approval 时,它轮询 GET /api/v1/firewall/approvals/:id 并在获批时携带审批头重新提交——你只需 编写规则并配备审查者。

5. 状态和角色一览

状态含义智能体动作
pending已挂起,等待一个决策继续轮询
approved审查者说是携带头重新提交一次
rejected审查者说否当作一次拒绝
操作路由认证 · 角色
列出队列GET /api/workspace/firewall/approvalsUserAuth · Developer+
解决PATCH /api/workspace/firewall/approvals/:idUserAuth · Developer+
Webhook 回调POST /api/v1/firewall/approvals/:id/callbackHMAC 签名
轮询状态GET /api/v1/firewall/approvals/:id网关令牌

6. 审批的定位

一个 pending_approval 判定是 防火墙判定 之一——它与一条策略中的其他一切组合。两个值得了解的交互:
  • 技能隔离升级为一次挂起。 如果一个被挂起的工具调用归属于一个 被隔离的技能,任何未到拒绝程度的判定都会 被自动升级为 pending_approval——隔离和审批是从两个方向看的同一个审查门。
  • 影子模式抹平它。影子模式 下, 一个 pending_approval 判定被降级为 audit 并记录为 [shadow] would …, 所以你能在一次挂起开始对真实流量设门之前衡量它本来会多频繁地触发。
这是针对 危险工具调用过度代理 的正确控制——在那些情形里, 一个”问一个人”的判定胜过 allow 和 deny。

接下来去哪里

判定

全部六个防火墙判定以及默认判定。

网关密钥

铸造用于轮询审批的 firewall-gateway 令牌。

影子模式

在一次挂起对真实流量设门之前衡量它。

规则参考

编写产生一个 pending_approval 判定的规则。