修复 M1A-2 引入的回归:未知工具名不是身份违规
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:
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
+2
-2
@@ -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(),
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -329,11 +329,9 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_block_after_lock(
|
||||
command_id: &str,
|
||||
pending_action: Option<&AgentRuntimePendingToolAction>,
|
||||
) -> Option<AgentRuntimeToolPolicyBlock> {
|
||||
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(),
|
||||
));
|
||||
|
||||
@@ -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` 解决的一项外,另有两处代码残余与三处文档残余。本条记录代码两处的处置,文档三处随本次提交一并订正。
|
||||
|
||||
Reference in New Issue
Block a user