澄清任务的身份豁免按任务判而不是按工具判

上一笔把豁免收窄成「工具必须是 user.input_request」,只覆盖了转述动作本身。
实际上用户答完后 pending_execution 调的是
run_recovered_game_creator_context_on_fresh_task(root, agent_id,
pending.task.clone(), ...):整条 run 从此改跑在转述任务上,同一 run 后续的
agent.run_status、agent.delegate 全都带着它,而 runtime.current_task 仍是用户
原始请求。

于是 run 15 过了转述那一关,却在续跑第一步就挂:Supervisor 按 supervisorRepair
的要求调 agent.run_status 重读权威合同,动作在等到项目锁之后被
validate_agent_runtime_pending_context 判成「pending action 身份已变化」,仍是
needs-reconciliation。

  pending.tool = agent.run_status
  pending.task = 子 Agent 需要用户澄清后才能继续。delegationId=delegation-4216f572…
  runtime.currentTask = 做个横版像素解谜小游戏,主角是个能操控自己影子的小机器人。

判据改成只看 task 是不是 Runtime 生成的转述指令。同一个 || 链里的
validate_agent_runtime_pending_action_after_lock 不比较 task,所以豁免面仍然只
有这一条文本相等;agent/task_id/session/run/source 与轮次检查照旧全走。

回归测试补上 run 15 实测的那个形状:同一 run、同一转述任务、工具是
agent.run_status 的续跑动作必须通过,而 task 被改写的同形状动作必须照旧被拒。
三向变异验证:恢复严格比较、整条删掉、把判据换成恒真替身,都会红。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-21 05:33:48 +00:00
parent ddb8eaec37
commit 98abcf4f0d
2 changed files with 37 additions and 14 deletions
@@ -173,18 +173,22 @@ pub(in crate::agent) fn validate_agent_runtime_pending_context(
{
return Err("Agent Runtime 待确认动作身份与当前状态不匹配".to_string());
}
// 转述 pending 是唯一一处 `task` 字段存的不是本 run 任务的动作Runtime 代
// 澄清转述是唯一一处 `task` 字段存的不是本 run 任务的场景Runtime 代
// Supervisor 汇总子 Agent 澄清问题时,把「回答后创建唯一 continuation 委派」
// 那段指令连同 delegationId 写在这里(provider_recovery.rs),而 run 的
// current_task 始终是用户原始请求。其余五个 pending 创建点传的都是 run 的真实
// task,所以这条相等断言对它们成立,对转述则永不可能成立——D11 澄清路径实测
// 每一次都会在 resume 时被判成 needs-reconciliation,用户答案已落盘却无法继续。
//
// 豁免按 task 判、不按工具判:用户答完后整条 run 都改跑在转述任务上
// run_recovered_game_creator_context_on_fresh_task 传的就是 pending.task),
// 所以续跑里的 agent.run_status、agent.delegate 同样带着它。
//
// 这里只豁免文本相等这一条。pending 与 runtime 的 agent/task_id/session/run/
// source 五项身份检查在上面已经全部通过,轮次检查在下面继续执行,转述 pending
// 本身也只能由 Runtime 在本 run 内生成,所以豁免不放开任何跨 run 或跨身份的
// 重放面。
if !agent_runtime_pending_is_delegate_clarification_relay(pending) {
if !agent_runtime_task_is_delegate_clarification_relay(&pending.task) {
validate_agent_runtime_context_task_parameter(root, runtime, &pending.task)?;
}
if pending.loop_iteration != runtime.loop_iteration {
@@ -894,20 +898,36 @@ mod tests {
};
let relay = build(&relay_task, &action);
assert!(agent_runtime_pending_is_delegate_clarification_relay(
&relay
assert!(agent_runtime_task_is_delegate_clarification_relay(
&relay.task
));
assert_ne!(relay.task, runtime.current_task);
validate_agent_runtime_pending_context(&root, &runtime, &relay)
.expect("转述 pending 必须能通过身份校验,否则用户答案落盘后无法续跑");
// 豁免只覆盖转述本身。同一工具、同一 run,但 task 不是 Runtime 生成的
// 转述指令时,原有的相等断言必须照旧拒绝——否则这就是个洞而不是修复。
// 用户答完后整条 run 都改跑在转述任务上,后续动作同样带着它。按工具收窄
// 豁免会让续跑第一步 agent.run_status 就被判身份变化——run 15 实测如此,
// 所以这里显式覆盖非 user.input_request 的后续动作。
let follow_up = AgentRuntimeToolAction {
tool: "agent.run_status".to_string(),
reason: Some("读取原委派的权威合同,准备创建唯一一次续跑委派".to_string()),
input: serde_json::json!({}),
};
let resumed = build(&relay_task, &follow_up);
validate_agent_runtime_pending_context(&root, &runtime, &resumed)
.expect("续跑动作同样跑在转述任务上,必须通过身份校验");
// 豁免只覆盖 Runtime 生成的转述任务。task 不是转述指令时,原有的相等断言
// 必须照旧拒绝——否则这就是个洞而不是修复。
let forged = build("被改写的任务", &action);
assert!(!agent_runtime_pending_is_delegate_clarification_relay(
&forged
assert!(!agent_runtime_task_is_delegate_clarification_relay(
&forged.task
));
assert!(validate_agent_runtime_pending_context(&root, &runtime, &forged).is_err());
let forged_follow_up = build("被改写的任务", &follow_up);
assert!(
validate_agent_runtime_pending_context(&root, &runtime, &forged_follow_up).is_err()
);
// 身份五项仍然照查:换一个 run_id 的转述 pending 不得被放行。
let mut cross_run = relay.clone();
@@ -17,12 +17,15 @@ pub(crate) fn agent_runtime_delegate_clarification_delegation_id(task: &str) ->
.filter(|value| !value.is_empty())
}
/// 该 pending 是否 Runtime 代 Supervisor 生成的子 Agent 澄清转述。
pub(crate) fn agent_runtime_pending_is_delegate_clarification_relay(
pending: &AgentRuntimePendingToolAction,
) -> bool {
pending.action.tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL
&& agent_runtime_delegate_clarification_delegation_id(&pending.task).is_some()
/// 该任务是否 Runtime 代 Supervisor 生成的子 Agent 澄清转述任务
///
/// 判据只看 task,不看工具:用户答完后
/// `run_recovered_game_creator_context_on_fresh_task` 会把 `pending.task` 当作同一
/// run 后续每一轮的任务,所以转述任务会一路传播到 `agent.run_status`、
/// `agent.delegate` 等动作上,而不是只停在那一次 `user.input_request`。按工具收窄
/// 会让续跑第一步就被判身份变化——run 15 实测如此。
pub(crate) fn agent_runtime_task_is_delegate_clarification_relay(task: &str) -> bool {
agent_runtime_delegate_clarification_delegation_id(task).is_some()
}
pub(crate) const AGENT_RUNTIME_USER_INPUT_SCHEMA_VERSION: &str =