From 203ebba9b44683243857389d8801dcbddfcd9cdb Mon Sep 17 00:00:00 2001 From: Linghong Date: Tue, 11 Aug 2026 11:22:20 +0000 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E6=94=B6=E7=B4=A7?= =?UTF-8?q?=20game-chat=20=E7=BE=8E=E6=9C=AF=E9=87=8D=E8=AF=95=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E5=88=A4=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 入口结构身份改用不可变 binding 链,避免 task journal 缺失漏过 typed retry 守卫 无法完整证明的 retry-source 美术候选保持 mutation 失败关闭,同时保留完整 DAG 语义 补齐缺失 journal、无完整 binding、公开错误 kind 与全链非回归测试 --- .../agent/runtime_driver/main_loop_tests.rs | 97 +++++++++++++++- .../src/agent/runtime_tools/file_ops.rs | 105 +++++++++++------- 2 files changed, 156 insertions(+), 46 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs index 126c202cb..f26e6fa4d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop_tests.rs @@ -464,6 +464,27 @@ fn game_chat_dynamic_art_terminal_children_reject_generic_retry_without_durable_ .expect("persist terminal game-chat art child task"); write_game_creator_agent_runtime_state(&root, &child) .expect("persist terminal game-chat art child state"); + fs::remove_file(game_creator_agent_runtime_task_path( + &root, + "code-prototype", + )) + .expect("remove parent task journal without removing immutable binding lineage"); + fs::remove_file(game_creator_agent_runtime_task_path( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + )) + .expect("remove root task journal without removing immutable binding lineage"); + let terminal_child = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("read terminal game-chat art child") + .expect("terminal game-chat art child exists"); + assert!( + game_chat_dynamic_art_child_structural_identity_at(&root, &terminal_child) + .expect("immutable binding lineage still identifies terminal art child") + ); let durable_before = snapshot_agent_durable_files(&root); let retry_run_id = format!("blocked-game-chat-art-retry-{case_index}"); @@ -476,10 +497,7 @@ fn game_chat_dynamic_art_terminal_children_reject_generic_retry_without_durable_ .expect_err("game-chat dynamic art child retry must be rejected"); assert!( - error.contains(&format!( - "kind={}", - AgentRuntimeRetryErrorKind::GameChatDynamicArtRetryUnsupported.as_str() - )), + error.contains("kind=game-chat-dynamic-art-retry-unsupported"), "unexpected typed retry error: {error}" ); assert!(error.contains("code-prototype"), "{error}"); @@ -605,6 +623,58 @@ async fn legacy_game_chat_dynamic_art_retry_runs_are_read_only_and_fail_closed() .is_some()); assert!(!root.join("assets/retry-must-not-write.txt").exists()); assert!(!root.join("assets/retry-patchset.txt").exists()); + + fs::remove_file(game_creator_agent_runtime_task_path( + &root, + "code-prototype", + )) + .expect("remove parent task journal for forged legacy fixture"); + let forged_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + &retry.agent_id, + &retry.run_id, + ) + .expect("read forged art retry") + .expect("forged art retry exists"); + assert!( + game_chat_dynamic_art_child_structural_identity_at(&root, &forged_task) + .expect("immutable bindings still prove structural identity") + ); + assert!( + !game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( + &root, + &retry.agent_id, + &retry.run_id, + AGENT_RUNTIME_ART_SPRITESHEET_PATH, + ) + .expect("missing parent task cannot prove strict authorization"), + "retry without a live parent task must not acquire strict authorization" + ); + let forged_action = AgentRuntimeToolAction { + tool: "file.write".to_string(), + reason: None, + input: serde_json::json!({ + "path":"assets/forged-retry-must-not-write.txt", + "content":"blocked", + }), + }; + let forged_observation = execute_game_creator_agent_runtime_tool_action( + &root, + &retry.agent_id, + &retry.run_id, + &retry.current_task, + &forged_action, + ) + .await; + assert_eq!( + forged_observation.status, "blocked", + "{forged_observation:?}" + ); + assert!( + forged_observation.summary.contains("不支持通用 retry"), + "{forged_observation:?}" + ); + assert!(!root.join("assets/forged-retry-must-not-write.txt").exists()); } #[test] @@ -708,6 +778,25 @@ fn game_chat_dynamic_art_retry_guard_preserves_dag_art_and_non_art_delegated_ret &serde_json::json!({"path":AGENT_RUNTIME_ART_SPEC_PATH,"content":"unchanged semantics"}), ) .is_none()); + + fs::remove_file(game_creator_agent_runtime_run_profile_binding_path( + &dag_root, + "art-director", + "full-dag-art-retry-run", + )) + .expect("remove full DAG retry binding for unproven-lineage fixture"); + let unproven_block = game_chat_delegated_art_agent_project_path_mutation_block( + &dag_root, + "art-director", + "full-dag-art-retry-run", + "file.write", + "assets/unproven-retry-must-not-write.txt", + ) + .expect("unproven retry-source art lineage must fail closed"); + assert!( + unproven_block.summary.contains("不支持通用 retry"), + "{unproven_block:?}" + ); drop(dag_art_lane); let delegated_temporary = tempfile::tempdir().expect("create non-art delegated retry root"); diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs index e8f676dbb..a3f45eff8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs @@ -12,6 +12,10 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at( task: &AgentRuntimeTaskRecord, ) -> Result { if !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan") + || !matches!( + task.source.as_str(), + "agent-delegate" | "agent-delegate-retry" + ) || task.parent_agent_id.as_deref() != Some("code-prototype") { return Ok(false); @@ -28,14 +32,6 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at( else { return Ok(false); }; - let Some(parent) = read_latest_game_creator_agent_runtime_task_by_run_id( - root, - "code-prototype", - parent_run_id, - )? - else { - return Ok(false); - }; let Some(parent_binding) = read_game_creator_agent_runtime_run_profile_binding(root, "code-prototype", parent_run_id)? else { @@ -49,14 +45,6 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at( else { return Ok(false); }; - let Some(root_task) = read_latest_game_creator_agent_runtime_task_by_run_id( - root, - &root_binding.agent_id, - &root_binding.run_id, - )? - else { - return Ok(false); - }; Ok(child_binding.agent_id == task.agent_id && child_binding.run_id == task.run_id @@ -67,20 +55,13 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at( && child_binding.parent_run_id == task.parent_run_id && child_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && child_binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - && parent.agent_id == "code-prototype" - && parent.run_id == parent_run_id - && parent.source == "agent-ready-task-scheduler" - && parent.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - && parent.parent_agent_id.as_deref() == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) - && parent.parent_run_id.as_deref() == Some(child_binding.root_run_id.as_str()) - && parent.delegation_id.is_none() - && parent_binding.agent_id == parent.agent_id - && parent_binding.run_id == parent.run_id - && parent_binding.source == parent.source - && parent_binding.profile == parent.run_profile - && parent_binding.binding_fingerprint == parent.run_profile_binding_fingerprint - && parent_binding.parent_agent_id == parent.parent_agent_id - && parent_binding.parent_run_id == parent.parent_run_id + && parent_binding.agent_id == "code-prototype" + && parent_binding.run_id == parent_run_id + && parent_binding.source == "agent-ready-task-scheduler" + && parent_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && parent_binding.parent_agent_id.as_deref() + == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) + && parent_binding.parent_run_id.as_deref() == Some(child_binding.root_run_id.as_str()) && parent_binding.project_id == child_binding.project_id && parent_binding.root_agent_id == child_binding.root_agent_id && parent_binding.root_run_id == child_binding.root_run_id @@ -95,16 +76,58 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at( && root_binding.root_run_id == root_binding.run_id && root_binding.parent_agent_id.is_none() && root_binding.parent_run_id.is_none() + && root_binding.parent_binding_fingerprint.is_none() && parent_binding.parent_binding_fingerprint.as_deref() - == Some(root_binding.binding_fingerprint.as_str()) - && root_task.agent_id == root_binding.agent_id - && root_task.run_id == root_binding.run_id - && root_task.source == root_binding.source - && root_task.run_profile == root_binding.profile - && root_task.run_profile_binding_fingerprint == root_binding.binding_fingerprint - && root_task.parent_agent_id.is_none() - && root_task.parent_run_id.is_none() - && root_task.delegation_id.is_none()) + == Some(root_binding.binding_fingerprint.as_str())) +} + +fn game_chat_dynamic_art_retry_claims_game_chat_lineage_at( + root: &Path, + task: &AgentRuntimeTaskRecord, +) -> Result { + if task.source != "agent-delegate-retry" + || !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan") + { + return Ok(false); + } + if task.parent_agent_id.as_deref() == Some("code-prototype") { + return Ok(true); + } + let child_binding = match read_game_creator_agent_runtime_run_profile_binding( + root, + &task.agent_id, + &task.run_id, + ) { + Ok(Some(binding)) => binding, + // A retry-source art task without a complete immutable binding chain + // cannot be downgraded to ordinary unrestricted art permissions. + Ok(None) | Err(_) => return Ok(true), + }; + if child_binding.agent_id != task.agent_id + || child_binding.run_id != task.run_id + || child_binding.source != task.source + || child_binding.profile != task.run_profile + || child_binding.binding_fingerprint != task.run_profile_binding_fingerprint + || child_binding.parent_agent_id != task.parent_agent_id + || child_binding.parent_run_id != task.parent_run_id + { + return Ok(true); + } + if child_binding.parent_agent_id.as_deref() == Some("code-prototype") { + return Ok(true); + } + let child_root_binding = match read_game_creator_agent_runtime_run_profile_binding( + root, + &child_binding.root_agent_id, + &child_binding.root_run_id, + ) { + Ok(Some(binding)) => binding, + Ok(None) | Err(_) => return Ok(true), + }; + Ok( + child_root_binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + && child_root_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, + ) } fn game_chat_delegated_art_agent_mutation_scope_at( @@ -120,9 +143,7 @@ fn game_chat_delegated_art_agent_mutation_scope_at( if let Some(task) = read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)? { - if task.source == "agent-delegate-retry" - && game_chat_dynamic_art_child_structural_identity_at(root, &task)? - { + if game_chat_dynamic_art_retry_claims_game_chat_lineage_at(root, &task)? { return Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked); } }