From 6234b420fcb4516a4f70f781164eae74cdb1b411 Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 18 Aug 2026 06:02:33 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20HEAD=20=E6=97=A2=E5=AD=98?= =?UTF-8?q?=E7=BA=A2=EF=BC=9A=E7=AD=96=E5=88=92=E6=81=A2=E5=A4=8D=E9=97=A8?= =?UTF-8?q?=E7=94=A8=E4=BE=8B=E6=94=B9=E5=BB=BA=E5=8F=AF=E8=BE=BE=E7=8A=B6?= =?UTF-8?q?=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit planning_recovery_contains_session_identity_conflict_before_provider 自 M1C-2b (152cc40c7)写下起一天都没绿过:它伪造一个 source=agent-background-task、且没有 Run Profile 绑定的策划任务,而 agent_runtime_tool_policy_snapshot_for_run_at 早在 M1A-2(09c7d7af8,152cc40c7 的祖先,已核对该门在 152cc40c7 当时就一模一样)就按 agent 身份挡死了这种 run。被告造不出来,庭开不了。当时的定向门禁过滤器 planning_clarification_* 恰好匹配不到它的名字,加上本 crate 不在 CI 里编译,两层 网都没接住。 那道创建门不能放宽:放宽后伪造的策划 run 就能拿到默认宽工具面,正是 M1A-2 关掉的 洞。所以改测试而不是改生产代码。 也不删:该用例第四条断言「身份门必须早于任何 Provider lifecycle」是全 crate 唯一 一处该不变量的否定断言(AGENT_RUNTIME_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE 共 22 处引用,否定断言仅此一处),且 plan_session_recovery_gate_tests 模块只有这一条 用例,删掉等于整个门失去覆盖。 先试了更贴现实的「事后删父绑定」,实测走不到本门:entrypoints 的 tool policy 收容 (currentAction=Run Profile 工具策略需要人工核对)先把 run 打成 failed, read_recoverable_... 随即不再认它,审计里只有 agent.runtime.turn。也就是说绑定层的 破坏到不了 session 门;能到这里的只有「绑定完好、task record 自身不满足 D11 契约」 这一类持久不一致——这正是本门存在的理由。 - 改成:supervisor plan 根与 planning child 绑定都合法(创建门放行),但裸 start 不带 task link,task record 上没有 parentAgentId/delegationId - 四条断言原样保留:resume 必须 Ok(收容不外溢)、该 run failed/needs-reconciliation 且 error 含 PLAN_SOURCE_PROFILE_MISMATCH、审计有 plan.session_recovery. needs_reconciliation、且没有任何 Provider lifecycle 记录 - 断言失败信息带上实际审计记录类型与 currentAction,下次判因不用再加探针 - 变异验证:把 exact_plan_child_identity_at 的策划识别翻成直接 Ok(None),用例报出 "PLAN_NEEDS_RECONCILIATION: project-planning 恢复任务未命中 planning session 协调器" ——证明它确实在考这道门,而不是碰巧绿 - recovery 107/0、planning_ 151/0、session 88/0、plan_ 160/0、approval 11/0 Co-Authored-By: Claude Opus 5 --- .../src/agent/runtime_driver/recovery_scan.rs | 72 ++++++++++++++++--- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs index 60b4ac18e..e8f816f9e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/recovery_scan.rs @@ -2176,6 +2176,21 @@ mod plan_gdd_approval_wait_recovery_tests { mod plan_session_recovery_gate_tests { use super::*; + /// 这条用例原来伪造一个 `source=agent-background-task`、且**没有 Run Profile + /// 绑定**的策划任务。但 `agent_runtime_tool_policy_snapshot_for_run_at` 早在 + /// `M1A-2`(`09c7d7af8`)就按 agent 身份挡死了这种 run——那道门是刻意的 + /// fail-closed 收窄(放宽它,伪造的策划 run 就能拿到默认宽工具面),于是被告 + /// 根本造不出来。用例自 `M1C-2b` 写下起一天都没绿过,只是当时的定向门禁过滤器 + /// (`planning_clarification_*`)恰好匹配不到它的名字。 + /// + /// 重写时试过「事后删掉父绑定」这种更贴现实的破坏方式,实测走不到本门:读路径 + /// 的 tool policy 收容(`entrypoints.rs` 里 `Run Profile 工具策略需要人工核对`) + /// 会先把 run 打成 failed,`read_recoverable_...` 随即不再认它。也就是说**绑定 + /// 层的破坏根本到不了 session 门**——能到这里的只有「绑定完好、task record 自身 + /// 不满足 D11 契约」这一类持久不一致,这正是本门存在的理由。 + /// + /// 所以改成:绑定全部合法(创建门放行),但 task record 缺 `delegationId` + /// (裸 start 不带 task link 时就是这个形状)。合同一字未改。 #[test] fn planning_recovery_contains_session_identity_conflict_before_provider() { let temporary = @@ -2183,13 +2198,40 @@ mod plan_session_recovery_gate_tests { let root = temporary.path(); init_local_game_project_at(root, "plan-recovery-gate", "策划恢复门冲突收敛") .expect("init project"); + let parent_run_id = "plan-session-recovery-conflict-root"; + let child_run_id = "plan-session-recovery-conflict-run"; + bind_game_creator_agent_runtime_run_profile_at( + root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + parent_run_id, + AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD), + None, + ) + .expect("bind supervisor plan root"); + bind_game_creator_agent_runtime_run_profile_at( + root, + GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, + child_run_id, + "agent-delegate", + Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD), + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()), + parent_run_id: Some(parent_run_id.to_string()), + delegation_id: Some("plan-session-recovery-conflict-delegation".to_string()), + }), + ) + .expect("bind delegated planning child"); + // 裸 start 的 task link 取自已排队的 record,这里没有,于是 task record 上 + // 的 parentRunId/delegationId 是空的——绑定说这是委派子 run,task record 却 + // 讲不出自己属于哪次委派。 start_game_creator_agent_runtime_task_at( root, GAME_CREATOR_PROJECT_PLANNING_AGENT_ID, - "不具备 Supervisor 委派身份的伪造策划任务", - "plan-session-recovery-conflict-run", - "agent-background-task", - "准备执行伪造策划任务", + "缺少委派链的策划任务", + child_run_id, + "agent-delegate", + "准备执行策划任务", vec!["不得发起 Provider 请求".to_string()], ) .expect("persist recoverable planning task"); @@ -2202,14 +2244,24 @@ mod plan_session_recovery_gate_tests { .expect("planning task is returned as contained"); assert_eq!(contained.state.status, "failed"); assert_eq!(contained.state.phase, "needs-reconciliation"); - assert!(contained - .state - .error - .as_deref() - .is_some_and(|error| error.contains("PLAN_SOURCE_PROFILE_MISMATCH"))); + let contained_error = contained.state.error.as_deref().unwrap_or_default(); + assert!( + contained_error.contains("PLAN_SOURCE_PROFILE_MISMATCH"), + "身份冲突必须落在 planning coordinator 的类型化合同里,实际为:{contained_error}" + ); let public_audit = fs::read_to_string(root.join(".agent/agent.db")).unwrap_or_default(); - assert!(public_audit.contains("agent.runtime.plan.session_recovery.needs_reconciliation")); + let record_types = public_audit + .split("\"recordType\"") + .skip(1) + .filter_map(|chunk| chunk.split('"').nth(1).map(str::to_string)) + .collect::>(); + assert!( + public_audit.contains("agent.runtime.plan.session_recovery.needs_reconciliation"), + "必须由 session 恢复门收容,实际审计记录类型:{record_types:?};\ + currentAction={}", + contained.state.current_action + ); assert!( !public_audit.contains(AGENT_RUNTIME_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE), "session identity 冲突必须在任何 Provider lifecycle 之前停止"