修复 M1A-2 引入的回归:未知工具名不是身份违规
Project CI / Repository checks (pull_request) Successful in 1m9s
Project CI / Frontend tests (pull_request) Successful in 2m54s
Project CI / Backend tests (pull_request) Successful in 3m56s
Project CI / Native shell tests (pull_request) Failing after 10m41s

background_agent_runtime_persists_receipts_for_rejected_actions 在本分支
恒失败(3/3),master 通过。首个 tool-plan 请求能收到,动作被拒绝后第二次
Provider follow-up 不再发出,测试等待超时。不是已知的 mock-LLM 本机 flake。

根因是 M1A-2 给主循环加身份门时用了 agent_runtime_tool_allowed_for_agent,
而该函数对非 project-planning 的 Agent 退化成「这个工具名是否已知」。于是
普通 Agent 调用一个不存在的工具(模型编名字,常见协议错误)被判成身份违规,
main_loop 直接 mark_needs_reconciliation 并返回 NeedsReconciliation,整个
run 中断。现役语义是未知工具产出一条 rejected observation、run 继续、由下
一轮 tool-plan 收束。

两类必须分开:身份禁止某个已知工具是安全边界,命中即硬拒;工具名根本不存在
是可恢复的协议错误,不得升级成中断整个 run。

新增 agent_runtime_tool_rejected_by_agent_identity,只在「该 Agent 带 exact
allowlist 且工具不在其中」时为真。三个命中即中断或整体拒绝的调用点改用它:
main_loop(本次回归直接原因)、provider_action_batch 的 identity_block(原会
把普通 Agent 的未知工具从 rejected 误判成 blocked)、runtime_tools/policy
(原本就正确限定 planning,改为复用同一判据以免再次分叉)。parallel_ledger
内部批次资格判定不变,其下一行的 command_id 检查本就拦得住。

planning 侧约束未放松,两条单测分别钉死普通 Agent 与 planning 两侧。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 06:35:45 +00:00
parent 5fa662be30
commit 323db581fa
6 changed files with 69 additions and 8 deletions
@@ -1,5 +1,14 @@
# 决策记录
## 2026-08-14 修复 M1A-2 引入的回归:未知工具名不是身份违规
- 症状:`background_agent_runtime_persists_receipts_for_rejected_actions` 在 feature 分支恒失败(3/3),`master` 通过。首个 tool-plan 请求能收到,动作被拒绝后**第二次 Provider follow-up 请求不再发出**,测试等待超时。**不是已知的 mock-LLM 本机 flake**,是确定性回归。
- 根因:`M1A-2` 给主循环加身份门时用了 `agent_runtime_tool_allowed_for_agent`,但该函数对**非** `project-planning` 的 Agent 退化成「这个工具名是否已知」(`game_creator_agent_runtime_tool_command_id(..).is_some()`)。于是普通 Agent 调用一个不存在的工具(模型编名字,属常见协议错误)被判成身份违规,`main_loop` 直接 `mark_needs_reconciliation` 并 `return NeedsReconciliation`,整个 run 中断。**现役语义是:未知工具走到执行层产出一条 `rejected` observation,run 继续,由下一轮 tool-plan 收束。**
- 两类必须分开:**「身份禁止某个已知工具」**(planning 的 exact allowlist)是安全边界,命中即硬拒;**「工具名根本不存在」**是可恢复的协议错误,不得升级成中断整个 run。混用会把模型一次笔误变成需要人工核对的终态。
- 处置:新增 `agent_runtime_tool_rejected_by_agent_identity`,只在「该 Agent 带 exact allowlist 且工具不在其中」时为真。三个「命中即中断 run 或整体拒绝动作」的调用点改用它——`main_loop.rs`(原为硬中断,本次回归的直接原因)、`provider_action_batch.rs` 的 `identity_block`(原会把普通 Agent 的未知工具从 `rejected` 误判成 `Denied`/`blocked`)、`runtime_tools/policy.rs`(原本就正确限定了 planning,改为复用同一判据以免再次分叉)。`parallel_ledger.rs` 内部的批次资格判定不变——它下一行的 `command_id` 检查本就会拦住未知工具,无过度拒绝。
- planning 侧约束未放松:planning + 未知工具、planning + 越权工具仍判身份拒绝,回归 `planning_identity_still_rejects_unknown_and_out_of_scope_tools` 钉死;`unknown_tool_on_ordinary_agent_is_not_an_identity_rejection` 钉死普通 Agent 侧。
- 教训:给已有主循环插「命中即中断」的门时,判据必须是**专门表达该门语义**的谓词。复用一个名字听起来正确、但对多数输入退化成别的含义的通用函数,会在没人测到的分支上改变现役语义。
## 2026-08-14 M1A 复核残余收口:retry 强判据前置、身份哨兵改为编译期约束
对 `M1A-1`~`M1A-4` 做整组复核后,除已由 `M1A-4` 解决的一项外,另有两处代码残余与三处文档残余。本条记录代码两处的处置,文档三处随本次提交一并订正。