审批标识与跨连接投票
区分 ACP 连接内 JSON-RPC ID、bridge requestId 和会话绑定。
审批可以通过当前 ACP transport 回答,也可以由另一个有权限的 HTTP 客户端使用 REST 回答。两条路径最终进入相同的 session-bound mediator 处理。
两套 request ID
| 标识 | 范围 | 用途 |
|---|---|---|
| _qwen_perm_N | 当前 ACP 连接内递增 | JSON-RPC 请求与响应关联 |
| bridge requestId | bridge 审批标识 | 在 REST 与 mediator 中定位请求 |
ACP session/request_permission 的 params._meta.qwen.requestId 携带 bridge 标识。REST 投票使用 POST /session/:id/permission/:requestId;不要把连接内 _qwen_perm_N 填进该 URL。
旧 POST /permission/:requestId 通过内部查找确定 session,再委派给同一路径。新客户端优先使用明确 session 的接口,不把旧全局路由当作跨工作区寻址保证。
检查顺序
- 地址中的 session 必须仍 live。
- requestId 若属于另一 session,拒绝且不披露成员关系。
- 未知 requestId 在 clientId 验证前返回未找到,避免通过不同错误探测会话成员。
- 显式 clientId 必须已登记到该 session;未提供时交给策略判断。
- 拒绝通过 selected optionId 注入内部取消 sentinel。
- 按请求创建时的 policy 处理投票。
匿名普通投票仅可能被 first-responder 或 loopback 的 local-only 接受;designated、consensus 拒绝匿名选择。已注册 ID 仍是 v1 自报身份,不具有持有证明。
回答 ACP 请求
在收到的同一 JSON-RPC id 上选择实际公布的 optionId:
{
"jsonrpc": "2.0",
"id": "_qwen_perm_3",
"result": {
"outcome": {
"outcome": "selected",
"optionId": "allow"
}
}
}allow 仅代表该请求实际提供这一选项时的示例。取消使用 outcome.outcome 为 cancelled;JSON-RPC error 响应也被 dispatcher 映射为取消。
内部 CANCEL_VOTE_SENTINEL 值为 cancelled,供 bridge 将显式取消转为 mediator 输入。wire 的 selected optionId 使用该值会拒绝;agent 公布允许选项时包含该 sentinel 也会触发 CancelSentinelCollisionError。
Loopback 按投票请求判断
REST 和 ACP 的 fromLoopback 都取自该次请求 socket remoteAddress,不使用 X-Forwarded-For、Forwarded 等 header。ACP initialize 来自本机,不代表后续从另一连接地址提交的投票也自动被认作本机。
这也意味着通过本地代理到达的请求与直接远端请求有不同 socket 来源;部署方不能仅用自报 clientId 或代理 header 推断操作者身份。
失败恢复
ACP dispatcher 回答审批时若 bridge 抛错,会尝试显式 cancelAbandonedPermission,避免留下无限等待。若原投票和取消都失败,保留 pending 映射供连接最终 teardown 再清理,而不是丢掉最后的恢复线索。
session close、eviction 和 bridge shutdown 都应调用 forgetSession,在 child 的关闭宽限期之前结算该 session 的待审批。不要把 HTTP 响应失败简单视为后台请求已经取消。