diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs index 691463586..3e06d06ff 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions.rs @@ -154,6 +154,7 @@ pub(crate) use tool_plan_protocol::{ pub(crate) use tool_policy_snapshot::{ agent_runtime_acceptance_evidence_tools, agent_runtime_autonomous_design_foundation_command_is_allowed, agent_runtime_executable_tools, - agent_runtime_native_executable_tools, agent_runtime_tool_policy_snapshot_for_run_at, - AGENT_RUNTIME_CANVAS_ASSET_KINDS, AGENT_RUNTIME_PROJECT_PLANNING_ACTION_TOOLS, + agent_runtime_native_executable_tools, agent_runtime_plan_root_supervisor_tools, + agent_runtime_tool_policy_snapshot_for_run_at, AGENT_RUNTIME_CANVAS_ASSET_KINDS, + AGENT_RUNTIME_PROJECT_PLANNING_ACTION_TOOLS, }; 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 66f087bf6..3ab974f91 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 @@ -553,6 +553,7 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( )?) .with_tool_choice(platform_llm::LlmToolChoice::Required); if plan_root { + retain_plan_root_supervisor_native_tools(&mut request.function_tools)?; restrict_plan_root_goal_contract_schema(&mut request.function_tools)?; } if runtime_owner_artifact_validation_available { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs index c625cee0a..4fc25f655 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs @@ -1284,6 +1284,14 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at "上一条输出不符合工具计划协议:{protocol_error}\n请修复格式,只调用当前请求广告的 update_agent_plan、动作工具或 respond_to_user;当前 in_progress 步骤已具备执行条件时,格式修复必须保留并调用对应动作工具,不能退化为只调用 update_agent_plan。不要解释,不要 markdown,不要代码围栏,也不要把计划、动作或回复放进普通文本。" ))); } + // 修复分支会先整份重建工具目录,再按各自的场景收窄。plan 根必须在 + // 所有分支收窄之后再取一次交集:早于分支就会让 + // `restrict_agent_runtime_supervisor_collaboration_repair_tools` + // 这类「必须包含 agent.spawn_isolated」的检查硬失败,晚于分支则 + // 保证任何修复轮都不会把被裁掉的 36 个工具重新广告回去。 + if plan_root { + retain_plan_root_supervisor_native_tools(&mut request.function_tools)?; + } request.enable_web_search = false; } Err(error) => { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs index 5a588037f..e15378549 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/tool_policy_snapshot.rs @@ -111,6 +111,30 @@ pub(crate) fn agent_runtime_executable_tools() -> Vec<&'static str> { ] } +/// plan 根 Supervisor(`source == project-supervisor-plan`)在整条策划链路里 +/// 只负责四件事:冻结 Goal Contract、委派与续跑 `project-planning`、代子 Agent +/// 向用户提问、按 §13.0 取证后建审批卡。策划内容全部由子 Agent 生产,Supervisor +/// 不写文件、不跑命令、不做预览、不生成素材、不调度 ready 任务、不并行委派。 +/// +/// 全量注册表会把 43 个原生工具摆在 Provider 眼前,其中约 36 个在 plan 根都会被 +/// 执行层拒绝——广告出去只会诱导 Supervisor 自己下场干活。这里给出 plan 根的 +/// exact allowlist,Provider 请求目录和 system prompt 的工具清单共用它,二者不得 +/// 各自维护一份。 +/// +/// `file.read`、`agent.acceptance_update`、`agent.action_history` 只为 §13.0 的审批 +/// 前置取证门存在(分页读 `game/fast_gdd.md` 并列出全部分页 actionId)。 +pub(crate) fn agent_runtime_plan_root_supervisor_tools() -> &'static [&'static str] { + &[ + "user.input_request", + "file.read", + "agent.delegate", + "agent.goal_contract", + "agent.acceptance_update", + "agent.action_history", + "agent.run_status", + ] +} + pub(crate) fn agent_runtime_native_executable_tools() -> Vec<&'static str> { agent_runtime_executable_tools() .into_iter() diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs index 57b49828f..3debb1f11 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent_native_tools.rs @@ -10,11 +10,12 @@ use serde_json::{json, Value}; use sha2::{Digest, Sha256}; use crate::agent::{ - agent_runtime_native_executable_tools, AgentRuntimePlanUpdate, AgentRuntimeToolAction, - AgentRuntimeToolPlan, AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT, - AGENT_RUNTIME_CANVAS_ASSET_KINDS, AGENT_RUNTIME_PLAN_STEP_LIMIT, - PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION, PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE, - PLAN_FAST_GDD_ACCEPTANCE_NODE_ID, PLAN_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL, + agent_runtime_native_executable_tools, agent_runtime_plan_root_supervisor_tools, + AgentRuntimePlanUpdate, AgentRuntimeToolAction, AgentRuntimeToolPlan, + AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT, AGENT_RUNTIME_CANVAS_ASSET_KINDS, + AGENT_RUNTIME_PLAN_STEP_LIMIT, PLAN_FAST_GDD_ACCEPTANCE_NODE_CRITERION, + PLAN_FAST_GDD_ACCEPTANCE_NODE_EVIDENCE, PLAN_FAST_GDD_ACCEPTANCE_NODE_ID, + PLAN_SUBMIT_GDD_INPUT_SCHEMA_VERSION, PLAN_SUBMIT_GDD_TOOL, }; use crate::mcp::{ validate_game_creator_mcp_tool_arguments, GameCreatorMcpCatalog, GameCreatorMcpCatalogTool, @@ -360,6 +361,38 @@ pub(crate) fn build_agent_runtime_native_function_tools_for_agent( Ok(functions) } +/// Narrow the plan root Supervisor's advertised function catalog down to +/// `agent_runtime_plan_root_supervisor_tools()` plus the two protocol controls. +/// +/// This is an intersection, not an assertion: the caller may already have +/// narrowed the catalog further for a protocol-repair turn (for example +/// `restrict_agent_runtime_supervisor_collaboration_repair_tools`), and this +/// pass must never widen it back. MCP function tools carry a different prefix +/// and are dropped here too — the plan lane never calls MCP. +/// +/// An empty result means the repair-branch allowlist and the plan-root +/// allowlist are disjoint, which would send a request with no callable tool at +/// all; that is a configuration error, so fail closed instead. +pub(crate) fn retain_plan_root_supervisor_native_tools( + functions: &mut Vec, +) -> Result<(), String> { + let mut allowed = BTreeSet::from([ + AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME.to_string(), + AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string(), + ]); + for tool in agent_runtime_plan_root_supervisor_tools() { + allowed.insert( + native_runtime_function_name(tool) + .ok_or_else(|| format!("无法生成 plan 根 Supervisor 工具函数名:{tool}"))?, + ); + } + functions.retain(|function| allowed.contains(&function.name)); + if functions.is_empty() { + return Err("plan 根 Supervisor 工具目录收窄后为空".to_string()); + } + Ok(()) +} + /// Narrow only the request-scoped Goal Contract schema used by the plan root. /// The capability registry itself must remain dynamic: game-chat and ordinary /// Supervisor runs still author their own acceptance graph. @@ -2101,6 +2134,45 @@ mod tests { assert!(description.contains("runId 必须为 null")); } + /// plan 根 Supervisor 只广告 7 个原生工具 + 2 个协议控制。全量注册表里其余 + /// 约 36 个在这条链路上全部会被执行层拒绝,广告出去只会诱导 Supervisor 自己 + /// 下场写文件、跑命令、查任务图。断言写成精确集合而不是「不包含某几个」, + /// 这样将来往注册表里加工具不会静默漏进 plan 根。 + #[test] + fn plan_root_supervisor_tool_catalog_is_an_exact_allowlist() { + let mcp_catalog = + native_mcp_catalog(json!({"type": "object", "additionalProperties": false})); + let mut functions = build_agent_runtime_native_function_tools_for_agent( + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &mcp_catalog, + ) + .expect("build supervisor catalog"); + let before = functions.len(); + retain_plan_root_supervisor_native_tools(&mut functions).expect("retain plan root tools"); + let mut expected = vec![ + AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME.to_string(), + AGENT_RUNTIME_RESPOND_FUNCTION_NAME.to_string(), + ]; + for tool in agent_runtime_plan_root_supervisor_tools() { + expected.push(native_runtime_function_name(tool).expect("plan root function name")); + } + expected.sort(); + let mut actual = functions + .iter() + .map(|function| function.name.clone()) + .collect::>(); + actual.sort(); + assert_eq!(actual, expected); + assert!( + before > functions.len(), + "收窄必须真的裁掉工具,否则这条测试是空跑" + ); + // MCP 前缀的动态工具同样不得残留:plan 根整条链路不调 MCP。 + assert!(!functions + .iter() + .any(|function| function.name.starts_with(AGENT_RUNTIME_NATIVE_MCP_PREFIX))); + } + #[test] fn native_goal_contract_and_acceptance_update_expose_dynamic_graph_contract() { let goal = runtime_tool_input_schema("agent.goal_contract");