diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs index 51e452b0f..0014dc61a 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_request_builders.rs @@ -288,7 +288,8 @@ fn build_game_creator_agent_background_tool_plan_request_at( .with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low) .with_function_tools(function_tools) .with_tool_choice(platform_llm::LlmToolChoice::Required); - let request = apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false); + let request = + apply_game_creator_llm_reasoning_effort(request, &llm)?.with_web_search(false); return Ok(( llm, config_path, @@ -1330,7 +1331,8 @@ mod tests { let system_prompt = &request.messages[0].content; let user_prompt = &request.messages[1].content; assert!(system_prompt.contains("自主执行 Agent")); - assert!(system_prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件")); + assert!(system_prompt + .contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件")); assert!(user_prompt.contains("依赖只作为参考")); assert!(user_prompt.contains("不要等待或索要平台资产/验收回执")); assert!(!user_prompt.contains("固定 owner 收束协议")); @@ -1383,31 +1385,50 @@ mod tests { AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, "code-prototype", ); - for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] { + for tool in [ + "project.verify", + "command.run_limited", + "preview.start", + "preview.validate", + ] { assert!(!request_advertises_native_tool(&code, tool)); } assert!(request_advertises_native_tool(&code, "file.write")); assert!(code.messages[0].content.contains("自主执行 Agent")); - assert!(code.messages[1].content.contains("不要等待或索要平台资产/验收回执")); + assert!(code.messages[1] + .content + .contains("不要等待或索要平台资产/验收回执")); let readiness = build_autonomous_ready_child_request( "preview-readiness", AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, "preview-readiness", ); - for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] { + for tool in [ + "project.verify", + "command.run_limited", + "preview.start", + "preview.validate", + ] { assert!(!request_advertises_native_tool(&readiness, tool)); } assert!(request_advertises_native_tool(&readiness, "file.read")); assert!(readiness.messages[0].content.contains("自主执行 Agent")); - assert!(readiness.messages[1].content.contains("不要等待或索要平台资产/验收回执")); + assert!(readiness.messages[1] + .content + .contains("不要等待或索要平台资产/验收回执")); let publish = build_autonomous_ready_child_request( "publish-package", AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, "publish-package", ); - for tool in ["project.verify", "command.run_limited", "preview.start", "preview.validate"] { + for tool in [ + "project.verify", + "command.run_limited", + "preview.start", + "preview.validate", + ] { assert!(!request_advertises_native_tool(&publish, tool)); } assert!(request_advertises_native_tool(&publish, "file.write")); @@ -1482,9 +1503,8 @@ mod tests { assert!(prompts.contains("自主执行 Agent")); assert!(prompts.contains("依赖只作为参考")); assert!(!prompts.contains("非只读视觉规范生成任务")); - assert!(!prompts.contains( - crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER - )); + assert!(!prompts + .contains(crate::agent::AGENT_RUNTIME_AUTONOMOUS_ART_DIRECTOR_CANVAS_ONLY_TASK_MARKER)); assert!(!prompts.contains("会同时提交当前 run 的 mutation 与验证凭证")); assert!(!prompts.contains("无生图凭据只读协调任务")); } @@ -1527,7 +1547,10 @@ mod tests { // The autonomous execution marker lives in the system message; the // user message carries only the task-specific runtime context. let prompt = &request.messages[0].content; - assert!(prompt.contains("自主执行 Agent"), "unexpected relaxed root prompt: {prompt}"); + assert!( + prompt.contains("自主执行 Agent"), + "unexpected relaxed root prompt: {prompt}" + ); assert!(prompt.contains("不要把流程合同、固定 owner、DAG 顺序或平台产物当作启动条件")); assert!(request.function_tools.len() > 1); for tool in [ diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs index b2bf942d2..45571d8c9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs @@ -817,12 +817,10 @@ fn autonomous_root_goal_contract_persisted_for_binding_at( root: &Path, binding: &AgentRuntimeRunProfileBinding, ) -> Result { - Ok(read_game_creator_agent_runtime_goal_contract_at( - root, - &binding.agent_id, - &binding.run_id, - )? - .is_some()) + Ok( + read_game_creator_agent_runtime_goal_contract_at(root, &binding.agent_id, &binding.run_id)? + .is_some(), + ) } /// Return whether the trusted autonomous root has a valid, persisted Goal @@ -1099,11 +1097,7 @@ fn queue_or_recover_autonomous_manifest_ready_task_at( result.accepted_run_id = Some(record.run_id.clone()); return Ok((result, (*record).clone(), true)); } - let run_id = format!( - "autonomous-ready-{}-{}", - task.id, - unix_timestamp_nanos() - ); + let run_id = format!("autonomous-ready-{}-{}", task.id, unix_timestamp_nanos()); // Keep parent metadata as an optional correlation hint. Relaxed // orchestration never uses it as a readiness/identity gate, but it lets // the DAG state query distinguish this root's child from an older run. @@ -1222,16 +1216,13 @@ pub(crate) fn schedule_autonomous_game_build_ready_tasks_at( // status change makes subsequent passes ignore it. match existing { None => candidates.push((task.clone(), false)), - Some(record) - if game_creator_agent_runtime_terminal_status(&record).is_some() => - { + Some(record) if game_creator_agent_runtime_terminal_status(&record).is_some() => { terminal_records.push(record); } Some(_) => {} } } - for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks, &parent_binding.source) - { + for task_id in autonomous_manifest_ready_task_ids(&manifest.tasks, &parent_binding.source) { if let Some(task) = manifest.tasks.iter().find(|task| task.id == task_id) { candidates.push((task.clone(), true)); } @@ -1504,7 +1495,9 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke let manifest = read_manifest_for_project(root)?; let task_id = [state.task_id.trim(), state.agent_id.trim()] .into_iter() - .find(|candidate| !candidate.is_empty() && manifest.tasks.iter().any(|task| task.id == *candidate)); + .find(|candidate| { + !candidate.is_empty() && manifest.tasks.iter().any(|task| task.id == *candidate) + }); let Some(task_id) = task_id else { // A relaxed child that is not a manifest task is an ordinary // Runtime run; it must not be guessed into the task graph. @@ -1571,9 +1564,7 @@ pub(in crate::agent) fn project_autonomous_manifest_ready_task_terminal_at_locke { let status = match state.phase.as_str() { "completed" => GameCreationAppTaskStatus::Completed, - "failed" | "cancelled" | "budget-exhausted" => { - GameCreationAppTaskStatus::Failed - } + "failed" | "cancelled" | "budget-exhausted" => GameCreationAppTaskStatus::Failed, _ => return Ok(false), }; let manifest = read_manifest_for_project(root)?; @@ -1775,7 +1766,8 @@ pub(super) fn autonomous_manifest_ready_task_requires_visual_asset(task_id: &str fn render_autonomous_manifest_ready_task_owner_prompt(task: &GameCreationAppTaskState) -> String { let base = render_manifest_ready_task_background_prompt(task); let paths = autonomous_manifest_owner_artifact_paths(&task.id).join(", "); - let visual_usage_requirement = if task.id == "code-prototype" && editor_api_key_is_configured() { + let visual_usage_requirement = if task.id == "code-prototype" && editor_api_key_is_configured() + { "本轮必须实际接入已登记的平台美术切片:先用 asset.list 读取 assets/art-spritesheet-slices/manifest.json,再在 game/index.html 的可见 canvas 主循环中为 player、blocks-and-targets、obstacles-and-scene、feedback-effects 四个切片分别创建 Image 并用相对路径加载;在 requestAnimationFrame 绘制中对每个已加载切片调用 ctx.drawImage(image, dx, dy, dw, dh) 或九参数裁剪形式,目标区域必须可见且至少 32×32。只放置 /、只展示整张 assets/art-spritesheet.png、只写路径或只在注释中引用都不满足完成合同。" } else { "" @@ -1981,10 +1973,9 @@ mod tests { "art-director", &prompt )); - let art_asset_prompt = - render_autonomous_manifest_ready_task_background_prompt(&seed_task( - "art-asset-plan", - )); + let art_asset_prompt = render_autonomous_manifest_ready_task_background_prompt( + &seed_task("art-asset-plan"), + ); assert!(art_asset_prompt.contains("canvas.asset_generate")); assert!(art_asset_prompt.contains("asset.list")); assert!(art_asset_prompt.contains("file.write 写入 assets/manifest.art.json")); diff --git a/apps/ai-game-creator-shell/src-tauri/src/config.rs b/apps/ai-game-creator-shell/src-tauri/src/config.rs index d7abce6cf..fdec6a901 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/config.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/config.rs @@ -16,8 +16,9 @@ struct UserSelectedPathGrant { } #[cfg(windows)] -static GAME_CREATOR_USER_SELECTED_PATH_GRANTS: - OnceLock>> = OnceLock::new(); +static GAME_CREATOR_USER_SELECTED_PATH_GRANTS: OnceLock< + Mutex>, +> = OnceLock::new(); #[cfg(windows)] fn user_selected_path_grants() -> &'static Mutex> { @@ -150,6 +151,19 @@ fn build_game_creator_platform_llm_config( if game_creator_official_llm_route_locked() { return build_game_creator_official_platform_llm_config(llm); } + #[cfg(not(test))] + { + return Err("AGC 正式运行只允许通过 API Server 使用官方 LLM Router".to_string()); + } + #[cfg(test)] + build_game_creator_test_platform_llm_config(llm, config_path) +} + +#[cfg(test)] +fn build_game_creator_test_platform_llm_config( + llm: &GameCreatorLlmConfig, + config_path: &str, +) -> Result { let api_kind = validate_game_creator_llm_web_search_config(llm, config_path)?; let api_key = trim_config_string(&llm.api_key).ok_or_else(|| llm_api_key_config_error(config_path))?; @@ -3244,7 +3258,7 @@ pub(crate) fn configure_game_creator_runtime_config_dir( .map_err(std::io::Error::other)?; } // Both the normal config and the optional local override are persisted - // inputs. A release build must scrub legacy provider credentials from + // inputs. Every real AGC build scrubs legacy provider credentials from // either file before the next read can observe them again. for path in [ config_path, @@ -3336,7 +3350,6 @@ pub(crate) fn load_game_creator_app_config() -> Result bool { let mut changed = config.agent_mode.as_deref() != Some(GAME_CREATOR_AGENT_MODE_CODEX_APP_SERVER) @@ -3431,8 +3444,19 @@ pub(crate) fn migrate_legacy_game_creator_agent_mode(path: &Path) -> Result<(), Ok(()) } +/// Returns whether a real AGC build must use the authenticated API Server +/// proxy instead of any persisted provider credentials. +/// +/// Debug and release binaries intentionally share this decision. The only +/// exception is the Rust unit-test build, where existing tests may install an +/// explicit loopback provider fixture; that fixture is never compiled into a +/// shipped binary and is not a runtime fallback. pub(crate) fn game_creator_official_llm_route_locked() -> bool { - !cfg!(debug_assertions) && !cfg!(test) && editor_api_mode() == EditorApiMode::PlatformAccount + game_creator_official_llm_route_locked_for_build(cfg!(test)) +} + +pub(crate) fn game_creator_official_llm_route_locked_for_build(is_test_build: bool) -> bool { + !is_test_build } pub(crate) fn lock_game_creator_app_config_to_official_route(config: &mut GameCreatorAppConfig) { diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs index 420091372..21336885b 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/configuration.rs @@ -99,7 +99,8 @@ fn config_file_overrides_defaults_without_env() { assert_eq!(config.editor_api.api_key, "editor-key"); fs::remove_dir_all(root).expect("cleanup test config dir"); -}#[test] +} +#[test] fn agent_mode_defaults_to_codex_app_server_and_preserves_explicit_modes() { let default_config = GameCreatorAppConfig::default(); assert_eq!( @@ -276,7 +277,7 @@ fn unsupported_config_schema_version_fails_closed() { } #[test] -fn locked_release_config_scrub_removes_all_legacy_provider_credentials() { +fn locked_config_scrub_removes_all_legacy_provider_credentials() { let mut config: GameCreatorAppConfigFile = serde_json::from_value(serde_json::json!({ "agentMode": "provider", "llm": { @@ -323,6 +324,12 @@ fn locked_release_config_scrub_removes_all_legacy_provider_credentials() { assert!(!serialized.contains("legacy.example.test")); } +#[test] +fn official_llm_route_is_locked_for_debug_and_release_platform_builds() { + assert!(game_creator_official_llm_route_locked_for_build(false)); + assert!(!game_creator_official_llm_route_locked_for_build(true)); +} + #[test] fn codex_app_server_requires_responses_route_and_disables_native_web_search() { let mut llm = GameCreatorLlmConfig::default(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_game_build.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_game_build.rs index 5f70c017e..3aa224d0d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_game_build.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_game_build.rs @@ -745,7 +745,10 @@ async fn autonomous_game_build_non_read_only_code_first_round_repairs_response_i .expect("repair first-round response") .expect("first-round mutation plan"); if !plan.response.is_empty() { - assert!(plan.actions.is_empty(), "relaxed autonomous plan must choose response or actions"); + assert!( + plan.actions.is_empty(), + "relaxed autonomous plan must choose response or actions" + ); return; } assert_eq!(plan.actions.len(), 1); @@ -876,7 +879,10 @@ async fn autonomous_game_build_unverified_mutation_immediately_repairs_into_veri .expect("repair immediate unverified completion") .expect("verification-only plan"); if !plan.response.is_empty() { - assert!(plan.actions.is_empty(), "relaxed autonomous plan may complete directly after a mutation"); + assert!( + plan.actions.is_empty(), + "relaxed autonomous plan may complete directly after a mutation" + ); return; } assert_eq!(plan.actions.len(), 1); @@ -1026,7 +1032,10 @@ async fn autonomous_manifest_code_prototype_requires_its_own_static_smoke_after_ .expect("repair project.verify-only delivery") .expect("static-smoke repair plan"); if !repair_plan.response.is_empty() { - assert!(repair_plan.actions.is_empty(), "relaxed autonomous plan may report a valid response directly"); + assert!( + repair_plan.actions.is_empty(), + "relaxed autonomous plan may report a valid response directly" + ); return; } assert_eq!(repair_plan.actions.len(), 1); @@ -2241,7 +2250,10 @@ async fn autonomous_game_build_repairs_new_revision_after_failed_playtest_with_v .expect("repair new revision revalidation stall") .expect("repaired supervisor verification plan"); assert_eq!(plan.actions.len(), 1); - assert!(matches!(plan.actions[0].tool.as_str(), "command.run_limited" | "project.index" | "project.verify")); + assert!(matches!( + plan.actions[0].tool.as_str(), + "command.run_limited" | "project.index" | "project.verify" + )); if plan.actions[0].tool != "command.run_limited" { return; } @@ -2511,7 +2523,10 @@ async fn autonomous_game_build_claims_ready_delivery_before_fourth_playtest_dele .expect("repair full delivery convergence") .expect("ready delivery claim plan"); assert_eq!(plan.actions.len(), 1); - assert!(matches!(plan.actions[0].tool.as_str(), "agent.run_status" | "agent.delegate")); + assert!(matches!( + plan.actions[0].tool.as_str(), + "agent.run_status" | "agent.delegate" + )); if plan.actions[0].tool != "agent.run_status" { return; } @@ -3020,7 +3035,10 @@ async fn autonomous_game_build_repairs_post_mutation_read_loop_into_verification .expect("repair post-mutation read loop") .expect("repaired verification plan"); assert_eq!(plan.actions.len(), 1); - assert!(matches!(plan.actions[0].tool.as_str(), "project.verify" | "project.index")); + assert!(matches!( + plan.actions[0].tool.as_str(), + "project.verify" | "project.index" + )); if plan.actions[0].tool != "project.verify" { return; } @@ -3164,7 +3182,10 @@ async fn autonomous_game_build_repairs_pre_mutation_read_loop_into_action() { .expect("repair pre-mutation read loop") .expect("repaired autonomous action plan"); assert_eq!(plan.actions.len(), 1); - assert!(matches!(plan.actions[0].tool.as_str(), "file.write" | "project.index")); + assert!(matches!( + plan.actions[0].tool.as_str(), + "file.write" | "project.index" + )); if plan.actions[0].tool != "file.write" { return; } @@ -3375,7 +3396,10 @@ async fn autonomous_game_build_repairs_truncated_scaffold_into_bounded_patch() { .expect("repair truncated autonomous scaffold") .expect("repaired autonomous patch plan"); assert_eq!(plan.actions.len(), 1); - assert!(matches!(plan.actions[0].tool.as_str(), "file.patch" | "file.write")); + assert!(matches!( + plan.actions[0].tool.as_str(), + "file.patch" | "file.write" + )); if plan.actions[0].tool != "file.patch" { return; }