From 323db581fafa0c7c186eba38e69b42f6ab414b07 Mon Sep 17 00:00:00 2001 From: Linghong Date: Fri, 14 Aug 2026 06:35:45 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20M1A-2=20=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E7=9A=84=E5=9B=9E=E5=BD=92=EF=BC=9A=E6=9C=AA=E7=9F=A5=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=90=8D=E4=B8=8D=E6=98=AF=E8=BA=AB=E4=BB=BD=E8=BF=9D?= =?UTF-8?q?=E8=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../src-tauri/src/agent/runtime_actions.rs | 1 + .../agent/runtime_actions/parallel_ledger.rs | 53 +++++++++++++++++++ .../runtime_actions/provider_action_batch.rs | 4 +- .../src/agent/runtime_driver/main_loop.rs | 2 +- .../src/agent/runtime_tools/policy.rs | 8 ++- .../shared-memory/decision-log.md | 9 ++++ 6 files changed, 69 insertions(+), 8 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs index c01b4d3d6..54a598dcc 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs @@ -73,6 +73,7 @@ pub(crate) use context_compaction::compact_game_creator_agent_runtime_session_at pub(crate) use parallel_ledger::{ agent_runtime_confirmation_path_component, agent_runtime_parallel_read_batch_len, agent_runtime_tool_allowed_for_agent, agent_runtime_tool_is_parallel_safe_read, + agent_runtime_tool_rejected_by_agent_identity, game_creator_agent_runtime_parallel_read_batch_path, game_creator_agent_runtime_pending_tool_action_path, game_creator_agent_runtime_provider_action_batch_path, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs index e154e25d8..ca7de3013 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/parallel_ledger.rs @@ -129,10 +129,63 @@ pub(crate) fn agent_runtime_tool_allowed_for_agent(agent_id: &str, tool: &str) - game_creator_agent_runtime_tool_command_id(tool.trim()).is_some() } +/// 身份层面的**显式**拒绝:该 Agent 身份带 exact allowlist,且工具不在其中。 +/// +/// **未知工具名不属于本判据。** `agent_runtime_tool_allowed_for_agent` 对普通 +/// Agent 退化成「这个工具名是否已知」,用它做身份门会把「模型编了个不存在的 +/// 工具」这种普通协议错误误判成身份违规。协议错误的既有语义是:走到执行层产出 +/// 一条 `rejected` observation,run 继续,由下一轮 tool-plan 收束;升级成身份 +/// 拒绝会让整个 run 进 needs-reconciliation 而**不再发出 follow-up 请求**。 +/// +/// 因此凡是「命中即中断 run 或整体拒绝动作」的调用点都必须用本判据,不能直接 +/// 用 `agent_runtime_tool_allowed_for_agent`。 +pub(crate) fn agent_runtime_tool_rejected_by_agent_identity(agent_id: &str, tool: &str) -> bool { + agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID + && !agent_runtime_tool_allowed_for_agent(agent_id, tool) +} + #[cfg(test)] mod identity_tests { use super::*; + /// 普通 Agent 编出来的未知工具名是**协议错误**,不是身份违规。 + /// + /// 判成身份违规会让 main_loop 把整个 run 打进 needs-reconciliation、不再发出 + /// follow-up tool-plan——曾导致 `background_agent_runtime_persists_receipts_for_rejected_actions` + /// 在等待第二次 Provider 请求时超时。 + #[test] + fn unknown_tool_on_ordinary_agent_is_not_an_identity_rejection() { + assert!(!agent_runtime_tool_rejected_by_agent_identity( + "design-director", + "runtime.unknown" + )); + assert!(!agent_runtime_tool_rejected_by_agent_identity( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + "runtime.unknown" + )); + assert!(!agent_runtime_tool_rejected_by_agent_identity( + "design-director", + "file.read" + )); + } + + /// planning 身份仍是 exact allowlist:未知工具与越权工具都算身份拒绝。 + #[test] + fn planning_identity_still_rejects_unknown_and_out_of_scope_tools() { + assert!(agent_runtime_tool_rejected_by_agent_identity( + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + "runtime.unknown" + )); + assert!(agent_runtime_tool_rejected_by_agent_identity( + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + "file.write" + )); + assert!(!agent_runtime_tool_rejected_by_agent_identity( + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + "file.read" + )); + } + #[test] fn planning_identity_does_not_inherit_project_search_alias() { assert!(agent_runtime_tool_allowed_for_agent( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs index d1a211297..a0578184a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs @@ -473,10 +473,10 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch( None, )?; let command_id = game_creator_agent_runtime_tool_command_id(action.tool.trim()); - let identity_block = (!agent_runtime_tool_allowed_for_agent( + let identity_block = agent_runtime_tool_rejected_by_agent_identity( &runtime.agent_id, action.tool.trim(), - )) + ) .then(|| { AgentRuntimeToolPolicyBlock::Denied( "当前 Agent 身份不允许执行该原始工具".to_string(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs index b65b36c3a..2612fd315 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs @@ -2696,7 +2696,7 @@ async fn run_game_creator_agent_background_task_pass_without_deadline( } return AgentBackgroundTaskOutcome::WaitingForUserInput; } - if !agent_runtime_tool_allowed_for_agent(&agent_id, action.tool.trim()) { + if agent_runtime_tool_rejected_by_agent_identity(&agent_id, action.tool.trim()) { let pending_action = prepared_action .as_ref() .expect("prepared action exists for an identity-rejected tool"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs index 86ed79339..9db10241c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs @@ -329,11 +329,9 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_block_after_lock( command_id: &str, pending_action: Option<&AgentRuntimePendingToolAction>, ) -> Option { - if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID - && pending_action.is_some_and(|pending| { - !agent_runtime_tool_allowed_for_agent(agent_id, &pending.action.tool) - }) - { + if pending_action.is_some_and(|pending| { + agent_runtime_tool_rejected_by_agent_identity(agent_id, &pending.action.tool) + }) { return Some(AgentRuntimeToolPolicyBlock::Denied( "当前 Agent 身份不允许执行该原始工具".to_string(), )); diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index b42877e35..847b563ca 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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` 解决的一项外,另有两处代码残余与三处文档残余。本条记录代码两处的处置,文档三处随本次提交一并订正。