diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs index 1f3c3d5cd..ebb9aad8e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs @@ -488,14 +488,14 @@ fn game_creator_design_foundation_tool_plan_prompt( prompt: &str, editor_api_key_is_configured: bool, ) -> String { - let role_boundary = "角色边界:项目文件写入只允许 memory/project.md 与 game/game_design.md;配置 External Editor API Key 且任务要求界面原型时,可额外产出指定的 assets/ui-prototype.png。不得创建、修改、删除或补丁 game/index.html,也不得改动任何其他程序实现、发布、音频或美术素材文件。可以调用 command.run_limited 的 game.static_smoke 提交当前 revision 的只读静态验证;不得调用 preview.start、preview.validate,也不得通过 command.exec、command.start 或其他工具启动本地预览服务、浏览器、Playwright,或执行任何桌面端、移动端试玩验证;这些集成验证必须交由程序或质量 Agent 完成。"; + let role_boundary = "角色边界:项目文件写入只允许 memory/project.md 与 game/game_design.md;配置 External Editor API Key 且任务要求界面原型时,可额外产出指定的 assets/ui-prototype.png。不得创建、修改、删除或补丁 game/index.html,也不得改动任何其他程序实现、发布、音频或美术素材文件。完成固定正式产物后直接交付,由 Runtime 在收束门内验证本人 owner 产物;不得调用 project.verify、command.run_limited、game.static_smoke、preview.start 或 preview.validate,也不得通过 command.exec、command.start 或其他工具启动本地预览服务、浏览器、Playwright,或执行任何桌面端、移动端试玩验证。完整 DAG 的最终静态验收仍属于 preview-readiness,浏览器验收仍属于 preview-playtest。"; if !editor_api_key_is_configured { return format!( - "{prompt}\n\n你负责玩法规格与界面原型基础交付。当前未配置 External Editor API Key,因此本轮必须完成 memory/project.md 与 game/game_design.md,不调用 canvas.asset_generate,也不伪造 assets/ui-prototype.png。把界面结构、控件、状态和双视口要求写进玩法规格,供后续程序组直接实现;完成写入并通过当前 revision 的非浏览器验证后即可交付。{role_boundary}" + "{prompt}\n\n你负责玩法规格与界面原型基础交付。当前未配置 External Editor API Key,因此本轮必须完成 memory/project.md 与 game/game_design.md,不调用 canvas.asset_generate,也不伪造 assets/ui-prototype.png。把界面结构、控件、状态和双视口要求写进玩法规格,供后续程序组直接实现;完成写入后直接交付,不要自行运行任何验证命令。{role_boundary}" ); } format!( - "{prompt}\n\n你负责玩法规格与界面原型交付。玩法类型和机制描述不代表用户授权复刻现有游戏;必须先为项目创造原创标题、实体、资源、目标名称与视觉语言,并在 memory/project.md、game/game_design.md 和图片提示中保持一致。不得沿用或近似改写知名游戏单位、角色、Logo、界面术语或受保护视觉语言。文本策划只是中间结果;最终必须先用 asset.list 确认 assets/art-spec.png 已登记为当前项目的 icon-spec 画布资源,再调用 canvas.asset_generate 生成 16:9、2K 横屏界面原型图并登记到 assets/ui-prototype.png,assetKind=ui-prototype、assetLabel=游戏横屏界面原型图、replaceExisting=false。图片 prompt 必须逐项继承当前任务和 game/game_design.md 的真实玩法、HUD、可玩区域、关键实体、主要操作、失败/重开与移动端触控要求;不得假设为塔防或补入合同中不存在的单位卡牌、费用、波次、敌人入口等结构。Runtime 固定把规范图资源作为 referenceImageSrcs 第一项,调用 External Editor v1 的 POST /api/external/v1/editor/images/generations(kind=ui-design);不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions,后者只用于从已有且带标注的 UI 设计图提取独立透明 UI 素材。缺少规范图时必须等待 art-director 依赖并如实阻塞,不得回退为无规范参考的普通生图。canvas.asset_generate 成功动作本身就是当前 revision 的验证。已有同路径画布资产时先核对登记,再在当前 run 对且只对 assets/ui-prototype.png 调用 image.inspect;检查已通过时不得重复生成或再次扣费。只有 ui-prototype.v2 的 informationHud、gameplaySurface、objectiveEntities、primaryControls、failureRestartFlow、responsiveLayout、implementationClarity、originalTheme 八项检查全部通过才可完成。纯场景图、概念图、地图、海报或只有角色而没有可玩界面的画面都不是 UI 原型。视觉检查未通过时不得提交最终回复;只有任务正文明确标识这是带 repairOfDelegationId 的唯一返工轮时,才可使用固定输出合同和 replaceExisting=true 原位替换旧候选;不得先删除正式图片。图片生成未配置、待确认或失败时同样不得提交最终回复,也不得把计划写完当成 completed。{role_boundary}" + "{prompt}\n\n你负责玩法规格与界面原型交付。玩法类型和机制描述不代表用户授权复刻现有游戏;必须先为项目创造原创标题、实体、资源、目标名称与视觉语言,并在 memory/project.md、game/game_design.md 和图片提示中保持一致。不得沿用或近似改写知名游戏单位、角色、Logo、界面术语或受保护视觉语言。文本策划只是中间结果;最终必须先用 asset.list 确认 assets/art-spec.png 已登记为当前项目的 icon-spec 画布资源,再调用 canvas.asset_generate 生成 16:9、2K 横屏界面原型图并登记到 assets/ui-prototype.png,assetKind=ui-prototype、assetLabel=游戏横屏界面原型图、replaceExisting=false。图片 prompt 必须逐项继承当前任务和 game/game_design.md 的真实玩法、HUD、可玩区域、关键实体、主要操作、失败/重开与移动端触控要求;不得假设为塔防或补入合同中不存在的单位卡牌、费用、波次、敌人入口等结构。Runtime 固定把规范图资源作为 referenceImageSrcs 第一项,调用 External Editor v1 的 POST /api/external/v1/editor/images/generations(kind=ui-design);不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions,后者只用于从已有且带标注的 UI 设计图提取独立透明 UI 素材。缺少规范图时必须等待 art-director 依赖并如实阻塞,不得回退为无规范参考的普通生图。canvas.asset_generate 成功只表示候选图片已生成并登记,不等于视觉验收完成。已有同路径画布资产时先核对登记,再在当前 run 对且只对 assets/ui-prototype.png 调用 image.inspect;检查已通过时不得重复生成或再次扣费。只有 ui-prototype.v2 的 informationHud、gameplaySurface、objectiveEntities、primaryControls、failureRestartFlow、responsiveLayout、implementationClarity、originalTheme 八项检查全部通过才可完成。八项视觉检查通过后直接交付,由 Runtime 在收束门内同时核对固定 owner 文档、当前 revision 与视觉证据。纯场景图、概念图、地图、海报或只有角色而没有可玩界面的画面都不是 UI 原型。视觉检查未通过时不得提交最终回复;只有任务正文明确标识这是带 repairOfDelegationId 的唯一返工轮时,才可使用固定输出合同和 replaceExisting=true 原位替换旧候选;不得先删除正式图片。图片生成未配置、待确认或失败时同样不得提交最终回复,也不得把计划写完当成 completed。{role_boundary}" ) } @@ -504,9 +504,9 @@ fn game_creator_art_director_tool_plan_prompt( editor_api_key_is_configured: bool, ) -> String { if !editor_api_key_is_configured { - return format!("{prompt}\n\n你负责确定原创视觉方向。当前未配置 External Editor API Key,只完成正式 director 文档,不调用 canvas.asset_generate,也不伪造 assets/art-spec.png。"); + return format!("{prompt}\n\n你负责确定原创视觉方向。当前未配置 External Editor API Key,这是只读协调任务:只完成正式 director 结论并直接交付,不修改项目文件,不调用 canvas.asset_generate,也不伪造 assets/art-spec.png。seed task 中生成规范图的图片产物与验收条款在本轮不适用。"); } - format!("{prompt}\n\n你负责生成项目唯一的统一视觉规范图。视觉方向文档只是中间结果;最终必须调用 canvas.asset_generate,以固定合同 outputPath=assets/art-spec.png、aspectRatio=1:1、imageSize=1K、assetKind=icon-spec、assetLabel=游戏统一视觉规范图、replaceExisting=false 生成真实图片。Runtime 固定调用 External Editor v1 的 POST /api/external/v1/editor/images/generations(kind=spec),并把结果同时登记到同名画布、素材库和项目 manifest。规范图必须覆盖玩家主体、目标物、地块、UI 图标、状态反馈、色板与材质规则,作为后续 UI 和透明图集共同引用的权威资源;不得用 generationInputs.artSpec JSON、纯文本计划、完整游戏截图、海报或普通黑底图集冒充。已有有效同路径资产时不得重复生成或扣费;只有带 repairOfDelegationId 的唯一返工轮可设置 replaceExisting=true 原位替换。生成失败或缺少 resourceId 时不得提交最终回复,也不得把计划写完当成 completed。") + format!("{prompt}\n\n你负责生成项目唯一的统一视觉规范图。视觉方向文档只是中间结果;最终必须调用 canvas.asset_generate,以固定合同 outputPath=assets/art-spec.png、aspectRatio=1:1、imageSize=1K、assetKind=icon-spec、assetLabel=游戏统一视觉规范图、replaceExisting=false 生成真实图片。Runtime 固定调用 External Editor v1 的 POST /api/external/v1/editor/images/generations(kind=spec),并把结果同时登记到同名画布、素材库和项目 manifest。规范图必须覆盖玩家主体、目标物、地块、UI 图标、状态反馈、色板与材质规则,作为后续 UI 和透明图集共同引用的权威资源;不得用 generationInputs.artSpec JSON、纯文本计划、完整游戏截图、海报或普通黑底图集冒充。canvas.asset_generate 成功只表示固定候选已生成并登记,不等于视觉门已经通过;生成成功后直接交付,由 Runtime 在收束时核对当前 revision、Canvas 登记、资源身份和视觉产物门。已有有效同路径资产时不得重复生成或扣费;只有带 repairOfDelegationId 的唯一返工轮可设置 replaceExisting=true 原位替换。生成失败或缺少 resourceId 时不得提交最终回复,也不得把计划写完当成 completed。") } fn game_creator_art_asset_plan_tool_plan_prompt( @@ -515,11 +515,11 @@ fn game_creator_art_asset_plan_tool_plan_prompt( ) -> String { if !editor_api_key_is_configured { return format!( - "{prompt}\n\n你负责首版美术资产清单交付。当前未配置 External Editor API Key,因此本轮必须写入可解析的 assets/manifest.art.json,记录所需素材、用途、推荐规格和当前未生成状态;不调用 canvas.asset_generate,也不伪造 assets/art-spritesheet.png。完成清单并通过当前 revision 的验证后即可交付,不得编辑 game/index.html。" + "{prompt}\n\n你负责首版美术资产清单交付。当前未配置 External Editor API Key,因此本轮必须写入可解析的 assets/manifest.art.json,记录所需素材、用途、推荐规格和当前未生成状态;不调用 canvas.asset_generate,也不伪造 assets/art-spritesheet.png。完成清单后直接交付,由 Runtime 验证本人固定 owner 产物;不得调用 project.verify、game.static_smoke 或 preview.validate,也不得编辑 game/index.html。" ); } format!( - "{prompt}\n\n你负责首版美术素材实际生成。资产清单和美术计划只是中间结果;最终必须调用 canvas.asset_generate 生成并登记 assets/art-spritesheet.png,固定使用 1:1、1K、assetKind=art-spritesheet、assetLabel=游戏首版核心美术素材、replaceExisting=false,并写入可解析的 assets/manifest.art.json。调用前必须用 asset.list 确认 assets/art-spec.png 已登记为当前项目的 icon-spec 画布资源,并依据当前任务、game/game_design.md 与 manifest 逐项说明真实需要的玩家主体及朝向/状态、目标或收集物、障碍/场景元素和反馈特效,由 Runtime 形成 iconDescriptions;不得假设为塔防或加入合同中不存在的单位、敌人、波次、卡牌。Runtime 固定以规范图的权威 resourceId 作为 referenceId,调用 POST /api/external/v1/editor/icon-spritesheets/generations,并用 screenColor=auto 完成透明后处理;不得把 UI 原型、Data URL、Blob URL、本地路径或结构化 JSON 冒充规范图引用,不得回退普通生图或 UI extraction。缺少规范图时必须等待 art-director 依赖并如实阻塞。成功后回读 observation 与 asset.list,核对服务端返回的透明 spritesheet、真实 alpha、warning 和 sliceWarning。warning.code=postprocess-failed-source-preserved 时没有透明图集,不得登记、验收或自动重试;仅 sliceWarning 时可保留完整透明图集,但不得声称独立切片已生成。已有有效同路径资产时不得重复生成或扣费;只有带 repairOfDelegationId 的唯一返工轮可 replaceExisting=true 原位替换。不得运行 game.static_smoke 或 preview.validate,也不得编辑 game/index.html。图片生成未配置、待确认、失败或透明证据不足时不得提交最终回复。" + "{prompt}\n\n你负责首版美术素材实际生成。资产清单和美术计划只是中间结果;最终必须调用 canvas.asset_generate 生成并登记 assets/art-spritesheet.png,固定使用 1:1、1K、assetKind=art-spritesheet、assetLabel=游戏首版核心美术素材、replaceExisting=false,并写入可解析的 assets/manifest.art.json。调用前必须用 asset.list 确认 assets/art-spec.png 已登记为当前项目的 icon-spec 画布资源,并依据当前任务、game/game_design.md 与 manifest 逐项说明真实需要的玩家主体及朝向/状态、目标或收集物、障碍/场景元素和反馈特效,由 Runtime 形成 iconDescriptions;不得假设为塔防或加入合同中不存在的单位、敌人、波次、卡牌。Runtime 固定以规范图的权威 resourceId 作为 referenceId,调用 POST /api/external/v1/editor/icon-spritesheets/generations,并用 screenColor=auto 完成透明后处理;不得把 UI 原型、Data URL、Blob URL、本地路径或结构化 JSON 冒充规范图引用,不得回退普通生图或 UI extraction。缺少规范图时必须等待 art-director 依赖并如实阻塞。成功后回读 observation 与 asset.list,核对服务端返回的透明 spritesheet、真实 alpha、warning 和 sliceWarning。warning.code=postprocess-failed-source-preserved 时没有透明图集,不得登记、验收或自动重试;仅 sliceWarning 时可保留完整透明图集,但不得声称独立切片已生成。透明证据核对完成后直接交付,由 Runtime 在收束门内验证本人固定 manifest 产物并复核 Canvas 证据。已有有效同路径资产时不得重复生成或扣费;只有带 repairOfDelegationId 的唯一返工轮可 replaceExisting=true 原位替换。不得运行 game.static_smoke 或 preview.validate,也不得编辑 game/index.html。图片生成未配置、待确认、失败或透明证据不足时不得提交最终回复。" ) } @@ -1178,12 +1178,16 @@ mod tests { assert!(prompt.contains("项目文件写入只允许 memory/project.md 与 game/game_design.md")); assert!(prompt.contains("不得创建、修改、删除或补丁 game/index.html")); - assert!(prompt.contains("可以调用 command.run_limited 的 game.static_smoke")); - assert!(prompt.contains("不得调用 preview.start、preview.validate")); + assert!(prompt.contains("由 Runtime 在收束门内验证本人 owner 产物")); + assert!( + prompt.contains("不得调用 project.verify、command.run_limited、game.static_smoke") + ); + assert!(prompt.contains("preview.start 或 preview.validate")); + assert!(prompt.contains("最终静态验收仍属于 preview-readiness")); + assert!(prompt.contains("浏览器验收仍属于 preview-playtest")); assert!(prompt.contains( "不得通过 command.exec、command.start 或其他工具启动本地预览服务、浏览器、Playwright" )); - assert!(prompt.contains("这些集成验证必须交由程序或质量 Agent 完成")); } } @@ -1202,6 +1206,9 @@ mod tests { assert!(with_canvas.contains("assets/ui-prototype.png")); assert!(with_canvas.contains("调用 image.inspect")); assert!(with_canvas.contains("ui-prototype.v2")); + assert!(with_canvas.contains("成功只表示候选图片已生成并登记,不等于视觉验收完成")); + assert!(with_canvas.contains("由 Runtime 在收束门内同时核对固定 owner 文档")); + assert!(!with_canvas.contains("成功动作本身就是当前 revision 的验证")); assert!(with_canvas.contains("informationHud")); assert!(with_canvas.contains("failureRestartFlow")); assert!(with_canvas.contains("不得假设为塔防")); @@ -1211,6 +1218,24 @@ mod tests { .contains("不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions")); } + #[test] + fn agent_prompt_art_director_switches_between_read_only_and_conditional_canvas_owner() { + let without_canvas = + game_creator_art_director_tool_plan_prompt("shared runtime contract", false); + assert!(without_canvas.contains("这是只读协调任务")); + assert!(without_canvas.contains("只完成正式 director 结论并直接交付")); + assert!(without_canvas.contains("不调用 canvas.asset_generate")); + assert!(without_canvas.contains("图片产物与验收条款在本轮不适用")); + + let with_canvas = + game_creator_art_director_tool_plan_prompt("shared runtime contract", true); + assert!(with_canvas.contains("outputPath=assets/art-spec.png")); + assert!(with_canvas.contains("assetKind=icon-spec")); + assert!(with_canvas.contains("成功只表示固定候选已生成并登记,不等于视觉门已经通过")); + assert!(with_canvas.contains("由 Runtime 在收束时核对当前 revision")); + assert!(!with_canvas.contains("成功会为本人当前 revision 形成验证凭证")); + } + #[test] fn agent_prompt_art_asset_plan_uses_fixed_transparent_spritesheet_route_and_warnings() { let without_canvas = @@ -1234,6 +1259,7 @@ mod tests { assert!(with_canvas.contains("warning.code=postprocess-failed-source-preserved")); assert!(with_canvas.contains("不得登记、验收或自动重试")); assert!(with_canvas.contains("仅 sliceWarning")); + assert!(with_canvas.contains("由 Runtime 在收束门内验证本人固定 manifest 产物")); } #[test] 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 cb72cf053..3ad7ef690 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 @@ -140,6 +140,7 @@ pub(crate) use tool_plan_protocol::{ parse_game_creator_agent_tool_plan_response, }; pub(crate) use tool_policy_snapshot::{ - agent_runtime_executable_tools, agent_runtime_native_executable_tools, - agent_runtime_tool_policy_snapshot_for_run_at, AGENT_RUNTIME_CANVAS_ASSET_KINDS, + 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, }; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs index 3fac4bccb..370f37149 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs @@ -176,9 +176,18 @@ pub(super) const AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_MUTATION_ONLY_REPAIR_ERROR_ "自主构建非只读专业 Agent 必须先完成本人 run 的项目修改"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX: &str = "自主构建非只读专业 Agent 必须先验证本人 run 的项目修改"; +pub(super) const AGENT_RUNTIME_AUTONOMOUS_OWNER_ARTIFACT_IDENTITY_ERROR_PREFIX: &str = + "自主构建固定 owner 的 Runtime 内部产物验证身份不可用"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_READ_ONLY_MUTATION_ERROR_PREFIX: &str = "自主构建只读专业 Agent 禁止执行写入或副作用动作"; +pub(in crate::agent) fn agent_runtime_autonomous_uses_owner_artifact_validation( + agent_id: &str, +) -> bool { + !autonomous_manifest_owner_artifact_paths(agent_id).is_empty() + && !matches!(agent_id, "code-prototype" | "publish-package") +} + fn autonomous_initial_collaboration_contract_error(detail: impl AsRef) -> String { format!( "{AGENT_RUNTIME_SUPERVISOR_INITIAL_COLLABORATION_LIVENESS_ERROR_PREFIX};autonomous-game-build 首批协作合同无效:{}", @@ -439,7 +448,7 @@ pub(crate) fn refresh_agent_runtime_autonomous_convergence_snapshot_after_provid )) } -pub(super) fn validate_agent_runtime_autonomous_plan_liveness_at( +pub(in crate::agent) fn validate_agent_runtime_autonomous_plan_liveness_at( root: &Path, agent_id: &str, run_id: &str, @@ -509,6 +518,15 @@ pub(super) fn validate_agent_runtime_autonomous_plan_liveness_at( } } + if autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)? + && !plan.response.trim().is_empty() + { + // 固定 pre-code 产物没有 Provider 可调用的验证动作。专业回复门确认 + // 当前 run 已有 mutation 后,即使此前累积了较长只读尾部,也必须让 + // finalization 进入 Runtime 确定性验证,不能再强迫模型伪造验证动作。 + return Ok(()); + } + let has_current_playtest_receipt = if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { let contract = read_autonomous_completion_contract(root, agent_id, run_id)?; match contract.as_ref() { @@ -783,14 +801,22 @@ pub(in crate::agent) fn agent_runtime_autonomous_verified_delivery_allows_plan_c agent_id: &str, verification_gate: &AgentRuntimeVerificationGate, ) -> bool { - agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - && verification_gate - .mutation_revision - .is_some_and(|mutation_revision| { - verification_gate - .verified_revision - .is_some_and(|verified_revision| verified_revision >= mutation_revision) - }) + if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { + return false; + } + let Some(mutation_revision) = verification_gate.mutation_revision else { + return false; + }; + if agent_id == "code-prototype" + && !verification_gate + .static_smoke_verified_revision + .is_some_and(|revision| revision >= mutation_revision) + { + return false; + } + verification_gate + .verified_revision + .is_some_and(|verified_revision| verified_revision >= mutation_revision) && verification_gate.last_verification_status.as_deref() == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) } @@ -841,9 +867,11 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_read_only_delivery_pla } pub(in crate::agent) fn validate_agent_runtime_autonomous_specialist_response_delivery( + root: &Path, agent_id: &str, run_id: &str, read_only_delivery: bool, + runtime_owner_artifact_validation_available: bool, verification_gate: &AgentRuntimeVerificationGate, plan: &AgentRuntimeToolPlan, ) -> Result<(), String> { @@ -861,12 +889,42 @@ pub(in crate::agent) fn validate_agent_runtime_autonomous_specialist_response_de "{AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_MUTATION_ONLY_REPAIR_ERROR_PREFIX};当前 respond_to_user 没有本人 run 的 mutationRevision。必须先执行实际项目修改,不能以其它 Agent 的 revision、只读检查、空验证或任务文案代替" )); } + if runtime_owner_artifact_validation_available + && agent_runtime_autonomous_uses_owner_artifact_validation(agent_id) + { + return Ok(()); + } + let trusted_game_chat_canvas_delivery = agent_id == "art-asset-plan" + && game_chat_delegated_art_asset_plan_uses_canvas_verification_at(root, agent_id, run_id)?; + if agent_runtime_autonomous_uses_owner_artifact_validation(agent_id) + && !trusted_game_chat_canvas_delivery + { + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_OWNER_ARTIFACT_IDENTITY_ERROR_PREFIX};当前 {agent_id}/{run_id} 既不是可信完整 autonomous DAG 的当前固定 owner child,也不是可信 game-chat code-prototype 的当前动态美术委派,不能借用 Runtime 内部验证或普通 Canvas 凭证" + )); + } + if trusted_game_chat_canvas_delivery + && (verification_gate.last_verification_tool.as_deref() != Some("canvas.asset_generate") + || verification_gate.static_smoke_verified_revision.is_some()) + { + return Err(format!( + "{AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX};可信 game-chat 动态美术委派只接受本人 canvas.asset_generate 的通过凭证,不能借用 project.verify、game.static_smoke 或其它验证" + )); + } if !agent_runtime_autonomous_verified_delivery_allows_plan_completion( agent_id, verification_gate, ) { + let required_verification = if agent_id == "code-prototype" { + format!( + "code-prototype 必须由本人 run 执行 game.static_smoke,且 staticSmokeVerifiedRevision 覆盖 mutationRevision;当前 staticSmokeVerifiedRevision={:?}", + verification_gate.static_smoke_verified_revision + ) + } else { + "必须先只验证本人 run 的最新 mutation revision".to_string() + }; return Err(format!( - "{AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX};当前 mutationRevision={:?}、verifiedRevision={:?}、verificationStatus={}。必须先只验证本人 run 的最新 mutation revision,通过后才能 respond_to_user", + "{AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX};当前 mutationRevision={:?}、verifiedRevision={:?}、verificationStatus={}。{required_verification},通过后才能 respond_to_user", verification_gate.mutation_revision, verification_gate.verified_revision, verification_gate @@ -1763,6 +1821,118 @@ mod tests { } } + #[test] + fn autonomous_owner_delivery_uses_internal_validation_only_for_fixed_pre_code_roles() { + let temporary = tempfile::tempdir().expect("create owner delivery policy root"); + let root = temporary.path().join("project"); + init_local_game_project_at(&root, "owner-delivery-policy", "owner 收束策略") + .expect("init owner delivery policy project"); + let plan = AgentRuntimeToolPlan { + response: "玩法规格与界面基础已经完成。".to_string(), + ..AgentRuntimeToolPlan::default() + }; + let gate_for = |agent_id: &str, run_id: &str| AgentRuntimeVerificationGate { + schema_version: "test".to_string(), + project_id: "test".to_string(), + agent_id: agent_id.to_string(), + run_id: run_id.to_string(), + requires_verification: true, + mutation_revision: Some(7), + verified_revision: None, + last_mutation_tool: Some("file.write".to_string()), + last_verification_tool: None, + last_verification_status: None, + static_smoke_verified_revision: None, + failed_playtest_revision: None, + updated_at: 0, + }; + + for agent_id in [ + "design-foundation", + "balance-seed", + "art-asset-plan", + "audio-asset-plan", + ] { + assert!(agent_runtime_autonomous_uses_owner_artifact_validation( + agent_id + )); + let run_id = format!("{agent_id}-run"); + let gate = gate_for(agent_id, &run_id); + let unavailable_error = validate_agent_runtime_autonomous_specialist_response_delivery( + &root, agent_id, &run_id, false, false, &gate, &plan, + ) + .expect_err("fixed owner must not fall back to Provider-visible verification"); + assert!(unavailable_error + .contains(AGENT_RUNTIME_AUTONOMOUS_OWNER_ARTIFACT_IDENTITY_ERROR_PREFIX)); + assert!(!unavailable_error.starts_with( + AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX + )); + + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, agent_id, &run_id, false, true, &gate, &plan, + ) + .expect("trusted Runtime owner-artifact validation may run inside finalization"); + + let mut manually_verified = gate.clone(); + manually_verified.verified_revision = Some(7); + manually_verified.last_verification_tool = Some("game.static_smoke".to_string()); + manually_verified.last_verification_status = + Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string()); + manually_verified.static_smoke_verified_revision = Some(7); + assert!( + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, + agent_id, + &run_id, + false, + false, + &manually_verified, + &plan, + ) + .expect_err("manual smoke must not replace fixed owner Runtime validation identity") + .contains(AGENT_RUNTIME_AUTONOMOUS_OWNER_ARTIFACT_IDENTITY_ERROR_PREFIX) + ); + + let mut wrong_run_gate = gate; + wrong_run_gate.run_id = "other-run".to_string(); + assert!( + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, + agent_id, + &run_id, + false, + true, + &wrong_run_gate, + &plan, + ) + .expect_err("another run's gate must not authorize Runtime owner validation") + .contains("verification gate 与当前 run 身份不匹配") + ); + } + + for agent_id in ["code-prototype", "publish-package", "art-director"] { + assert!(!agent_runtime_autonomous_uses_owner_artifact_validation( + agent_id + )); + } + let publish_gate = gate_for("publish-package", "publish-package-run"); + assert!( + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, + "publish-package", + "publish-package-run", + false, + true, + &publish_gate, + &plan, + ) + .expect_err( + "caller flag must not classify publish-package as an internal-validation owner" + ) + .contains(AGENT_RUNTIME_AUTONOMOUS_SPECIALIST_VERIFICATION_ONLY_REPAIR_ERROR_PREFIX) + ); + } + #[test] fn autonomous_read_only_validation_actions_are_role_scoped() { let plan_for = |tool: &str, input: serde_json::Value| AgentRuntimeToolPlan { diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs index 5a1c8146e..32a46bfa7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs @@ -373,6 +373,8 @@ pub(crate) fn finish_agent_runtime_project_verification_locked( } if verification_tool.as_deref() == Some("game.static_smoke") { gate.static_smoke_verified_revision = passed.then_some(current_revision.revision); + } else if verification_tool.as_deref() == Some(AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL) { + gate.static_smoke_verified_revision = None; } gate.last_verification_status = Some( if passed { @@ -1391,17 +1393,217 @@ pub(in crate::agent) fn project_verification_completion_blocker_at_locked( } } +fn owner_artifact_verification_is_current( + gate: &AgentRuntimeVerificationGate, + current_revision: u64, +) -> bool { + gate.last_verification_tool.as_deref() == Some(AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL) + && gate.last_verification_status.as_deref() + == Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) + && gate.static_smoke_verified_revision.is_none() + && gate.mutation_revision.is_some_and(|mutation_revision| { + gate.verified_revision.is_some_and(|verified_revision| { + verified_revision >= mutation_revision && verified_revision <= current_revision + }) + }) +} + +fn ensure_owner_artifact_validation_audit_at_locked( + root: &Path, + agent_id: &str, + run_id: &str, + revision: u64, + paths: &[&str], +) -> Result<(), String> { + let binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? + .ok_or_else(|| "owner-artifact 验证审计缺少 Run Profile 绑定".to_string())?; + let (records, _) = + read_agent_db_records_bounded(root, AGENT_RUNTIME_ACTION_HISTORY_MAX_DB_BYTES)?; + let audit_exists = records.iter().rev().any(|record| { + record.get("recordType").and_then(serde_json::Value::as_str) + == Some("agent.runtime.owner_artifacts.validated") + && record.get("agentId").and_then(serde_json::Value::as_str) == Some(agent_id) + && record.get("runId").and_then(serde_json::Value::as_str) == Some(run_id) + && record.get("rootRunId").and_then(serde_json::Value::as_str) + == Some(binding.root_run_id.as_str()) + && record + .get("runProfileBindingFingerprint") + .and_then(serde_json::Value::as_str) + == Some(binding.binding_fingerprint.as_str()) + && record.get("revision").and_then(serde_json::Value::as_u64) == Some(revision) + && record + .get("verificationTool") + .and_then(serde_json::Value::as_str) + == Some(AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL) + && record + .get("paths") + .and_then(serde_json::Value::as_array) + .is_some_and(|items| { + items.len() == paths.len() + && items + .iter() + .zip(paths) + .all(|(item, path)| item.as_str().is_some_and(|value| value == *path)) + }) + }); + if audit_exists { + return Ok(()); + } + append_agent_db_record( + root, + serde_json::json!({ + "recordType": "agent.runtime.owner_artifacts.validated", + "agentId": agent_id, + "runId": run_id, + "rootAgentId": binding.root_agent_id, + "rootRunId": binding.root_run_id, + "parentAgentId": binding.parent_agent_id, + "parentRunId": binding.parent_run_id, + "source": binding.source, + "runProfile": binding.profile, + "runProfileBindingFingerprint": binding.binding_fingerprint, + "revision": revision, + "verificationTool": AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL, + "paths": paths, + }), + ) +} + pub(in crate::agent) fn evaluate_project_verification_completion_at_locked( root: &Path, agent_id: &str, run_id: &str, observations: &[AgentRuntimeToolObservation], ) -> Result, String> { - if let Some(blocker) = project_verification_completion_blocker(observations) { - return Ok(Some(blocker)); - } let revision = read_game_creator_agent_runtime_project_revision(root)?; - let gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + let runtime_owner_artifact_validation_available = + autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)?; + let mut gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + let autonomous_owner_artifact_role = + if agent_runtime_autonomous_uses_owner_artifact_validation(agent_id) { + match read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? { + Some(binding) => binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, + None => { + read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)? + .is_some_and(|task| { + task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + }) + } + } + } else { + false + }; + let trusted_game_chat_canvas_delivery = autonomous_owner_artifact_role + && game_chat_delegated_art_asset_plan_uses_canvas_verification_at(root, agent_id, run_id)?; + if autonomous_owner_artifact_role + && !runtime_owner_artifact_validation_available + && !trusted_game_chat_canvas_delivery + { + return Ok(Some(agent_runtime_verification_blocker( + "当前 owner Run 不具备可用的验证身份,不能把任务标记为完成", + "只有完整 GUI/CLI autonomous DAG 的当前 fixed owner 可由 Runtime 内部验证;只有可信 game-chat code-prototype 的当前 art-asset-plan 动态委派可沿用普通 Canvas 验证。", + ))); + } + if trusted_game_chat_canvas_delivery + && (gate.last_verification_tool.as_deref() != Some("canvas.asset_generate") + || gate.last_verification_status.as_deref() + != Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) + || gate.static_smoke_verified_revision.is_some() + || !gate.mutation_revision.is_some_and(|mutation_revision| { + gate.verified_revision + .is_some_and(|verified_revision| verified_revision >= mutation_revision) + })) + { + return Ok(Some(agent_runtime_verification_blocker( + "game-chat 动态 art-asset-plan 尚未形成可信 Canvas 交付凭证", + "当前动态美术 child 必须由本人 canvas.asset_generate 生成正式素材并通过当前 mutation revision;project.verify、game.static_smoke 或其它 Agent 的凭证均不能替代。", + ))); + } + if runtime_owner_artifact_validation_available && gate.requires_verification { + let paths = + match validate_autonomous_owner_artifacts_for_run_at_locked(root, agent_id, run_id) { + Ok(paths) => paths, + Err(error) => { + return Ok(Some(agent_runtime_verification_blocker( + "当前 owner 正式产物尚未通过 Runtime 结构验证", + error, + ))); + } + }; + let owner_artifact_verification_current = + owner_artifact_verification_is_current(&gate, revision.revision); + let audit_revision = if owner_artifact_verification_current { + gate.verified_revision + .expect("current owner-artifact verification has a revision") + } else { + revision.revision + }; + ensure_owner_artifact_validation_audit_at_locked( + root, + agent_id, + run_id, + audit_revision, + paths, + )?; + if !owner_artifact_verification_current { + let (expected_revision, verification_gate) = + begin_agent_runtime_project_verification_locked( + root, + agent_id, + run_id, + AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL, + )?; + finish_agent_runtime_project_verification_locked( + root, + &expected_revision, + verification_gate, + true, + )?; + gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + } + } + let autonomous_code_prototype = if agent_id == "code-prototype" { + match read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? { + Some(binding) => binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD, + None => read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)? + .is_some_and(|task| { + task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + }), + } + } else { + false + }; + if autonomous_code_prototype { + ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)?; + let Some(mutation_revision) = gate.mutation_revision.filter(|revision| *revision > 0) + else { + return Ok(Some(agent_runtime_verification_blocker( + "code-prototype 尚未形成本人 run 的项目修改,不能把任务标记为完成", + "完整 autonomous DAG 的 code-prototype 必须先实际修改可玩入口,再由本人执行 game.static_smoke。", + ))); + }; + if !gate + .static_smoke_verified_revision + .is_some_and(|revision| revision >= mutation_revision) + { + return Ok(Some(agent_runtime_verification_blocker( + "code-prototype 尚未通过覆盖本人最后一次修改的 game.static_smoke", + format!( + "mutationRevision={mutation_revision}, staticSmokeVerifiedRevision={};project.verify 或其它 Agent 的验证凭证不能替代本人 run 的 game.static_smoke。", + gate.static_smoke_verified_revision + .map(|revision| revision.to_string()) + .unwrap_or_else(|| "none".to_string()) + ), + ))); + } + } + let owner_artifact_verification_current = runtime_owner_artifact_validation_available + && owner_artifact_verification_is_current(&gate, revision.revision); + if !owner_artifact_verification_current { + if let Some(blocker) = project_verification_completion_blocker(observations) { + return Ok(Some(blocker)); + } + } if let Some(status) = gate.last_verification_status.as_deref() { if status == AGENT_RUNTIME_VERIFICATION_STATUS_RUNNING { return Ok(Some(agent_runtime_verification_blocker( 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 534adcb58..53b9e46fe 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 @@ -1,5 +1,6 @@ use super::*; use crate::mcp::GAME_CREATOR_MCP_CALL_TOOL; +use platform_llm::LlmFunctionTool; const AGENT_RUNTIME_COMPLETION_BLOCKER_TOOL_PLAN_PROTOCOL: &str = "通用完成阻断规则:如果最新 observation 的 tool 为 runtime.autonomous_completion 且 status 为 blocked,本轮禁止调用 respond_to_user;必须先读取该 observation.detail 的 nextRequiredAction,并据此调用合适的读取、修复和验证工具。只有完成要求的动作、取得后续可信 observation 且完成门禁不再阻断后,才能给最终回复;不得反复提交 final response,也不得按项目正文硬编码某一种 blocker 的处理方式。"; @@ -63,6 +64,36 @@ fn game_creator_agent_context_preload_notice(agent_id: &str) -> &'static str { } } +pub(in crate::agent) fn remove_autonomous_owner_manual_verification_tools( + tools: &mut Vec, +) -> Result<(), String> { + let project_verify = native_runtime_function_name("project.verify") + .ok_or_else(|| "无法生成 project.verify 原生函数名".to_string())?; + let limited_command = native_runtime_function_name("command.run_limited") + .ok_or_else(|| "无法生成 command.run_limited 原生函数名".to_string())?; + tools.retain(|tool| tool.name != project_verify && tool.name != limited_command); + Ok(()) +} + +pub(in crate::agent) fn remove_autonomous_art_director_non_canvas_validation_tools( + tools: &mut Vec, +) -> Result<(), String> { + let denied_function_names = [ + "project.verify", + "command.run_limited", + "preview.start", + "preview.validate", + ] + .into_iter() + .map(|tool| { + native_runtime_function_name(tool) + .ok_or_else(|| format!("无法生成 art-director 禁用工具函数名:{tool}")) + }) + .collect::, _>>()?; + tools.retain(|tool| !denied_function_names.contains(&tool.name)); + Ok(()) +} + pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( root: &Path, agent_id: &str, @@ -93,8 +124,10 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( agent_runtime_tool_policy_snapshot_for_run_at(root, agent_id, run_id, None, None)?; let autonomous_game_build = tool_policy.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD; - let autonomous_project_verify_available = - !autonomous_game_build || agent_runtime_autonomous_project_verify_available(root); + let runtime_owner_artifact_validation_available = autonomous_game_build + && autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)?; + let autonomous_project_verify_available = !runtime_owner_artifact_validation_available + && (!autonomous_game_build || agent_runtime_autonomous_project_verify_available(root)); let mut allowed_tools = tool_policy.allowed_tools.clone(); let mut auto_tools = tool_policy.auto_tools.clone(); let mut confirm_tools = tool_policy.confirm_tools.clone(); @@ -148,6 +181,16 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( let loop_index = loop_index.saturating_add(1); let context_preload_notice = game_creator_agent_context_preload_notice(agent_id); let canvas_asset_kind_catalog = AGENT_RUNTIME_CANVAS_ASSET_KINDS.join("|"); + let limited_command_contract = if runtime_owner_artifact_validation_available { + "当前固定 owner 不得调用 command.run_limited 或 project.verify;完成固定正式产物后直接交付,由 Runtime 在收束门内执行结构验证。".to_string() + } else { + "command.run_limited 使用 {\"commandId\":\"game.static_smoke\"}。".to_string() + }; + let project_tools_contract = if runtime_owner_artifact_validation_available { + "project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;当前固定 owner 的函数目录不广告 project.verify;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string() + } else { + "project.search 使用 {\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.verify 使用 {\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120},只执行项目根 package.json 中同名 npm 脚本,expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {\"checkpointId\":\"checkpoint id\"};project.diff 使用 {\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000};git.inspect 使用 {\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。".to_string() + }; let prompt = format!( concat!( "当前工具策略:\n{tool_policy_json}\n\n", @@ -162,10 +205,10 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( "计划更新约定:复杂任务首次拆解、实际进度变化、steer 改变顺序或最终收束时调用 update_agent_plan,arguments 只提交 {{\"explanation\":\"本次计划变化\",\"steps\":[{{\"step\":\"步骤\",\"status\":\"pending|in_progress|completed\"}}]}};无需更新时不要调用 update_agent_plan。steps 最多 8 条且同时最多一个 in_progress,已完成步骤必须继续保留且不得回退。持久计划仍有 pending / in_progress 时不得调用 respond_to_user,Runtime 也不会按动作返回顺序自动完成步骤。\n\n", "工具 input 字段约定:下列每个示例对象都必须放入对应动作函数的 arguments.input;arguments 外层严格为 {{\"reason\":\"为什么需要\",\"input\":{{...}}}},禁止把 input 扁平到 arguments 顶层。\n", "memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}},其中 agent scope 只能写当前 Agent 自己的私有记忆,跨 Agent 共享请用 blackboard.write 或 agent.message。\n", - "project.search 使用 {{\"query\":\"要查找的字面文本\",\"path\":\"\",\"maxResults\":20,\"caseSensitive\":false}},path 为空字符串时搜索整个项目,返回 path:line 和匹配行;project.verify 使用 {{\"script\":\"check|typecheck|test|lint|build\",\"expectedCommand\":\"从 package.json 读取的完整原始脚本\",\"timeoutSeconds\":120}},只执行项目根 package.json 中同名 npm 脚本,expectedCommand 不一致时拒绝执行,确认策略以当前工具策略中 project.verify 的独立权限为准;project.checkpoint 使用空对象,只用于多个 file.* 写动作前或需要独立回退点时创建本地 checkpoint;project.patchset 会自动创建 checkpoint,不要为同一批变更额外调用 project.checkpoint;project.restore 使用 {{\"checkpointId\":\"checkpoint id\"}};project.diff 使用 {{\"checkpointId\":\"checkpoint id\",\"includeContent\":true,\"maxFiles\":20,\"maxChars\":24000}};git.inspect 使用 {{\"includeDiff\":true,\"maxFiles\":20,\"maxChars\":24000}},只读项目根 Git 状态和有界 diff,不得用它提交、暂存、切分支、合并、重置、stash、worktree 或访问 remote。\n", + "{project_tools_contract}\n", "project.patchset 的每个 change 必须显式提交七个字段。create 使用 {{\"operation\":\"create\",\"path\":\"项目内相对文件\",\"content\":\"完整内容\",\"expectedSha256\":null,\"oldText\":null,\"newText\":null,\"expectedReplacements\":null}};update 使用 {{\"operation\":\"update\",\"path\":\"项目内相对文件\",\"content\":null,\"expectedSha256\":\"file.read 返回的 SHA-256\",\"oldText\":\"精确原文\",\"newText\":\"替换后文本\",\"expectedReplacements\":1}};delete 使用 {{\"operation\":\"delete\",\"path\":\"项目内相对文件\",\"content\":null,\"expectedSha256\":\"file.read 返回的 SHA-256\",\"oldText\":null,\"newText\":null,\"expectedReplacements\":null}}。成功后必须用返回的 checkpointId 调用 project.diff includeContent=true 审查整体变更。\n", "file.list 使用 {{\"path\":\"\"}},path 为空字符串时列出项目摘要;file.read 使用 {{\"path\":\"项目内相对路径\",\"startLine\":1,\"maxLines\":120}};file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"完整文件内容\"}};file.patch 使用 {{\"path\":\"项目内相对路径\",\"oldText\":\"必须精确匹配的原文\",\"newText\":\"替换后的文本\",\"expectedReplacements\":1}};file.delete 使用 {{\"path\":\"项目内相对路径\"}},只删除项目内普通文件,不删除目录或任何 .agent 控制面文件。\n", - "task.create 使用 {{\"taskId\":null,\"title\":\"任务标题\",\"group\":\"design|art|code|balance|audio|publishing\",\"role\":\"角色名\",\"dependencies\":[],\"artifacts\":[],\"acceptanceCriteria\":[\"验收标准\"],\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}},需要自定义 taskId 时把 null 替换为合法 ID;task.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}};command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}}。\n", + "task.create 使用 {{\"taskId\":null,\"title\":\"任务标题\",\"group\":\"design|art|code|balance|audio|publishing\",\"role\":\"角色名\",\"dependencies\":[],\"artifacts\":[],\"acceptanceCriteria\":[\"验收标准\"],\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}},需要自定义 taskId 时把 null 替换为合法 ID;task.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}};{limited_command_contract}\n", "canvas.asset_generate 使用 {{\"prompt\":\"图片描述\",\"outputPath\":null,\"aspectRatio\":null,\"imageSize\":null,\"assetKind\":null,\"assetLabel\":null,\"replaceExisting\":false}};需要指定时,aspectRatio 只允许 1:1|2:3|3:2|9:16|16:9,imageSize 只允许 0.5K|1K|2K,assetKind 只允许 {canvas_asset_kind_catalog}。replaceExisting 只能在带 repairOfDelegationId 的唯一返工委派中设为 true,普通生成必须为 false,并通过配置的 External Editor API 同时写入画布、同名素材库目录和本地 assets。\n", "blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"acceptanceCriteria\":[\"可核对的语义验收条件\"],\"expectedArtifacts\":[],\"repairOfDelegationId\":null,\"runId\":null}},expectedArtifacts 无产物时传空数组且不接受 glob;返工时 repairOfDelegationId 指向已认领原 delivery 且 runId 必须为 null;agent.schedule_ready 使用 {{\"limit\":1}};agent.route_manifest 使用 {{\"strategy\":\"audit-existing-first|use-existing-art|generate-missing-art\",\"intentSummary\":\"Supervisor 自行理解的用户意图,仅 Supervisor 提交\",\"missingAssetSlots\":[]}};Supervisor 必须自行概括非空 intentSummary,并以 audit-existing-first 提交执行安全策略;code-prototype 先 asset.list 后只能按权威缺口提交 use-existing-art 或 generate-missing-art,且无需提交 intentSummary;agent.run_status 使用 {{\"agentId\":null,\"scope\":\"all\",\"delegationId\":null}},指定目标 Agent 或已认领 delegation 时把对应 null 替换为实际 ID;Project Supervisor 传 delegationId 时读取当前父 run 的未截断权威返工合同。\n", "当前请求中的每个 MCP 工具都以单独的动态函数广告;必须从实际广告函数中选择,并严格按该函数的 input schema 提交 arguments.input。server、tool、catalogFingerprint 和 toolFingerprint 由 Runtime 注入,禁止构造目录外包装调用。\n", @@ -184,14 +227,22 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( steers_json = steers_json, observations_json = observations_json, canvas_asset_kind_catalog = canvas_asset_kind_catalog, + limited_command_contract = limited_command_contract, + project_tools_contract = project_tools_contract, ); - let command_exec_contract = provider_command_exec_contract(); - let prompt = format!( - "{prompt}\n\n补充协议:project.verify 的 script 除 check、typecheck、test、lint、build 外,还可使用 check:、test:(例如 test:unit)、lint:、typecheck:、build:、verify:、validate: 形式的命名脚本;冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点,并且 script 与 expectedCommand 都必须原样来自项目根 package.json。{command_exec_contract};args 中的项目路径必须相对 cwd,禁止绝对路径、file URI、路径加行号以及把绝对路径嵌入脚本或说明文字。该工具默认需要精确确认,适合运行定向测试、构建检查和只读诊断。durable command.exec observation 会直接返回可复用的 sourceActionId;短 observation 不足以定位失败时,使用 command.output_read {{\"actionId\":\"该 sourceActionId\",\"startLine\":1,\"maxLines\":160}} 分页读取同一 Agent 的已清洗命令输出,并按 nextLine 继续,不要先猜 actionId 或为取得它额外查询动作历史,也不得仅凭输出尾部猜测。只有 cargo check/test/clippy/fmt/build、npm test 或命名为 check/typecheck/test/lint/build/verify/validate 的验证脚本,以及精确 node --test 测试文件可签发验证凭证;git、rg、cargo metadata 和普通 npm run 只作为诊断结果。每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec 或 command.start,都会产生新的项目 revision;最后一次修改后必须成功执行 project.verify、可验证 command.exec,或成功执行 command.run_limited 的 game.static_smoke,才能调用 respond_to_user 收束。文件回读不能替代可执行验证,验证后再次修改必须重新验证。每 {AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT} 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。" - ); - let prompt = format!( - "{prompt}\n\n受控本地 Git 提交:git.inspect 会返回 commitSnapshotFingerprint;只有在完整审阅变更且最后一次源码修改已获得当前 revision 的 passed 验证后,才能调用 project.git_commit {{\"message\":\"提交标题和正文\",\"paths\":[\"显式相对路径\"],\"expectedHead\":\"git.inspect 返回的 head\",\"expectedSnapshotFingerprint\":\"git.inspect 返回的 commitSnapshotFingerprint\"}}。project.git_commit 最多提交 12 个显式安全路径,要求 attached branch 和空 staged index,只创建本地 commit;不得用它或 command.exec 执行 push、分支、merge、rebase、reset、stash、tag、submodule 或 worktree 写操作。" - ); + let prompt = if runtime_owner_artifact_validation_available { + format!( + "{prompt}\n\n固定 owner 收束协议:当前请求不广告 project.verify 或 command.run_limited。完成本人固定路径的正式产物后直接调用 respond_to_user;Runtime 会在同一活跃根、同一 Agent/run 和当前 mutation revision 上检查非空内容、JSON 解析、未完成标记与父完成合同 baseline 变化。不得为了取得验证凭证运行项目命令、静态 smoke、预览或提交 Git;文件回读也不能代替 Runtime 收束门。再次修改会使旧 owner 凭证失效,必须重新直接交付并接受收束检查。每 {AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT} 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。" + ) + } else { + let command_exec_contract = provider_command_exec_contract(); + let prompt = format!( + "{prompt}\n\n补充协议:project.verify 的 script 除 check、typecheck、test、lint、build 外,还可使用 check:、test:(例如 test:unit)、lint:、typecheck:、build:、verify:、validate: 形式的命名脚本;冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点,并且 script 与 expectedCommand 都必须原样来自项目根 package.json。{command_exec_contract};args 中的项目路径必须相对 cwd,禁止绝对路径、file URI、路径加行号以及把绝对路径嵌入脚本或说明文字。该工具默认需要精确确认,适合运行定向测试、构建检查和只读诊断。durable command.exec observation 会直接返回可复用的 sourceActionId;短 observation 不足以定位失败时,使用 command.output_read {{\"actionId\":\"该 sourceActionId\",\"startLine\":1,\"maxLines\":160}} 分页读取同一 Agent 的已清洗命令输出,并按 nextLine 继续,不要先猜 actionId 或为取得它额外查询动作历史,也不得仅凭输出尾部猜测。只有 cargo check/test/clippy/fmt/build、npm test 或命名为 check/typecheck/test/lint/build/verify/validate 的验证脚本,以及精确 node --test 测试文件可签发验证凭证;git、rg、cargo metadata 和普通 npm run 只作为诊断结果。每次成功执行 file.write、file.patch、file.delete、project.patchset 或 project.restore,以及每次真正启动 command.exec 或 command.start,都会产生新的项目 revision;最后一次修改后必须使用当前角色可用的 project.verify、可验证 command.exec 或 game.static_smoke 完成验证,才能调用 respond_to_user。文件回读不能替代验证,验证后再次修改必须重新验证。每 {AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT} 轮只是一次进度 checkpoint 与停滞检测,不是上下文压缩或 run 的终止上限;只要 observation 出现新的独立进展,就在同一 run 继续下一窗口,只有窗口没有新进展时才按停滞处理。真正的上下文压缩仅由 token 阈值或显式 compact 触发。" + ); + format!( + "{prompt}\n\n受控本地 Git 提交:git.inspect 会返回 commitSnapshotFingerprint;只有在完整审阅变更且最后一次源码修改已获得当前 revision 的 passed 验证后,才能调用 project.git_commit {{\"message\":\"提交标题和正文\",\"paths\":[\"显式相对路径\"],\"expectedHead\":\"git.inspect 返回的 head\",\"expectedSnapshotFingerprint\":\"git.inspect 返回的 commitSnapshotFingerprint\"}}。project.git_commit 最多提交 12 个显式安全路径,要求 attached branch 和空 staged index,只创建本地 commit;不得用它或 command.exec 执行 push、分支、merge、rebase、reset、stash、tag、submodule 或 worktree 写操作。" + ) + }; let isolated_tool_contract = required_runtime_prompt_section(RUNTIME_PROMPT_PROVIDER_ISOLATED_TOOL_CONTRACT_SECTION); let prompt = format!( @@ -226,7 +277,25 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( let playtest_contract = autonomous_playtest_contract_prompt(playtest_scenario); system_prompt.push_str("\n\n"); system_prompt.push_str(playtest_contract); - system_prompt.push_str(" 只有当前 revision 通过 game.static_smoke,并由 preview.validate 对上述固定状态面和控件完成真实浏览器动作后,Runtime 才允许最终回复;不要伪造已通过 observation。"); + let root_source = agent_runtime_root_source_at(root, agent_id, run_id)?; + let verification_contract = if runtime_owner_artifact_validation_available { + " 当前固定 owner 写入后直接交付;Runtime 会在收束门内检查本人正式产物,禁止调用 game.static_smoke、project.verify 或 preview.validate 冒充。" + } else if agent_id == "preview-readiness" { + " 当前只读静态验收任务必须对最终 revision 执行 game.static_smoke,不执行 preview.validate。" + } else if agent_id == "preview-playtest" { + " 当前只读试玩任务必须执行 preview.validate,不执行 game.static_smoke。" + } else if agent_id == "code-prototype" + && root_source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE + { + " 当前 game-chat 主 Agent 必须对本人最终 mutation revision 依次通过 game.static_smoke 与 preview.validate。" + } else if agent_id == "code-prototype" { + " 当前程序 owner 必须对可玩入口执行 game.static_smoke;完整 DAG 的最终静态与浏览器验收仍由后续质量任务负责。" + } else if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID { + " 根 Supervisor 只按完成合同防御性复核最终 revision,不得把自身复核冒充专业 child 职责。" + } else { + " 当前专业任务只按自身完成合同收束,不得伪造 smoke 或 preview observation。" + }; + system_prompt.push_str(verification_contract); } if autonomous_game_build { let root_source = agent_runtime_root_source_at(root, agent_id, run_id)?; @@ -249,6 +318,12 @@ pub(in crate::agent) fn build_game_creator_agent_background_tool_plan_request( .with_response_text_verbosity(platform_llm::LlmResponseTextVerbosity::Low) .with_function_tools(build_agent_runtime_native_function_tools(mcp_catalog)?) .with_tool_choice(platform_llm::LlmToolChoice::Required); + if runtime_owner_artifact_validation_available { + remove_autonomous_owner_manual_verification_tools(&mut request.function_tools)?; + } + if autonomous_game_build && agent_id == "art-director" { + remove_autonomous_art_director_non_canvas_validation_tools(&mut request.function_tools)?; + } if autonomous_game_build && !editor_api_key_is_configured() { let canvas_function = native_runtime_function_name("canvas.asset_generate") .ok_or_else(|| "无法生成画布素材工具函数名".to_string())?; @@ -461,7 +536,8 @@ mod tests { build_game_creator_agent_background_tool_plan_request, game_creator_agent_context_preload_notice, game_creator_agent_runtime_role_overlay_prompt, game_creator_project_supervisor_chat_system_prompt, init_local_game_project_at, - provider_command_exec_contract, provider_command_start_contract, + new_game_creation_app_seed_tasks, provider_command_exec_contract, + provider_command_start_contract, render_autonomous_manifest_ready_task_background_prompt, required_runtime_prompt_section, resolve_agent_conversation_session_id_at, start_game_creator_agent_runtime_task_at, AgentRuntimeTaskLink, AgentRuntimeToolPlan, GameCreatorMcpCatalog, GameCreatorMcpCatalogTool, @@ -585,6 +661,92 @@ mod tests { request.messages[0].content.clone() } + fn build_autonomous_ready_child_request( + agent_id: &str, + root_source: &str, + suffix: &str, + ) -> platform_llm::LlmRunRequest { + let temporary = + crate::tests::canonical_test_tempdir(&format!("provider-owner-artifact-{suffix}-")); + let root = temporary.path().join("project"); + init_local_game_project_at( + &root, + &format!("owner-artifact-{suffix}"), + "owner artifact provider contract", + ) + .expect("project init"); + let parent_run_id = format!("owner-artifact-parent-{suffix}"); + let parent = bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &parent_run_id, + root_source, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("bind autonomous parent profile"); + let child_run_id = format!("owner-artifact-child-{suffix}"); + let child_link = AgentRuntimeTaskLink { + parent_agent_id: Some(parent.agent_id), + parent_run_id: Some(parent.run_id), + delegation_id: None, + }; + bind_game_creator_agent_runtime_run_profile_at( + &root, + agent_id, + &child_run_id, + "agent-ready-task-scheduler", + None, + Some(&child_link), + ) + .expect("bind autonomous ready child profile"); + let seed_task = new_game_creation_app_seed_tasks() + .into_iter() + .find(|task| task.id == agent_id) + .unwrap_or_else(|| panic!("missing seed task {agent_id}")); + let task = render_autonomous_manifest_ready_task_background_prompt(&seed_task); + assert!( + task.contains("这是 autonomous-game-build"), + "ready task prompt lost autonomous overlay: {task}" + ); + let state = start_game_creator_agent_runtime_task_at( + &root, + agent_id, + &task, + &child_run_id, + "agent-ready-task-scheduler", + "构建 owner artifact planning request", + vec!["交付当前 manifest task".to_string()], + ) + .expect("start autonomous ready child task"); + let catalog = GameCreatorMcpCatalog { + fingerprint: String::new(), + servers: Vec::new(), + tools: Vec::new(), + }; + let (_, _, request, _) = build_game_creator_agent_background_tool_plan_request( + &root, + agent_id, + &state.session_id, + &state.run_id, + &state.current_task, + &[], + 0, + &catalog, + ) + .expect("build autonomous ready child request"); + request + } + + fn request_advertises_native_tool(request: &platform_llm::LlmRunRequest, tool: &str) -> bool { + let function_name = crate::agent_native_tools::native_runtime_function_name(tool) + .expect("native runtime function name"); + request + .function_tools + .iter() + .any(|function| function.name == function_name) + } + #[test] fn context_preload_notice_matches_agent_context() { assert_eq!( @@ -599,6 +761,177 @@ mod tests { ); } + #[test] + fn full_dag_pre_code_owner_requests_do_not_advertise_manual_verification() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + for (index, agent_id) in [ + "design-foundation", + "balance-seed", + "art-asset-plan", + "audio-asset-plan", + ] + .into_iter() + .enumerate() + { + let root_source = if index % 2 == 0 { + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE + } else { + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE + }; + let request = build_autonomous_ready_child_request( + agent_id, + root_source, + &format!("owner-{index}"), + ); + assert!(!request_advertises_native_tool(&request, "project.verify")); + assert!(!request_advertises_native_tool( + &request, + "command.run_limited" + )); + let system_prompt = &request.messages[0].content; + let user_prompt = &request.messages[1].content; + assert!(system_prompt.contains("固定 owner 写入后直接交付")); + assert!(system_prompt.contains("Runtime 会在收束门内检查本人正式产物")); + assert!(user_prompt.contains("固定 owner 收束协议")); + assert!(user_prompt.contains("当前请求不广告 project.verify 或 command.run_limited")); + assert!(!user_prompt.contains("project.verify 使用")); + assert!(!user_prompt.contains("command.run_limited 使用")); + assert!(user_prompt.contains("完成本人固定路径的正式产物后直接调用 respond_to_user")); + } + } + + #[test] + fn autonomous_art_director_advertises_canvas_without_smoke_or_preview_tools() { + let _config_guard = crate::tests::write_test_local_config( + r#"{"editorApi":{"apiKey":"art-director-provider-key"}}"#.to_string(), + ); + let request = build_autonomous_ready_child_request( + "art-director", + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + "art-director-canvas-only", + ); + assert!(request_advertises_native_tool( + &request, + "canvas.asset_generate" + )); + for tool in [ + "project.verify", + "command.run_limited", + "preview.start", + "preview.validate", + ] { + assert!( + !request_advertises_native_tool(&request, tool), + "art-director must not advertise non-Canvas validation tool {tool}" + ); + } + } + + #[test] + fn playable_and_late_stage_requests_keep_their_existing_verification_boundaries() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + + let code = build_autonomous_ready_child_request( + "code-prototype", + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + "code-prototype", + ); + assert!(request_advertises_native_tool(&code, "command.run_limited")); + assert!(code.messages[0] + .content + .contains("程序 owner 必须对可玩入口执行 game.static_smoke")); + assert!(code.messages[1] + .content + .contains("必须对可玩入口执行 game.static_smoke")); + + let readiness = build_autonomous_ready_child_request( + "preview-readiness", + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + "preview-readiness", + ); + assert!(request_advertises_native_tool( + &readiness, + "command.run_limited" + )); + assert!(readiness.messages[0] + .content + .contains("必须对最终 revision 执行 game.static_smoke,不执行 preview.validate")); + assert!(readiness.messages[1] + .content + .contains("且只能是 command.run_limited(commandId=game.static_smoke)")); + + let publish = build_autonomous_ready_child_request( + "publish-package", + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + "publish-package", + ); + assert!(!request_advertises_native_tool(&publish, "project.verify")); + assert!(request_advertises_native_tool( + &publish, + "command.run_limited" + )); + let publish_prompts = publish + .messages + .iter() + .map(|message| message.content.as_str()) + .collect::>() + .join("\n"); + assert!(publish_prompts.contains("不在前置固定 owner 的内部产物验证范围内")); + assert!(!publish_prompts.contains("当前固定 owner 写入后直接交付")); + assert!(!publish_prompts.contains("固定 owner 收束协议")); + } + + #[test] + fn art_director_request_exposes_canvas_only_for_the_keyed_owner_route() { + { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let request = build_autonomous_ready_child_request( + "art-director", + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + "art-director-no-key", + ); + assert!(!request_advertises_native_tool( + &request, + "canvas.asset_generate" + )); + let prompts = request + .messages + .iter() + .map(|message| message.content.as_str()) + .collect::>() + .join("\n"); + assert!( + prompts.contains("无生图凭据只读协调任务"), + "unexpected no-key art-director prompts: {prompts}" + ); + assert!(prompts.contains("不调用 canvas.asset_generate")); + } + { + let _config_guard = crate::tests::write_test_local_config( + r#"{"editorApi":{"apiKey":"provider-art-director-key"}}"#.to_string(), + ); + let request = build_autonomous_ready_child_request( + "art-director", + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + "art-director-keyed", + ); + assert!(request_advertises_native_tool( + &request, + "canvas.asset_generate" + )); + let prompts = request + .messages + .iter() + .map(|message| message.content.as_str()) + .collect::>() + .join("\n"); + assert!(prompts.contains("条件 Canvas owner 任务")); + assert!(prompts.contains("assets/art-spec.png")); + assert!(prompts.contains("成功只表示候选已生成并登记,不等于视觉门已经通过")); + assert!(!prompts.contains("成功会为本人当前 revision 形成验证凭证")); + } + } + #[test] fn provider_request_source_does_not_patch_natural_language_with_replace_chains() { let source = include_str!("provider_request_builders.rs"); 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 82d214132..51fe8f35f 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 @@ -289,20 +289,28 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at && agent_runtime_task_requires_read_only_delivery_at( root, agent_id, session_id, run_id, task, )?; + let runtime_owner_artifact_validation_available = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)?; + let code_prototype_requires_static_smoke = run_profile + == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && agent_id == "code-prototype"; let verified_delivery = if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { let verification_gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; - agent_runtime_autonomous_verified_delivery_allows_plan_completion( - agent_id, - &verification_gate, - ) + runtime_owner_artifact_validation_available + || agent_runtime_autonomous_verified_delivery_allows_plan_completion( + agent_id, + &verification_gate, + ) } else { false }; let allow_runtime_plan_completion = read_only_delivery || verified_delivery; let autonomous_project_verify_available = run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - || agent_runtime_autonomous_project_verify_available(root); + || (!runtime_owner_artifact_validation_available + && agent_runtime_autonomous_project_verify_available(root)); let mut autonomous_scaffold_repair_active = false; let mut supervisor_collaboration_repair_active = false; let mut supervisor_collaboration_repair_actions = Vec::new(); @@ -434,9 +442,11 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at ) })?; validate_agent_runtime_autonomous_specialist_response_delivery( + root, agent_id, run_id, read_only_delivery, + runtime_owner_artifact_validation_available, &verification_gate, &parsed.plan, ) @@ -849,6 +859,11 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at && protocol_error .starts_with(AGENT_RUNTIME_AUTONOMOUS_TRUNCATED_SCAFFOLD_ERROR_PREFIX) && !request.function_tools.is_empty(); + let force_autonomous_owner_artifact_delivery = + runtime_owner_artifact_validation_available + && (force_autonomous_specialist_verification_only + || force_autonomous_pending_verification + || force_autonomous_reverify_after_mutation); let force_supervisor_initial_collaboration = agent_runtime_protocol_error_requires_supervisor_collaboration_repair( &protocol_error, @@ -871,6 +886,18 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at { request.function_tools = build_agent_runtime_native_function_tools(&mcp_catalog)?; + if runtime_owner_artifact_validation_available { + remove_autonomous_owner_manual_verification_tools( + &mut request.function_tools, + )?; + } + if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && agent_id == "art-director" + { + remove_autonomous_art_director_non_canvas_validation_tools( + &mut request.function_tools, + )?; + } if !autonomous_project_verify_available { let project_verify_function = native_runtime_function_name("project.verify") @@ -920,12 +947,20 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at request.messages.push(LlmMessage::user(format!( "上一条输出不符合工具计划协议:{protocol_error}\n当前是 autonomous-game-build 的非只读专业任务,本次修复的原生工具目录只保留项目 mutation 工具。必须立即完成本人 run 的实际项目修改;不得 respond_to_user、验证、更新计划、读取、搜索、查询状态或委派。首次源码字段不得超过 {AGENT_RUNTIME_AUTONOMOUS_SCAFFOLD_SOURCE_MAX_CHARS} 字符,完整写入 game/index.html 时必须保持 HTML 与 script 闭合。不要解释,不要 markdown,不要代码围栏。" ))); + } else if force_autonomous_owner_artifact_delivery { + restrict_agent_runtime_autonomous_verified_delivery_repair_tools(&mut request)?; + request.messages.push(LlmMessage::user(format!( + "上一条输出不符合工具计划协议:{protocol_error}\n当前是完整 autonomous DAG 的固定 owner 写入任务。若本人正式产物已经完整,立即 respond_to_user;Runtime 会在收束门内按固定路径和父完成合同 baseline 执行结构验证。不得调用 project.verify、command.run_limited、preview、继续读取或解释;产物仍有缺口时,本轮 finalization 会返回精确 blocker,再按该 blocker 修复。不要 markdown,不要代码围栏。" + ))); } else if force_autonomous_specialist_verification_only { restrict_agent_runtime_autonomous_reverification_repair_tools( &mut request, - autonomous_project_verify_available, + autonomous_project_verify_available + && !code_prototype_requires_static_smoke, )?; - let verification_instruction = if autonomous_project_verify_available { + let verification_instruction = if code_prototype_requires_static_smoke { + "本次修复的原生工具目录只保留 command.run_limited;必须立即调用 game.static_smoke 验证本人 run 的最新 mutation revision,project.verify 不能满足 code-prototype 的可玩入口交付合同" + } else if autonomous_project_verify_available { "本次修复的原生工具目录只保留 project.verify 与 command.run_limited;必须立即验证本人 run 的最新 mutation revision" } else { "当前项目没有可用的 package.json,本次修复的原生工具目录只保留 command.run_limited;必须立即调用 game.static_smoke 验证本人 run 的最新 mutation revision" @@ -981,9 +1016,12 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at } else if force_autonomous_reverify_after_mutation { restrict_agent_runtime_autonomous_reverification_repair_tools( &mut request, - autonomous_project_verify_available, + autonomous_project_verify_available + && !code_prototype_requires_static_smoke, )?; - let verification_instruction = if autonomous_project_verify_available { + let verification_instruction = if code_prototype_requires_static_smoke { + "本次修复的原生工具目录只保留 command.run_limited;必须立即调用 game.static_smoke 验证当前 revision,project.verify 不能满足 code-prototype 的可玩入口交付合同" + } else if autonomous_project_verify_available { "本次修复的原生工具目录只保留 project.verify 与 command.run_limited;必须立即验证当前 revision" } else { "当前项目没有可用的 package.json,本次修复的原生工具目录只保留 command.run_limited;必须立即调用 game.static_smoke 验证当前 revision" @@ -994,9 +1032,12 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at } else if force_autonomous_pending_verification { restrict_agent_runtime_autonomous_verification_repair_tools( &mut request, - autonomous_project_verify_available, + autonomous_project_verify_available + && !code_prototype_requires_static_smoke, )?; - let verification_instruction = if autonomous_project_verify_available { + let verification_instruction = if code_prototype_requires_static_smoke { + "或在项目已经满足要求时立即调用 command.run_limited 的 game.static_smoke;project.verify 只可作为早期诊断,不能满足 code-prototype 完成合同" + } else if autonomous_project_verify_available { "或在项目已经满足要求时立即调用 project.verify / command.run_limited" } else { "或在项目已经满足要求时立即调用 command.run_limited 的 game.static_smoke" 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 eb1fa5779..e099eb362 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 @@ -77,6 +77,30 @@ fn autonomous_game_build_agent_can_generate_canvas_asset(agent_id: &str) -> bool ) } +pub(crate) fn agent_runtime_autonomous_design_foundation_command_is_allowed( + command_id: &str, +) -> bool { + matches!( + command_id, + "memory.read" + | "conversation.read" + | "asset.list" + | "project.index" + | "file.read" + | "project.diff" + | "project.git_inspect" + | "file.list" + | "file.write" + | "file.delete" + | "project.patchset" + | "task.list" + | "image.inspect" + | "canvas.asset_generate" + | "agent.audit" + | "agent.run_status" + ) +} + pub(in crate::agent) fn agent_runtime_tool_policy_snapshot_at( root: &Path, agent_id: &str, @@ -182,6 +206,37 @@ pub(crate) fn agent_runtime_tool_policy_snapshot_for_run_at( .push("canvas.asset_generate".to_string()); } } + if agent_id == "design-foundation" { + for tool in agent_runtime_executable_tools() { + let allowed = game_creator_agent_runtime_tool_command_id(tool) + .is_some_and(agent_runtime_autonomous_design_foundation_command_is_allowed); + if allowed { + continue; + } + snapshot.auto_tools.retain(|candidate| candidate != tool); + snapshot.confirm_tools.retain(|candidate| candidate != tool); + if !snapshot + .denied_tools + .iter() + .any(|candidate| candidate == tool) + { + snapshot.denied_tools.push(tool.to_string()); + } + } + } + if autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)? { + for tool in ["project.verify", "command.run_limited"] { + snapshot.auto_tools.retain(|candidate| candidate != tool); + snapshot.confirm_tools.retain(|candidate| candidate != tool); + if !snapshot + .denied_tools + .iter() + .any(|candidate| candidate == tool) + { + snapshot.denied_tools.push(tool.to_string()); + } + } + } for tool in agent_runtime_executable_tools() { let Some(command_id) = game_creator_agent_runtime_tool_command_id(tool) else { continue; diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs index 5b5839a95..a0d4183f1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver.rs @@ -123,10 +123,12 @@ pub(super) const AGENT_RUNTIME_RUN_PROFILE_BINDING_SCHEMA_VERSION: &str = pub(super) const AGENT_RUNTIME_AUTONOMOUS_COMPLETION_CONTRACT_SCHEMA_VERSION: &str = "game-creator-autonomous-completion-contract.v2"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_PLAYTEST_RECEIPT_SCHEMA_VERSION: &str = - "game-creator-autonomous-playtest-receipt.v1"; + "game-creator-autonomous-playtest-receipt.v2"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES: u64 = 4 * 1024 * 1024; pub(super) const AGENT_RUNTIME_AUTONOMOUS_BROWSER_EVIDENCE_MAX_BYTES: u64 = 16 * 1024 * 1024; pub(super) const AGENT_RUNTIME_GAME_INDEX_PATH: &str = "game/index.html"; +pub(crate) const AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL: &str = + "runtime.owner_artifacts_validate"; pub(super) const AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS: &[&str] = &[ "file.write", "file.delete", @@ -201,6 +203,14 @@ pub(super) struct AgentRuntimeAutonomousPlaytestReceipt { pub(super) agent_id: String, pub(super) run_id: String, pub(super) run_profile_binding_fingerprint: String, + #[serde(default)] + pub(super) executor_agent_id: String, + #[serde(default)] + pub(super) executor_run_id: String, + #[serde(default)] + pub(super) executor_source: String, + #[serde(default)] + pub(super) executor_run_profile_binding_fingerprint: String, pub(super) action_id: String, pub(super) action_fingerprint: String, pub(super) revision: u64, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs index 021ff399c..ae8e3fc0d 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/game_chat_fast_path.rs @@ -1426,6 +1426,13 @@ pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at .ok_or_else(|| "game-chat 美术 child 缺少 Run Profile 绑定".to_string())?; if task.agent_id != agent_id || task.run_id != run_id + || 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 || task.source != "agent-delegate" || task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD || task.status != "running" @@ -1447,6 +1454,27 @@ pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at else { return Ok(false); }; + let Some(parent_binding) = + read_game_creator_agent_runtime_run_profile_binding(root, parent_agent_id, parent_run_id)? + else { + return Ok(false); + }; + let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding( + root, + &child_binding.root_agent_id, + &child_binding.root_run_id, + )? + 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); + }; let parent_is_main = parent.agent_id == "code-prototype" && parent.source == "agent-ready-task-scheduler" && parent.parent_agent_id.as_deref() == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) @@ -1454,6 +1482,38 @@ pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at if !parent_is_main || parent.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD || parent.status != "running" + || parent.delegation_id.is_some() + || 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.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 + || child_binding.parent_binding_fingerprint.as_deref() + != Some(parent_binding.binding_fingerprint.as_str()) + || root_binding.project_id != child_binding.project_id + || root_binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + || root_binding.run_id != child_binding.root_run_id + || root_binding.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE + || root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || root_binding.root_agent_id != root_binding.agent_id + || root_binding.root_run_id != root_binding.run_id + || root_binding.parent_agent_id.is_some() + || root_binding.parent_run_id.is_some() + || 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_some() + || root_task.parent_run_id.is_some() + || root_task.delegation_id.is_some() { return Ok(false); } @@ -1465,14 +1525,187 @@ pub(crate) fn game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at { return Ok(false); } - Ok( - read_game_chat_asset_route_at(root, &child_binding.root_run_id)?.is_some_and(|route| { + let delegation_id = task + .delegation_id + .as_deref() + .expect("validated game-chat art child has delegation id"); + let Some(delivery) = read_static_delegate_delivery_at(root, delegation_id)? else { + return Ok(false); + }; + let expected_delegation_id = agent_runtime_delegation_id( + &parent.agent_id, + &parent.run_id, + &task.agent_id, + &delivery.parent_action_id, + ); + let route_authorizes_agent = read_game_chat_asset_route_at(root, &child_binding.root_run_id)? + .is_some_and(|route| { route .generated_task_ids .iter() .any(|task_id| task_id == agent_id) - }), - ) + }); + Ok(route_authorizes_agent + && delivery.delegation_id == delegation_id + && expected_delegation_id == delegation_id + && delivery.parent_agent_id == parent.agent_id + && delivery.parent_session_id == parent.session_id + && delivery.parent_run_id == parent.run_id + && delivery.target_agent_id == task.agent_id + && delivery.target_session_id == task.session_id + && delivery.target_run_id == task.run_id + && !delivery.acceptance_criteria.is_empty() + && delivery.expected_artifacts.len() == 1 + && delivery.expected_artifacts.first().map(String::as_str) == Some(output_path) + && delivery.repair_of_delegation_id.is_none() + && delivery.status == StaticDelegateDeliveryStatus::Dispatched) +} + +pub(in crate::agent) fn game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + root: &Path, + agent_id: &str, + run_id: &str, +) -> Result { + if agent_id != "art-asset-plan" + || !game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( + root, + agent_id, + run_id, + AGENT_RUNTIME_ART_SPRITESHEET_PATH, + )? + { + return Ok(false); + } + let Some(task) = read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)? + else { + return Ok(false); + }; + let Some(delegation_id) = task + .delegation_id + .as_deref() + .filter(|value| !value.trim().is_empty()) + else { + return Ok(false); + }; + let Some(binding) = + read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? + else { + return Ok(false); + }; + if binding.agent_id != task.agent_id + || binding.run_id != task.run_id + || binding.source != task.source + || binding.profile != task.run_profile + || binding.binding_fingerprint != task.run_profile_binding_fingerprint + || binding.parent_agent_id != task.parent_agent_id + || binding.parent_run_id != task.parent_run_id + || binding.source != "agent-delegate" + || binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || binding.parent_agent_id.as_deref() != Some("code-prototype") + || binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + || task.status != "running" + { + return Ok(false); + } + let parent_agent_id = binding + .parent_agent_id + .as_deref() + .expect("validated game-chat art child has parent agent"); + let parent_run_id = binding + .parent_run_id + .as_deref() + .expect("validated game-chat art child has parent run"); + let Some(parent) = read_latest_game_creator_agent_runtime_task_by_run_id( + root, + parent_agent_id, + parent_run_id, + )? + else { + return Ok(false); + }; + let Some(parent_binding) = + read_game_creator_agent_runtime_run_profile_binding(root, parent_agent_id, parent_run_id)? + else { + return Ok(false); + }; + let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding( + root, + &binding.root_agent_id, + &binding.root_run_id, + )? + 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); + }; + if 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.source != "agent-ready-task-scheduler" + || parent_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || parent_binding.project_id != binding.project_id + || parent_binding.root_agent_id != binding.root_agent_id + || parent_binding.root_run_id != binding.root_run_id + || parent_binding.parent_agent_id.as_deref() != Some(root_binding.agent_id.as_str()) + || parent_binding.parent_run_id.as_deref() != Some(root_binding.run_id.as_str()) + || parent_binding.parent_binding_fingerprint.as_deref() + != Some(root_binding.binding_fingerprint.as_str()) + || binding.parent_binding_fingerprint.as_deref() + != Some(parent_binding.binding_fingerprint.as_str()) + || root_binding.project_id != binding.project_id + || root_binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + || root_binding.run_id != binding.root_run_id + || root_binding.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE + || root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || root_binding.root_agent_id != root_binding.agent_id + || root_binding.root_run_id != root_binding.run_id + || root_binding.parent_agent_id.is_some() + || root_binding.parent_run_id.is_some() + || 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_some() + || root_task.parent_run_id.is_some() + || root_task.delegation_id.is_some() + || parent.status != "running" + { + return Ok(false); + } + let Some(delivery) = read_static_delegate_delivery_at(root, delegation_id)? else { + return Ok(false); + }; + let expected_delegation_id = agent_runtime_delegation_id( + &parent.agent_id, + &parent.run_id, + &task.agent_id, + &delivery.parent_action_id, + ); + Ok(delivery.delegation_id == delegation_id + && expected_delegation_id == delegation_id + && delivery.parent_agent_id == parent.agent_id + && delivery.parent_session_id == parent.session_id + && delivery.parent_run_id == parent.run_id + && delivery.target_agent_id == task.agent_id + && delivery.target_session_id == task.session_id + && delivery.target_run_id == task.run_id + && !delivery.acceptance_criteria.is_empty() + && delivery.expected_artifacts.len() == 1 + && delivery.expected_artifacts.first().map(String::as_str) + == Some(AGENT_RUNTIME_ART_SPRITESHEET_PATH) + && delivery.repair_of_delegation_id.is_none() + && delivery.status == StaticDelegateDeliveryStatus::Dispatched) } pub(in crate::agent) fn game_chat_fast_path_art_slice_paths( 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 ceb4dc3d3..1764bb157 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 @@ -514,6 +514,324 @@ async fn game_chat_main_agent_delegates_only_real_missing_art_and_limits_childre ); } +#[test] +fn game_chat_delegated_art_asset_plan_keeps_canvas_verification_and_rejects_borrowed_lineage() { + let temporary = tempfile::tempdir().expect("create game-chat Canvas delivery root"); + let root = temporary.path().join("project"); + init_local_game_project_at(&root, "game-chat-art-child", "单主美术委派") + .expect("pre-initialize game-chat Canvas delivery project"); + let _child_runtime_lane = + try_acquire_game_creator_agent_runtime_task_lock(&root, "art-asset-plan") + .expect("acquire game-chat Canvas art child runtime lane") + .expect("game-chat Canvas art child runtime lane is free"); + let (_main, mut child, delegation_id) = + game_chat_main_art_child_fixture(&root, "art-asset-plan", &["core-spritesheet"]); + child.status = "running".to_string(); + child.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&root, &child) + .expect("persist running game-chat art child"); + write_game_creator_agent_runtime_state(&root, &child) + .expect("persist running game-chat art runtime"); + + assert!(!autonomous_owner_artifact_validation_available_for_run_at( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("evaluate fixed owner validation identity")); + assert!( + game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("validate game-chat delegated art lineage") + ); + + { + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + &root, + "test.game-chat-canvas-delivery", + ) + .expect("lock game-chat Canvas delivery root"); + prepare_agent_runtime_project_mutation_locked( + &root, + &child.agent_id, + &child.run_id, + "canvas.asset_generate", + ) + .expect("record Canvas mutation"); + let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( + &root, + &child.agent_id, + &child.run_id, + "canvas.asset_generate", + ) + .expect("begin Canvas verification"); + finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true) + .expect("finish Canvas verification"); + } + let plan = AgentRuntimeToolPlan { + response: "已按委派生成并登记图集素材。".to_string(), + ..AgentRuntimeToolPlan::default() + }; + let gate = + read_game_creator_agent_runtime_verification_gate(&root, &child.agent_id, &child.run_id) + .expect("read Canvas verification gate"); + assert_eq!( + gate.last_verification_tool.as_deref(), + Some("canvas.asset_generate") + ); + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, + &child.agent_id, + &child.run_id, + false, + false, + &gate, + &plan, + ) + .expect("trusted game-chat art child may finalize with Canvas verification"); + + let mut borrowed_project_verify_gate = gate.clone(); + borrowed_project_verify_gate.last_verification_tool = Some("project.verify".to_string()); + write_game_creator_agent_runtime_verification_gate(&root, &borrowed_project_verify_gate) + .expect("persist deliberately borrowed project.verify gate"); + assert!( + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, + &child.agent_id, + &child.run_id, + false, + false, + &borrowed_project_verify_gate, + &plan, + ) + .expect_err("trusted lineage must still require its own Canvas credential") + .contains("只接受本人 canvas.asset_generate") + ); + let borrowed_completion_blocker = + project_verification_completion_blocker_at(&root, &child.agent_id, &child.run_id, &[]) + .expect("borrowed project.verify must fail the completion gate"); + assert!(borrowed_completion_blocker + .summary + .contains("Canvas 交付凭证")); + write_game_creator_agent_runtime_verification_gate(&root, &gate) + .expect("restore Canvas verification gate"); + let restored_blocker = + project_verification_completion_blocker_at(&root, &child.agent_id, &child.run_id, &[]); + assert!( + restored_blocker.is_none(), + "restored Canvas credential must satisfy the dynamic art completion gate: {restored_blocker:?}" + ); + assert_eq!( + read_game_creator_agent_runtime_verification_gate(&root, &child.agent_id, &child.run_id,) + .expect("reread Canvas verification gate") + .last_verification_tool + .as_deref(), + Some("canvas.asset_generate") + ); + + let delivery = read_static_delegate_delivery_at(&root, &delegation_id) + .expect("read game-chat Canvas delivery") + .expect("game-chat Canvas delivery exists"); + let mut wrong_artifact_delivery = delivery.clone(); + wrong_artifact_delivery.expected_artifacts = vec!["assets/other.png".to_string()]; + write_agent_runtime_json_sidecar( + &root, + &format!(".agent/runtime/delegation-deliveries/{delegation_id}.json"), + "forged game-chat Canvas delivery", + &wrong_artifact_delivery, + ) + .expect("persist forged game-chat Canvas artifact contract"); + assert!( + !game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("reject game-chat Canvas delivery with wrong expected artifact") + ); + write_agent_runtime_json_sidecar( + &root, + &format!(".agent/runtime/delegation-deliveries/{delegation_id}.json"), + "restored game-chat Canvas delivery", + &delivery, + ) + .expect("restore game-chat Canvas artifact contract"); + + // A task journal and binding that say `agent-delegate` are not sufficient: + // removing the Runtime-issued durable delivery makes the otherwise valid + // game-chat lineage ineligible for the narrow Canvas exception. + fs::remove_file(root.join(format!( + ".agent/runtime/delegation-deliveries/{delegation_id}.json" + ))) + .expect("remove durable delegation proof"); + let forged_asset_path = "assets/forged-without-delivery.txt"; + let forged_asset_write = observe_agent_runtime_file_write( + &root, + &child.agent_id, + &child.run_id, + &AgentRuntimeToolAction { + tool: "file.write".to_string(), + reason: Some("验证伪造 game-chat 美术 lineage 不能取得 assets 写权限".to_string()), + input: serde_json::json!({ + "path": forged_asset_path, + "content": "forged delegated art mutation", + }), + }, + &"0".repeat(64), + None, + ); + assert_eq!( + forged_asset_write.status, "blocked", + "{forged_asset_write:?}" + ); + assert!( + !root.join(forged_asset_path).exists(), + "missing durable delivery must be rejected before assets/** mutation" + ); + assert!( + !game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("reject forged delegated source without durable delivery") + ); + let forged_completion_blocker = + project_verification_completion_blocker_at(&root, &child.agent_id, &child.run_id, &[]) + .expect("forged delegated source must also fail the durable completion gate"); + assert!(forged_completion_blocker + .summary + .contains("不具备可用的验证身份")); + assert!( + validate_agent_runtime_autonomous_specialist_response_delivery( + &root, + &child.agent_id, + &child.run_id, + false, + false, + &gate, + &plan, + ) + .expect_err("forged delegated source must not borrow Canvas finalization") + .contains("固定 owner 的 Runtime 内部产物验证身份不可用") + ); + + write_agent_runtime_json_sidecar( + &root, + &format!(".agent/runtime/delegation-deliveries/{delegation_id}.json"), + "restored game-chat Canvas delivery before root source forgery", + &delivery, + ) + .expect("restore durable delivery before root source forgery"); + assert!( + game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("restored game-chat Canvas lineage remains valid") + ); + let root_run_id = "game-chat-main-art-root"; + let root_binding = read_game_creator_agent_runtime_run_profile_binding( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + root_run_id, + ) + .expect("read game-chat Canvas root binding") + .expect("game-chat Canvas root binding exists"); + assert_eq!( + root_binding.source, + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE + ); + let root_task = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + root_run_id, + ) + .expect("read game-chat Canvas root task") + .expect("game-chat Canvas root task exists"); + let mut wrong_source_root = agent_runtime_state_from_task_record(&root_task); + // Keep the signed binding and route coherent so this reaches the + // task-versus-binding cross-check instead of failing on a broken parent chain. + wrong_source_root.source = AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE.to_string(); + append_game_creator_agent_runtime_task(&root, &wrong_source_root) + .expect("persist forged non-game-chat root task"); + assert!( + !game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + &root, + &child.agent_id, + &child.run_id, + ) + .expect("reject Canvas exception outside a game-chat root") + ); + + let ordinary_temporary = tempfile::tempdir().expect("create ordinary delegated owner root"); + let ordinary_root = ordinary_temporary.path().join("project"); + init_local_game_project_at( + &ordinary_root, + "ordinary-delegated-owner", + "普通委派不能借 game-chat 例外", + ) + .expect("init ordinary delegated owner project"); + let supervisor_session = resolve_agent_conversation_session_id_at( + &ordinary_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + None, + true, + ) + .expect("resolve ordinary Supervisor session"); + let supervisor_record = append_unique_game_creator_agent_runtime_pending_task( + &ordinary_root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &supervisor_session, + "普通 GUI 自主任务", + "ordinary-delegated-root-run", + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("queue ordinary Supervisor"); + let mut supervisor_state = agent_runtime_state_from_task_record(&supervisor_record); + supervisor_state.status = "running".to_string(); + supervisor_state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&ordinary_root, &supervisor_state) + .expect("persist running ordinary Supervisor"); + let ordinary_session = + resolve_agent_conversation_session_id_at(&ordinary_root, "art-asset-plan", None, true) + .expect("resolve ordinary art session"); + let ordinary_record = append_unique_game_creator_agent_runtime_pending_task( + &ordinary_root, + "art-asset-plan", + &ordinary_session, + "普通 delegated 美术任务", + "ordinary-delegated-art-run", + "agent-delegate", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(supervisor_state.agent_id.clone()), + parent_run_id: Some(supervisor_state.run_id.clone()), + delegation_id: Some("ordinary-delegation".to_string()), + }), + ) + .expect("queue ordinary delegated art task"); + let mut ordinary_child = agent_runtime_state_from_task_record(&ordinary_record); + ordinary_child.status = "running".to_string(); + ordinary_child.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&ordinary_root, &ordinary_child) + .expect("persist running ordinary art child"); + assert!( + !game_chat_delegated_art_asset_plan_uses_canvas_verification_at( + &ordinary_root, + &ordinary_child.agent_id, + &ordinary_child.run_id, + ) + .expect("ordinary delegated source is not game-chat lineage") + ); +} + #[test] fn game_chat_main_agent_rejects_unneeded_or_wrong_art_delegation() { let temporary = tempfile::tempdir().expect("create wrong art delegation root"); @@ -838,6 +1156,41 @@ fn prepare_autonomous_completion_evidence( prepare_autonomous_completion_evidence_for_actor(root, state, state, expect_complete) } +fn start_full_dag_playtest_child_for_completion_fixture( + root: &Path, + parent_state: &AgentRuntimeState, +) -> AgentRuntimeState { + let manifest = read_manifest_for_project(root).expect("read completion fixture manifest"); + let task = manifest + .tasks + .iter() + .find(|task| task.id == "preview-playtest") + .expect("completion fixture preview-playtest task"); + let session_id = resolve_agent_conversation_session_id_at(root, "preview-playtest", None, true) + .expect("resolve completion fixture preview-playtest session"); + let record = append_unique_game_creator_agent_runtime_pending_task( + root, + "preview-playtest", + &session_id, + &render_autonomous_manifest_ready_task_background_prompt(task), + &autonomous_manifest_ready_task_run_id(&parent_state.run_id, "preview-playtest"), + "agent-ready-task-scheduler", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(parent_state.agent_id.clone()), + parent_run_id: Some(parent_state.run_id.clone()), + delegation_id: None, + }), + ) + .expect("queue completion fixture preview-playtest child"); + let mut state = agent_runtime_state_from_task_record(&record); + state.status = "running".to_string(); + state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(root, &state) + .expect("persist running completion fixture preview-playtest child"); + state +} + fn prepare_autonomous_completion_evidence_for_actor( root: &Path, contract_state: &AgentRuntimeState, @@ -848,6 +1201,19 @@ fn prepare_autonomous_completion_evidence_for_actor( read_autonomous_completion_contract(root, &contract_state.agent_id, &contract_state.run_id) .expect("read autonomous completion contract") .expect("autonomous completion contract exists"); + let root_binding = read_game_creator_agent_runtime_run_profile_binding( + root, + &contract_state.agent_id, + &contract_state.run_id, + ) + .expect("read completion fixture root binding") + .expect("completion fixture root binding exists"); + let full_dag_playtest_state = matches!( + root_binding.source.as_str(), + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE + ) + .then(|| start_full_dag_playtest_child_for_completion_fixture(root, contract_state)); + let playtest_state = full_dag_playtest_state.as_ref().unwrap_or(actor_state); let revision = { let latest = read_latest_game_creator_agent_runtime_task_by_run_id( root, @@ -943,11 +1309,11 @@ fn prepare_autonomous_completion_evidence_for_actor( let evidence_root = root .join(".agent/runtime/browser-validations") .join(agent_runtime_confirmation_path_component( - &contract_state.agent_id, + &playtest_state.agent_id, "agent", )) .join(agent_runtime_confirmation_path_component( - &contract_state.run_id, + &playtest_state.run_id, "run", )) .join(revision.to_string()); @@ -1045,17 +1411,27 @@ fn prepare_autonomous_completion_evidence_for_actor( input: serde_json::json!({}), }; let action_fingerprint = - agent_runtime_tool_action_fingerprint(&action, &actor_state.current_task); - let action_id = agent_runtime_tool_action_id(&actor_state.run_id, 1, 0, 1, &action_fingerprint); + agent_runtime_tool_action_fingerprint(&action, &playtest_state.current_task); + let action_id = + agent_runtime_tool_action_id(&playtest_state.run_id, 1, 0, 1, &action_fingerprint); write_autonomous_playtest_receipt_at( root, &contract, + playtest_state, &action_id, &action_fingerprint, revision, &result, ) .expect("write autonomous playtest receipt"); + if let Some(playtest_state) = full_dag_playtest_state.as_ref() { + let mut completed = playtest_state.clone(); + completed.status = "completed".to_string(); + completed.phase = "completed".to_string(); + completed.current_action = "已完成独立桌面与移动试玩".to_string(); + append_game_creator_agent_runtime_task(root, &completed) + .expect("complete fixture preview-playtest child after receipt"); + } if expect_complete { let blocker = autonomous_game_build_completion_blocker_at_locked(root, actor_state); assert!( @@ -1072,11 +1448,11 @@ fn game_chat_preview_playtest_migrates_legacy_generic_tetris_receipt_before_deli let root = temporary.path().join("project"); init_local_game_project_at(&root, "legacy-game-chat-tetris", "水晶俄罗斯方块") .expect("init project"); - let (root_state, preview_state) = queue_game_chat_fast_path_child( + let (root_state, mut main_state) = queue_game_chat_fast_path_child( &root, "legacy-game-chat-tetris-root", "做一个俄罗斯方块,包含旋转、重力下落、锁定和消行", - "preview-playtest", + "code-prototype", ); let mut legacy_contract = read_autonomous_completion_contract(&root, &root_state.agent_id, &root_state.run_id) @@ -1095,30 +1471,24 @@ fn game_chat_preview_playtest_migrates_legacy_generic_tetris_receipt_before_deli &legacy_contract, ) .expect("persist legacy Generic Tetris contract"); - prepare_autonomous_completion_evidence(&root, &root_state, false); + prepare_autonomous_completion_evidence_for_actor(&root, &root_state, &main_state, false); + main_state.status = "running".to_string(); + main_state.phase = "planning".to_string(); + persist_game_chat_main_asset_audit_and_route(&root, &root_state, &mut main_state); - let bound_at = read_game_creator_agent_runtime_run_profile_binding( - &root, - &root_state.agent_id, - &root_state.run_id, - ) - .expect("read root binding") - .expect("root binding exists") - .bound_at; - let plan = - game_chat_fast_path_plan_at(&root, &preview_state, &preview_state.current_task, bound_at) - .expect("evaluate migrated preview-playtest fast path") - .expect("preview-playtest emits a deterministic plan"); - assert_eq!(plan.actions.len(), 1, "unexpected plan: {plan:?}"); - assert_eq!(plan.actions[0].tool, "preview.validate"); - let migrated = - read_autonomous_completion_contract(&root, &root_state.agent_id, &root_state.run_id) - .expect("read migrated root contract") - .expect("migrated root contract exists"); + let migrated = autonomous_playtest_execution_contract_for_state_at(&root, &main_state) + .expect("evaluate migrated code-prototype preview execution contract") + .expect("code-prototype inherits the migrated root contract"); assert_eq!( migrated.playtest_scenario, BrowserPlaytestScenario::TetrisV1 ); + assert!( + read_autonomous_playtest_receipt(&root, &migrated) + .expect("legacy Generic receipt becomes recoverably stale") + .is_none(), + "the Generic receipt must not satisfy the migrated Tetris contract" + ); } #[test] @@ -3297,7 +3667,7 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac read_game_creator_agent_runtime_at(&root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) .expect("read autonomous Supervisor runtime") .state; - for _ in 0..500 { + for _ in 0..1_500 { if runtime.status == "idle" || runtime.status == "failed" { break; } @@ -3310,7 +3680,11 @@ async fn autonomous_supervisor_converged_final_reply_deserialize_commits_fallbac let fallback = format!( "项目已完成生成,并通过当前 revision {revision} 的静态检查和桌面、移动端交互试玩验证。" ); - assert_eq!(runtime.status, "idle"); + assert_eq!( + runtime.status, "idle", + "phase={}, currentAction={}, waitingOn={}, error={:?}", + runtime.phase, runtime.current_action, runtime.waiting_on, runtime.error + ); assert_eq!(runtime.phase, "completed"); assert_eq!(runtime.last_response.as_deref(), Some(fallback.as_str())); 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 45d47c634..b74cd5576 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 @@ -1460,8 +1460,14 @@ fn render_autonomous_manifest_ready_task_owner_prompt(task: &GameCreationAppTask } else { "" }; + let verification_requirement = match task.id.as_str() { + "code-prototype" => "code-prototype 必须对可玩入口执行 game.static_smoke;完整 DAG 的最终静态与浏览器验收继续由后续质量任务承担。", + task_id if agent_runtime_autonomous_uses_owner_artifact_validation(task_id) => "完成固定正式产物后直接交付,由 Runtime 在收束门内验证本人固定 owner 产物;禁止调用 game.static_smoke、project.verify、command.run_limited 或 preview 工具冒充 owner 产物验证。", + "publish-package" => "publish-package 不在前置固定 owner 的内部产物验证范围内;必须继续按现有发布完成合同和当前 run 的可用验证门收束,不得借用前置 owner 的验证凭证。", + _ => "完成修改后按当前任务的既有验证合同收束。", + }; format!( - "{base}\n\n这是 autonomous-game-build 的正式 owner 写入任务。必须实际生成并写入非空正式产物:{paths};JSON 文件必须是可解析 JSON,code-prototype 的 game/index.html 不能沿用初始化占位。{publish_package_requirement}任务声明中的视觉图片继续按现有 visual gate 生成、登记并验收。完成修改后按当前 run 的验证门完成验证并直接交付结论;不要调用 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。" + "{base}\n\n这是 autonomous-game-build 的正式 owner 写入任务。必须实际生成并写入非空正式产物:{paths};JSON 文件必须是可解析 JSON,code-prototype 的 game/index.html 不能沿用初始化占位。{publish_package_requirement}任务声明中的视觉图片继续按现有 visual gate 生成、登记并验收。{verification_requirement}不要调用 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。" ) } @@ -1491,6 +1497,16 @@ pub(in crate::agent) fn render_autonomous_manifest_ready_task_background_prompt( "{base}\n\n这是 autonomous-game-build 的只读试玩验收任务,不要修改项目文件。固定核心动作是且只能是 preview.validate;完成当前 revision 的桌面与移动试玩后直接交付验收结论,不要调用项目 mutation、其它预览动作或 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。" ); } + if task.id == "art-director" { + if autonomous_manifest_ready_task_requires_visual_asset(&task.id) { + return format!( + "{base}\n\n这是 autonomous-game-build 的条件 Canvas owner 任务。当前已配置 External Editor 生图凭据,必须按角色固定合同生成并登记 assets/art-spec.png。canvas.asset_generate 成功只表示候选已生成并登记,不等于视觉门已经通过;完成后直接交付,由 Runtime 在收束时核对当前 revision、Canvas 登记、资源身份和视觉产物门。项目 mutation 仅限固定 Canvas 产物;不得调用 game.static_smoke、project.verify、command.run_limited、preview 或 task.update,Runtime 会在子 Run 终态后幂等投影 manifest。" + ); + } + return format!( + "{base}\n\n这是 autonomous-game-build 的无生图凭据只读协调任务。当前未配置 External Editor 生图凭据,上述 seed task 中 assets/art-spec.png 图片产物与生成验收条款在本轮不适用;只交付正式视觉方向结论,不要修改项目文件,不调用 canvas.asset_generate、game.static_smoke、project.verify、command.run_limited、preview 或 task.update。Runtime 会在子 Run 终态后幂等投影 manifest。" + ); + } if autonomous_manifest_task_requires_project_mutation(&task.id) { let owner_prompt = render_autonomous_manifest_ready_task_owner_prompt(task); let code_visual_asset_requirement = @@ -1535,3 +1551,89 @@ pub(in crate::agent) fn render_manifest_ready_task_background_prompt( acceptance ) } + +#[cfg(test)] +mod tests { + use super::*; + + fn seed_task(task_id: &str) -> GameCreationAppTaskState { + new_game_creation_app_seed_tasks() + .into_iter() + .find(|task| task.id == task_id) + .unwrap_or_else(|| panic!("missing seed task {task_id}")) + } + + #[test] + fn autonomous_ready_task_prompts_separate_owner_artifacts_from_playable_validation() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + + for task_id in [ + "design-foundation", + "balance-seed", + "art-asset-plan", + "audio-asset-plan", + ] { + let prompt = + render_autonomous_manifest_ready_task_background_prompt(&seed_task(task_id)); + assert!(prompt.contains("由 Runtime 在收束门内验证本人固定 owner 产物")); + assert!( + prompt.contains("禁止调用 game.static_smoke、project.verify、command.run_limited") + ); + } + + let code_prompt = + render_autonomous_manifest_ready_task_background_prompt(&seed_task("code-prototype")); + assert!(code_prompt.contains("必须对可玩入口执行 game.static_smoke")); + assert!(!code_prompt.contains("验证本人固定 owner 产物")); + + let readiness_prompt = render_autonomous_manifest_ready_task_background_prompt(&seed_task( + "preview-readiness", + )); + assert!( + readiness_prompt.contains("且只能是 command.run_limited(commandId=game.static_smoke)") + ); + assert!(!readiness_prompt.contains("preview.validate")); + + let publish_prompt = + render_autonomous_manifest_ready_task_background_prompt(&seed_task("publish-package")); + assert!(publish_prompt.contains("不在前置固定 owner 的内部产物验证范围内")); + assert!(!publish_prompt.contains("验证本人固定 owner 产物")); + } + + #[test] + fn art_director_ready_task_is_read_only_without_key_and_canvas_owner_with_key() { + let task = seed_task("art-director"); + { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let prompt = render_autonomous_manifest_ready_task_background_prompt(&task); + assert!(prompt.contains("无生图凭据只读协调任务")); + assert!(prompt.contains("assets/art-spec.png 图片产物与生成验收条款在本轮不适用")); + assert!(prompt.contains("不要修改项目文件")); + assert!(!autonomous_manifest_ready_task_requires_visual_asset( + "art-director" + )); + assert!(agent_runtime_task_requires_read_only_delivery( + "art-director", + &prompt + )); + } + { + let _config_guard = crate::tests::write_test_local_config( + r#"{"editorApi":{"apiKey":"art-director-ready-task-key"}}"#.to_string(), + ); + let prompt = render_autonomous_manifest_ready_task_background_prompt(&task); + assert!(prompt.contains("条件 Canvas owner 任务")); + assert!(prompt.contains("生成并登记 assets/art-spec.png")); + assert!(prompt.contains("成功只表示候选已生成并登记,不等于视觉门已经通过")); + assert!(prompt.contains("由 Runtime 在收束时核对当前 revision")); + assert!(!prompt.contains("成功会为本人当前 revision 形成验证凭证")); + assert!(autonomous_manifest_ready_task_requires_visual_asset( + "art-director" + )); + assert!(!agent_runtime_task_requires_read_only_delivery( + "art-director", + &prompt + )); + } + } +} diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs index 8d2d866d8..a84fdcfa8 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs @@ -413,6 +413,114 @@ fn autonomous_manifest_owner_artifact_gaps_at( Ok(gaps) } +pub(in crate::agent) fn autonomous_owner_artifact_validation_available_for_run_at( + root: &Path, + agent_id: &str, + run_id: &str, +) -> Result { + if !matches!( + agent_id, + "design-foundation" | "balance-seed" | "art-asset-plan" | "audio-asset-plan" + ) { + return Ok(false); + } + let Some(binding) = + read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? + else { + return Ok(false); + }; + if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || binding.source != "agent-ready-task-scheduler" + || binding.agent_id != agent_id + || binding.run_id != run_id + || binding.parent_agent_id.as_deref() != Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID) + || binding.parent_run_id.as_deref() != Some(binding.root_run_id.as_str()) + || binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + { + return Ok(false); + } + let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding( + root, + &binding.root_agent_id, + &binding.root_run_id, + )? + else { + return Err("owner-artifact 验证缺少根 Supervisor Run Profile 绑定".to_string()); + }; + Ok( + root_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && matches!( + root_binding.source.as_str(), + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE + ) + && root_binding.agent_id == binding.root_agent_id + && root_binding.run_id == binding.root_run_id + && root_binding.root_agent_id == root_binding.agent_id + && root_binding.root_run_id == root_binding.run_id + && root_binding.parent_agent_id.is_none() + && root_binding.parent_run_id.is_none() + && binding.parent_binding_fingerprint.as_deref() + == Some(root_binding.binding_fingerprint.as_str()), + ) +} + +pub(in crate::agent) fn validate_autonomous_owner_artifacts_for_run_at_locked( + root: &Path, + agent_id: &str, + run_id: &str, +) -> Result<&'static [&'static str], String> { + if !autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id)? { + return Err( + "owner-artifact 验证只允许完整 autonomous DAG 的固定 owner 写入任务".to_string(), + ); + } + ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, run_id)?; + let revision = read_game_creator_agent_runtime_project_revision(root)?; + let gate = read_game_creator_agent_runtime_verification_gate(root, agent_id, run_id)?; + let mutation_revision = gate + .mutation_revision + .filter(|value| *value > 0) + .ok_or_else(|| "owner-artifact 验证要求当前 run 已形成 mutationRevision".to_string())?; + if !gate.requires_verification || mutation_revision > revision.revision { + return Err("owner-artifact 验证门与当前项目 revision 不一致".to_string()); + } + let binding = read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? + .ok_or_else(|| "owner-artifact 验证缺少当前 Run Profile 绑定".to_string())?; + let parent_agent_id = binding + .parent_agent_id + .as_deref() + .ok_or_else(|| "owner-artifact 验证缺少 parentAgentId".to_string())?; + let parent_run_id = binding + .parent_run_id + .as_deref() + .ok_or_else(|| "owner-artifact 验证缺少 parentRunId".to_string())?; + let parent_contract = + read_autonomous_completion_contract(root, parent_agent_id, parent_run_id)? + .ok_or_else(|| "owner-artifact 验证缺少父 Supervisor 完成合同".to_string())?; + if binding.parent_binding_fingerprint.as_deref() + != Some(parent_contract.run_profile_binding_fingerprint.as_str()) + { + return Err("owner-artifact 验证与父 Supervisor 完成合同绑定不一致".to_string()); + } + let gaps = autonomous_manifest_owner_artifact_gaps_at( + root, + agent_id, + parent_contract.baseline_index_sha256.as_deref(), + &parent_contract.baseline_artifacts, + )?; + if !gaps.is_empty() { + return Err(format!( + "owner-artifact 验证未通过:{}", + autonomous_manifest_artifact_gaps_detail(&gaps) + )); + } + let paths = autonomous_manifest_owner_artifact_paths(agent_id); + if paths.is_empty() { + return Err("owner-artifact 验证合同没有固定产物路径".to_string()); + } + Ok(paths) +} + fn autonomous_code_prototype_art_asset_reference_gap_at( root: &Path, task_id: &str, @@ -5893,6 +6001,67 @@ pub(in crate::agent) fn game_creation_app_task_status_label( .unwrap_or_else(|| "unknown".to_string()) } +fn autonomous_manifest_completed_code_static_smoke_gap_at( + root: &Path, + contract: &AgentRuntimeAutonomousCompletionContract, + task_id: &str, +) -> Result, String> { + if task_id != "code-prototype" { + return Ok(None); + } + let child_run_id = autonomous_manifest_ready_task_run_id(&contract.run_id, task_id); + let Some(task) = + read_latest_game_creator_agent_runtime_task_by_run_id(root, task_id, &child_run_id)? + else { + return Ok(None); + }; + let invalid = |detail: &str| { + Some(AutonomousManifestArtifactGap::new(format!( + "{task_id}(verification-invalid:{detail})" + ))) + }; + if task.status != "completed" + || task.phase != "completed" + || task.source != "agent-ready-task-scheduler" + || task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || task.parent_agent_id.as_deref() != Some(contract.agent_id.as_str()) + || task.parent_run_id.as_deref() != Some(contract.run_id.as_str()) + || task.delegation_id.is_some() + { + return Ok(invalid("terminal-child-identity")); + } + let Some(binding) = + read_game_creator_agent_runtime_run_profile_binding(root, task_id, &child_run_id)? + else { + return Ok(invalid("missing-run-profile-binding")); + }; + if binding.agent_id != task.agent_id + || binding.run_id != task.run_id + || binding.source != task.source + || binding.profile != task.run_profile + || binding.binding_fingerprint != task.run_profile_binding_fingerprint + || binding.root_agent_id != contract.agent_id + || binding.root_run_id != contract.run_id + || binding.parent_agent_id.as_deref() != Some(contract.agent_id.as_str()) + || binding.parent_run_id.as_deref() != Some(contract.run_id.as_str()) + || binding.parent_binding_fingerprint.as_deref() + != Some(contract.run_profile_binding_fingerprint.as_str()) + { + return Ok(invalid("run-profile-binding")); + } + let gate = read_game_creator_agent_runtime_verification_gate(root, task_id, &child_run_id)?; + let Some(mutation_revision) = gate.mutation_revision.filter(|value| *value > 0) else { + return Ok(invalid("missing-code-mutation")); + }; + if !gate + .static_smoke_verified_revision + .is_some_and(|verified_revision| verified_revision >= mutation_revision) + { + return Ok(invalid("code-static-smoke")); + } + Ok(None) +} + fn autonomous_manifest_parent_completion_gaps_at( root: &Path, contract: &AgentRuntimeAutonomousCompletionContract, @@ -5952,6 +6121,11 @@ fn autonomous_manifest_parent_completion_gaps_at( if !completed { continue; } + if let Some(gap) = + autonomous_manifest_completed_code_static_smoke_gap_at(root, contract, &seed_task.id)? + { + missing_paths.push(gap); + } let mut owner_artifact_gaps = autonomous_manifest_owner_artifact_gaps_at( root, &seed_task.id, @@ -6320,6 +6494,47 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked( } } if gaps.is_empty() { + if state.agent_id == "code-prototype" + && matches!( + root_source.as_str(), + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE + ) + { + let gate = match read_game_creator_agent_runtime_verification_gate( + root, + &state.agent_id, + &state.run_id, + ) { + Ok(gate) => gate, + Err(error) => { + return Some(autonomous_completion_blocker( + "code-prototype 静态验证凭证不可用", + error, + )); + } + }; + let Some(mutation_revision) = gate.mutation_revision.filter(|revision| *revision > 0) + else { + return Some(autonomous_completion_blocker( + "code-prototype 尚未形成本人 run 的项目修改", + "完整 GUI/CLI autonomous DAG 的 code-prototype 必须先实际修改可玩入口,再由本人执行 game.static_smoke。", + )); + }; + if !gate + .static_smoke_verified_revision + .is_some_and(|revision| revision >= mutation_revision) + { + return Some(autonomous_completion_blocker( + "code-prototype 尚未通过覆盖本人最后一次修改的 game.static_smoke", + format!( + "mutationRevision={mutation_revision}, staticSmokeVerifiedRevision={};project.verify 或其它 Agent 的验证凭证不能替代本人 run 的 game.static_smoke。", + gate.static_smoke_verified_revision + .map(|revision| revision.to_string()) + .unwrap_or_else(|| "none".to_string()) + ), + )); + } + } return None; } let next_required_action = autonomous_manifest_artifact_next_required_action(&gaps) @@ -6702,6 +6917,10 @@ pub(in crate::agent) fn autonomous_playtest_receipt_fingerprint( "agentId": receipt.agent_id, "runId": receipt.run_id, "runProfileBindingFingerprint": receipt.run_profile_binding_fingerprint, + "executorAgentId": receipt.executor_agent_id, + "executorRunId": receipt.executor_run_id, + "executorSource": receipt.executor_source, + "executorRunProfileBindingFingerprint": receipt.executor_run_profile_binding_fingerprint, "actionId": receipt.action_id, "actionFingerprint": receipt.action_fingerprint, "revision": receipt.revision, @@ -7158,7 +7377,7 @@ fn reset_cancelled_reconciliation_manifest_tasks_for_continuation_at( .into_iter() .map(|task| task.id) .collect::>(); - let _lock = acquire_project_write_lock( + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( root, "runtime.autonomous.manifest.reconciliation_cancel_retry", )?; @@ -14510,7 +14729,10 @@ fn reset_autonomous_manifest_seed_tasks_at( root: &Path, task: &AgentRuntimeTaskRecord, ) -> Result<(), String> { - let _lock = acquire_project_write_lock(root, "runtime.autonomous.manifest.reset")?; + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + root, + "runtime.autonomous.manifest.reset", + )?; let (manifest_path, mut manifest) = read_or_create_manifest(root)?; ensure_manifest_seed_tasks(root, &mut manifest); let seed_task_ids = new_game_creation_app_seed_tasks() @@ -14579,6 +14801,196 @@ pub(in crate::agent) fn validate_autonomous_evidence_digest( Ok(()) } +fn autonomous_playtest_executor_agent_id_for_root_source( + source: &str, +) -> Result<&'static str, String> { + match source { + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE | AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE => { + Ok("preview-playtest") + } + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE => Ok("code-prototype"), + _ => Err("自主试玩根 Run 来源不受信任".to_string()), + } +} + +fn validate_autonomous_playtest_executor_state_lineage_at( + root: &Path, + state: &AgentRuntimeState, +) -> Result<(AgentRuntimeRunProfileBinding, AgentRuntimeRunProfileBinding), String> { + if state.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { + return Err("自主试玩执行 Run Profile 无效".to_string()); + } + ensure_current_autonomous_ready_child_mutation_at_locked(root, &state.agent_id, &state.run_id) + .map_err(|error| format!("自主试玩执行 child 身份不可用:{error}"))?; + let binding = + read_game_creator_agent_runtime_run_profile_binding(root, &state.agent_id, &state.run_id)? + .ok_or_else(|| "自主试玩执行 child 缺少 Run Profile 绑定".to_string())?; + let root_binding = read_game_creator_agent_runtime_run_profile_binding( + root, + &binding.root_agent_id, + &binding.root_run_id, + )? + .ok_or_else(|| "自主试玩执行 child 缺少根 Supervisor Run Profile 绑定".to_string())?; + let expected_agent_id = + autonomous_playtest_executor_agent_id_for_root_source(&root_binding.source)?; + let expected_run_id = + autonomous_manifest_ready_task_run_id(&root_binding.run_id, expected_agent_id); + if root_binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + || root_binding.root_agent_id != root_binding.agent_id + || root_binding.root_run_id != root_binding.run_id + || root_binding.parent_agent_id.is_some() + || root_binding.parent_run_id.is_some() + || root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || binding.agent_id != state.agent_id + || binding.run_id != state.run_id + || binding.binding_fingerprint != state.run_profile_binding_fingerprint + || binding.source != "agent-ready-task-scheduler" + || binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || binding.root_agent_id != root_binding.agent_id + || binding.root_run_id != root_binding.run_id + || binding.parent_agent_id.as_deref() != Some(root_binding.agent_id.as_str()) + || binding.parent_run_id.as_deref() != Some(root_binding.run_id.as_str()) + || binding.parent_binding_fingerprint.as_deref() + != Some(root_binding.binding_fingerprint.as_str()) + || state.agent_id != expected_agent_id + || state.run_id != expected_run_id + || state.source != binding.source + || state.parent_agent_id != binding.parent_agent_id + || state.parent_run_id != binding.parent_run_id + || state.delegation_id.is_some() + { + return Err(format!( + "自主试玩必须由当前根 Run 的确定性 {expected_agent_id} child 独立执行" + )); + } + Ok((binding, root_binding)) +} + +fn validate_autonomous_playtest_executor_receipt_identity_at( + root: &Path, + contract: &AgentRuntimeAutonomousCompletionContract, + receipt: &AgentRuntimeAutonomousPlaytestReceipt, +) -> Result<(), String> { + let root_binding = read_game_creator_agent_runtime_run_profile_binding( + root, + &contract.agent_id, + &contract.run_id, + )? + .ok_or_else(|| "自主试玩回执缺少根 Supervisor Run Profile 绑定".to_string())?; + let expected_agent_id = + autonomous_playtest_executor_agent_id_for_root_source(&root_binding.source)?; + let expected_run_id = + autonomous_manifest_ready_task_run_id(&root_binding.run_id, expected_agent_id); + if root_binding.project_id != contract.project_id + || root_binding.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID + || root_binding.run_id != contract.run_id + || root_binding.root_agent_id != root_binding.agent_id + || root_binding.root_run_id != root_binding.run_id + || root_binding.parent_agent_id.is_some() + || root_binding.parent_run_id.is_some() + || root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || root_binding.binding_fingerprint != contract.run_profile_binding_fingerprint + || receipt.executor_agent_id != expected_agent_id + || receipt.executor_run_id != expected_run_id + || receipt.executor_source != "agent-ready-task-scheduler" + || !is_lowercase_sha256(&receipt.executor_run_profile_binding_fingerprint) + { + return Err("自主试玩回执的执行 child 身份无效".to_string()); + } + let root_task = read_latest_game_creator_agent_runtime_task_by_run_id( + root, + &root_binding.agent_id, + &root_binding.run_id, + )? + .ok_or_else(|| "自主试玩回执缺少根 Supervisor durable task journal".to_string())?; + let current_root = current_autonomous_game_build_root_task_at(root)? + .ok_or_else(|| "自主试玩回执对应的当前根 Supervisor Run 已不存在".to_string())?; + if 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_some() + || root_task.parent_run_id.is_some() + || root_task.delegation_id.is_some() + || current_root.run_id != root_task.run_id + || current_root.source != root_task.source + || current_root.run_profile_binding_fingerprint != root_task.run_profile_binding_fingerprint + || !autonomous_game_build_root_task_is_active(¤t_root) + { + return Err("自主试玩回执与当前根 Supervisor durable identity 不匹配".to_string()); + } + let binding = read_game_creator_agent_runtime_run_profile_binding( + root, + &receipt.executor_agent_id, + &receipt.executor_run_id, + )? + .ok_or_else(|| "自主试玩回执缺少执行 child Run Profile 绑定".to_string())?; + if binding.project_id != contract.project_id + || binding.agent_id != receipt.executor_agent_id + || binding.run_id != receipt.executor_run_id + || binding.source != receipt.executor_source + || binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + || binding.binding_fingerprint != receipt.executor_run_profile_binding_fingerprint + || binding.root_agent_id != root_binding.agent_id + || binding.root_run_id != root_binding.run_id + || binding.parent_agent_id.as_deref() != Some(root_binding.agent_id.as_str()) + || binding.parent_run_id.as_deref() != Some(root_binding.run_id.as_str()) + || binding.parent_binding_fingerprint.as_deref() + != Some(root_binding.binding_fingerprint.as_str()) + { + return Err("自主试玩回执与执行 child Run Profile 绑定不匹配".to_string()); + } + let task = read_latest_game_creator_agent_runtime_task_by_run_id( + root, + &receipt.executor_agent_id, + &receipt.executor_run_id, + )? + .ok_or_else(|| "自主试玩回执缺少执行 child durable task journal".to_string())?; + if task.agent_id != binding.agent_id + || task.task_id != expected_agent_id + || task.run_id != binding.run_id + || task.source != binding.source + || task.run_profile != binding.profile + || task.run_profile_binding_fingerprint != binding.binding_fingerprint + || task.parent_agent_id != binding.parent_agent_id + || task.parent_run_id != binding.parent_run_id + || task.delegation_id.is_some() + || matches!( + game_creator_agent_runtime_terminal_status(&task), + Some("failed" | "cancelled" | "budget-exhausted") + ) + { + return Err("自主试玩回执与执行 child durable task journal 不匹配".to_string()); + } + Ok(()) +} + +pub(in crate::agent) fn autonomous_playtest_execution_contract_for_state_at( + root: &Path, + state: &AgentRuntimeState, +) -> Result, String> { + if state.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD { + return Ok(None); + } + let (binding, root_binding) = + validate_autonomous_playtest_executor_state_lineage_at(root, state)?; + let contract = + read_autonomous_completion_contract(root, &root_binding.agent_id, &root_binding.run_id)? + .ok_or_else(|| "自主试玩执行 child 缺少根 Supervisor 完成合同".to_string())?; + let contract = migrate_legacy_tetris_completion_contract_for_run_at(root, contract)?; + if contract.project_id != root_binding.project_id + || contract.agent_id != root_binding.agent_id + || contract.run_id != root_binding.run_id + || contract.run_profile_binding_fingerprint != root_binding.binding_fingerprint + || binding.parent_binding_fingerprint.as_deref() + != Some(contract.run_profile_binding_fingerprint.as_str()) + { + return Err("自主试玩执行 child 与根 Supervisor 完成合同不匹配".to_string()); + } + Ok(Some(contract)) +} + fn validate_autonomous_playtest_receipt_integrity( root: &Path, contract: &AgentRuntimeAutonomousCompletionContract, @@ -14603,11 +15015,12 @@ fn validate_autonomous_playtest_receipt_integrity( { return Err("自主试玩回执身份无效".to_string()); } + validate_autonomous_playtest_executor_receipt_identity_at(root, contract, receipt)?; validate_autonomous_evidence_digest(&receipt.game_index, Some(AGENT_RUNTIME_GAME_INDEX_PATH))?; let evidence_prefix = format!( ".agent/runtime/browser-validations/{}/{}/{}/", - agent_runtime_confirmation_path_component(&receipt.agent_id, "agent"), - agent_runtime_confirmation_path_component(&receipt.run_id, "run"), + agent_runtime_confirmation_path_component(&receipt.executor_agent_id, "agent"), + agent_runtime_confirmation_path_component(&receipt.executor_run_id, "run"), receipt.revision ); validate_autonomous_evidence_digest( @@ -14676,6 +15089,9 @@ pub(in crate::agent) fn read_autonomous_playtest_receipt( "自主试玩回执", )?; if let Some(receipt) = receipt.as_ref() { + if receipt.schema_version == "game-creator-autonomous-playtest-receipt.v1" { + return Ok(None); + } if receipt.playtest_scenario != contract.playtest_scenario { return Ok(None); } @@ -14735,11 +15151,21 @@ pub(in crate::agent) fn verify_autonomous_playtest_evidence_files_at( pub(in crate::agent) fn write_autonomous_playtest_receipt_at( root: &Path, contract: &AgentRuntimeAutonomousCompletionContract, + executor_state: &AgentRuntimeState, action_id: &str, action_fingerprint: &str, revision: u64, result: &BrowserValidationResult, ) -> Result { + let (executor_binding, root_binding) = + validate_autonomous_playtest_executor_state_lineage_at(root, executor_state)?; + if contract.project_id != root_binding.project_id + || contract.agent_id != root_binding.agent_id + || contract.run_id != root_binding.run_id + || contract.run_profile_binding_fingerprint != root_binding.binding_fingerprint + { + return Err("自主试玩执行 child 与回执完成合同不匹配".to_string()); + } if revision <= contract.baseline_revision || !is_valid_agent_runtime_action_id(action_id) || !is_valid_agent_runtime_action_fingerprint(action_fingerprint) @@ -14819,6 +15245,10 @@ pub(in crate::agent) fn write_autonomous_playtest_receipt_at( agent_id: contract.agent_id.clone(), run_id: contract.run_id.clone(), run_profile_binding_fingerprint: contract.run_profile_binding_fingerprint.clone(), + executor_agent_id: executor_binding.agent_id.clone(), + executor_run_id: executor_binding.run_id.clone(), + executor_source: executor_binding.source.clone(), + executor_run_profile_binding_fingerprint: executor_binding.binding_fingerprint.clone(), action_id: action_id.to_string(), action_fingerprint: action_fingerprint.to_string(), revision, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs index ae2270cb0..804e70217 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion_contract_tests.rs @@ -478,6 +478,24 @@ fn complete_and_claim_game_chat_art_delivery( .expect("read delegated art child") .expect("delegated art child exists"); let mut completed = agent_runtime_state_from_task_record(&child); + let revision = read_game_creator_agent_runtime_project_revision(root) + .expect("read revision for Canvas delivery fixture") + .revision; + assert!( + revision > 0, + "Canvas delivery fixture requires a project revision" + ); + let mut gate = + read_game_creator_agent_runtime_verification_gate(root, target_agent_id, &completed.run_id) + .expect("read Canvas delivery verification gate"); + gate.requires_verification = true; + gate.mutation_revision = Some(revision); + gate.verified_revision = Some(revision); + gate.last_mutation_tool = Some("canvas.asset_generate".to_string()); + gate.last_verification_tool = Some("canvas.asset_generate".to_string()); + gate.last_verification_status = Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string()); + write_game_creator_agent_runtime_verification_gate(root, &gate) + .expect("persist Canvas delivery verification gate"); completed.status = "completed".to_string(); completed.phase = "completed".to_string(); completed.current_action = "已补齐受委派美术".to_string(); @@ -510,10 +528,9 @@ fn persist_game_chat_main_playtest_receipt( read_autonomous_completion_contract(root, &parent_state.agent_id, &parent_state.run_id) .expect("read game-chat root completion contract") .expect("game-chat root completion contract exists"); - // The durable receipt belongs to the root completion contract even though - // code-prototype performed the validation. Keep its evidence under the - // root identity so receipt integrity can bind it to that contract. - let result = browser_result_fixture(root, parent_state, revision, contract.playtest_scenario); + // The durable receipt is indexed by the root completion contract, while + // its evidence and executor fields remain bound to the single main child. + let result = browser_result_fixture(root, main_state, revision, contract.playtest_scenario); let action = AgentRuntimeToolAction { tool: "preview.validate".to_string(), reason: Some("game-chat code-prototype desktop/mobile playtest".to_string()), @@ -525,6 +542,7 @@ fn persist_game_chat_main_playtest_receipt( write_autonomous_playtest_receipt_at( root, &contract, + main_state, &action_id, &action_fingerprint, revision, @@ -562,6 +580,76 @@ fn advance_game_index_revision(root: &Path, state: &AgentRuntimeState, html: &st revision } +fn advance_owner_artifact_revision( + root: &Path, + state: &AgentRuntimeState, + path: &str, + content: &str, +) -> u64 { + let latest = + read_latest_game_creator_agent_runtime_task_by_run_id(root, &state.agent_id, &state.run_id) + .expect("read owner run before project mutation") + .expect("owner run exists before project mutation"); + if latest.status != "running" { + let mut running = state.clone(); + running.status = "running".to_string(); + running.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(root, &running) + .expect("append durable running owner run before project mutation"); + } + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + root, + "test.autonomous.owner-artifact.mutate", + ) + .expect("acquire owner artifact mutation lock"); + let revision = prepare_agent_runtime_project_mutation_locked( + root, + &state.agent_id, + &state.run_id, + "file.write", + ) + .expect("advance owner artifact revision"); + write_local_project_file_at(root, path, content).expect("write owner artifact"); + revision +} + +fn start_autonomous_owner_child( + root: &Path, + parent_state: &AgentRuntimeState, + agent_id: &str, +) -> AgentRuntimeState { + update_manifest_task_status_at(root, agent_id, GameCreationAppTaskStatus::Running) + .expect("mark autonomous owner manifest task running"); + let mut state = agent_runtime_state_from_task_record(&queue_autonomous_manifest_child_fixture( + root, + parent_state, + agent_id, + )); + state.status = "running".to_string(); + state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(root, &state) + .expect("persist running autonomous owner child"); + state +} + +fn start_autonomous_playtest_child( + root: &Path, + parent_state: &AgentRuntimeState, +) -> AgentRuntimeState { + update_manifest_task_status_at(root, "preview-playtest", GameCreationAppTaskStatus::Running) + .expect("mark autonomous preview-playtest manifest task running"); + let mut state = agent_runtime_state_from_task_record(&queue_autonomous_manifest_child_fixture( + root, + parent_state, + "preview-playtest", + )); + state.status = "running".to_string(); + state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(root, &state) + .expect("persist running autonomous preview-playtest child"); + state +} + fn mark_verification_passed(root: &Path, state: &AgentRuntimeState, tool: &str) { let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( root, @@ -826,9 +914,10 @@ fn preview_child_migrates_legacy_generic_tetris_contract_and_invalidates_its_rec &parent_state, "", ); + let child_state = start_autonomous_playtest_child(&root, &parent_state); let result = browser_result_fixture( &root, - &parent_state, + &child_state, revision, BrowserPlaytestScenario::GenericV1, ); @@ -838,12 +927,12 @@ fn preview_child_migrates_legacy_generic_tetris_contract_and_invalidates_its_rec input: serde_json::json!({}), }; let action_fingerprint = - agent_runtime_tool_action_fingerprint(&action, &parent_state.current_task); - let action_id = - agent_runtime_tool_action_id(&parent_state.run_id, 1, 0, 1, &action_fingerprint); + agent_runtime_tool_action_fingerprint(&action, &child_state.current_task); + let action_id = agent_runtime_tool_action_id(&child_state.run_id, 1, 0, 1, &action_fingerprint); write_autonomous_playtest_receipt_at( &root, &legacy_contract, + &child_state, &action_id, &action_fingerprint, revision, @@ -851,9 +940,6 @@ fn preview_child_migrates_legacy_generic_tetris_contract_and_invalidates_its_rec ) .expect("persist legacy Generic Tetris receipt"); - let child_record = - queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-readiness"); - let child_state = agent_runtime_state_from_task_record(&child_record); let migrated = autonomous_playtest_completion_contract_for_state_at(&root, &child_state) .expect("recover child playtest contract") .expect("preview child inherits the root contract"); @@ -1084,6 +1170,10 @@ fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress( "继续完成继承的现有游戏目标", ) .expect("persist continued root Supervisor decision"); + let code_runtime_lock = + try_acquire_game_creator_agent_runtime_task_lock(&root, "code-prototype") + .expect("acquire continued code-prototype runtime lane") + .expect("continued code-prototype runtime lane is free"); let scheduled = schedule_autonomous_game_build_ready_tasks_at( &root, GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, @@ -1093,6 +1183,7 @@ fn game_chat_pure_continue_inherits_failed_root_semantics_and_manifest_progress( .expect("continued root must pass the scheduler contract gate"); assert_eq!(scheduled.len(), 1); assert_eq!(scheduled[0].state.agent_id, "code-prototype"); + drop(code_runtime_lock); update_manifest_task_status_at( &root, "code-prototype", @@ -1598,17 +1689,20 @@ fn inherited_tetris_contract_rejects_a_generic_collection_replacement() { .as_deref() .is_some_and(|detail| detail.contains("tetris-identity"))); - let result = browser_result_fixture(&root, &state, revision, contract.playtest_scenario); + let main_state = start_game_chat_main_agent(&root, &state); + let result = browser_result_fixture(&root, &main_state, revision, contract.playtest_scenario); let action = AgentRuntimeToolAction { tool: "preview.validate".to_string(), reason: Some("negative semantic continuity fixture".to_string()), input: serde_json::json!({}), }; - let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task); - let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint); + let action_fingerprint = + agent_runtime_tool_action_fingerprint(&action, &main_state.current_task); + let action_id = agent_runtime_tool_action_id(&main_state.run_id, 1, 0, 1, &action_fingerprint); let receipt_error = write_autonomous_playtest_receipt_at( &root, &contract, + &main_state, &action_id, &action_fingerprint, revision, @@ -5378,15 +5472,7 @@ fn autonomous_preview_manifest_tasks_accept_bound_current_revision_receipts() { "做一个完整小游戏", "autonomous-preview-playtest-receipt-parent", ); - update_manifest_task_status_at( - &root, - "preview-playtest", - GameCreationAppTaskStatus::Running, - ) - .expect("mark preview playtest running"); - let playtest_child = - queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-playtest"); - let playtest_state = agent_runtime_state_from_task_record(&playtest_child); + let playtest_state = start_autonomous_playtest_child(&root, &parent_state); let revision = advance_game_index_revision( &root, &parent_state, @@ -5394,7 +5480,7 @@ fn autonomous_preview_manifest_tasks_accept_bound_current_revision_receipts() { ); let result = browser_result_fixture( &root, - &parent_state, + &playtest_state, revision, BrowserPlaytestScenario::GenericV1, ); @@ -5407,18 +5493,114 @@ fn autonomous_preview_manifest_tasks_accept_bound_current_revision_receipts() { agent_runtime_tool_action_fingerprint(&action, &playtest_state.current_task); let action_id = agent_runtime_tool_action_id(&playtest_state.run_id, 1, 0, 1, &action_fingerprint); - write_autonomous_playtest_receipt_at( + let receipt = write_autonomous_playtest_receipt_at( &root, &contract, + &playtest_state, &action_id, &action_fingerprint, revision, &result, ) .expect("persist child-bound autonomous playtest receipt"); + assert_eq!(receipt.executor_agent_id, "preview-playtest"); + assert_eq!(receipt.executor_run_id, playtest_state.run_id); + assert_eq!(receipt.executor_source, "agent-ready-task-scheduler"); + assert_eq!( + receipt.executor_run_profile_binding_fingerprint, + playtest_state.run_profile_binding_fingerprint + ); + assert!( + receipt.report.path.contains("/preview-playtest/") + || receipt.report.path.contains("/preview-playtest-") + ); + + let mut borrowed_root_receipt = receipt.clone(); + borrowed_root_receipt.executor_agent_id = contract.agent_id.clone(); + borrowed_root_receipt.executor_run_id = contract.run_id.clone(); + borrowed_root_receipt.executor_source = parent_state.source.clone(); + borrowed_root_receipt.executor_run_profile_binding_fingerprint = + contract.run_profile_binding_fingerprint.clone(); + borrowed_root_receipt.receipt_fingerprint = + autonomous_playtest_receipt_fingerprint(&borrowed_root_receipt); + let error = validate_autonomous_playtest_receipt(&root, &contract, &borrowed_root_receipt) + .expect_err( + "root-owned receipt must not replace the deterministic playtest child identity", + ); + assert!( + error.contains("执行 child 身份"), + "unexpected error: {error}" + ); assert!(autonomous_game_build_completion_blocker_at_locked(&root, &playtest_state).is_none()); } +#[test] +fn full_dag_preview_execution_rejects_upstream_owner_and_accepts_only_playtest_child() { + for source in [ + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + ] { + let (_temporary, root, parent_state, expected_contract) = autonomous_fixture_with_source( + "做一个完整小游戏", + &format!("autonomous-preview-owner-{source}"), + source, + ); + let code_state = start_autonomous_owner_child(&root, &parent_state, "code-prototype"); + let error = autonomous_playtest_execution_contract_for_state_at(&root, &code_state) + .expect_err("full DAG code-prototype must not execute preview.validate"); + assert!( + error.contains("确定性 preview-playtest child"), + "unexpected error for {source}: {error}" + ); + assert!( + autonomous_playtest_execution_contract_for_state_at(&root, &parent_state).is_err(), + "the full DAG root must not execute preview.validate for {source}" + ); + + let playtest_state = start_autonomous_playtest_child(&root, &parent_state); + let actual_contract = + autonomous_playtest_execution_contract_for_state_at(&root, &playtest_state) + .expect("validate deterministic preview-playtest child") + .expect("full DAG playtest child inherits completion contract"); + assert_eq!(actual_contract, expected_contract); + + let mut forged_source = playtest_state.clone(); + forged_source.source = "agent-delegate".to_string(); + assert!( + autonomous_playtest_execution_contract_for_state_at(&root, &forged_source).is_err(), + "a forged child source must fail closed for {source}" + ); + let mut forged_parent = playtest_state.clone(); + forged_parent.parent_run_id = Some("forged-preview-parent".to_string()); + assert!( + autonomous_playtest_execution_contract_for_state_at(&root, &forged_parent).is_err(), + "a forged child lineage must fail closed for {source}" + ); + } +} + +#[test] +fn game_chat_preview_execution_keeps_the_single_main_code_prototype_route() { + let (_temporary, root, parent_state, expected_contract) = autonomous_fixture_with_source( + "继续完善当前小游戏", + "game-chat-preview-single-main-owner", + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, + ); + let main_state = start_game_chat_main_agent(&root, &parent_state); + let actual_contract = autonomous_playtest_execution_contract_for_state_at(&root, &main_state) + .expect("validate game-chat code-prototype playtest route") + .expect("game-chat main child inherits completion contract"); + assert_eq!(actual_contract, expected_contract); + + let playtest_state = start_autonomous_playtest_child(&root, &parent_state); + let error = autonomous_playtest_execution_contract_for_state_at(&root, &playtest_state) + .expect_err("game-chat must not create a parallel preview-playtest owner"); + assert!( + error.contains("确定性 code-prototype child"), + "unexpected error: {error}" + ); +} + #[test] fn autonomous_completion_rejects_formal_artifact_unchanged_from_run_baseline() { let baseline_bytes = @@ -5563,15 +5745,22 @@ fn autonomous_playtest_receipt_rejects_previous_scenario_fingerprint() { &state, "新游戏", ); - let result = - browser_result_fixture(&root, &state, revision, BrowserPlaytestScenario::GenericV1); + let playtest_state = start_autonomous_playtest_child(&root, &state); + let result = browser_result_fixture( + &root, + &playtest_state, + revision, + BrowserPlaytestScenario::GenericV1, + ); let action = AgentRuntimeToolAction { tool: "preview.validate".to_string(), reason: Some("验证真实可玩闭环".to_string()), input: serde_json::json!({}), }; - let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task); - let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint); + let action_fingerprint = + agent_runtime_tool_action_fingerprint(&action, &playtest_state.current_task); + let action_id = + agent_runtime_tool_action_id(&playtest_state.run_id, 1, 0, 1, &action_fingerprint); let mut stale_result = result.clone(); stale_result .playtest @@ -5582,6 +5771,7 @@ fn autonomous_playtest_receipt_rejects_previous_scenario_fingerprint() { write_autonomous_playtest_receipt_at( &root, &contract, + &playtest_state, &action_id, &action_fingerprint, revision, @@ -5592,6 +5782,7 @@ fn autonomous_playtest_receipt_rejects_previous_scenario_fingerprint() { let mut receipt = write_autonomous_playtest_receipt_at( &root, &contract, + &playtest_state, &action_id, &action_fingerprint, revision, @@ -5654,18 +5845,26 @@ fn stale_scenario_receipt_reads_as_missing_and_can_be_replaced() { &state, "可恢复试玩", ); - let result = - browser_result_fixture(&root, &state, revision, BrowserPlaytestScenario::GenericV1); + let playtest_state = start_autonomous_playtest_child(&root, &state); + let result = browser_result_fixture( + &root, + &playtest_state, + revision, + BrowserPlaytestScenario::GenericV1, + ); let action = AgentRuntimeToolAction { tool: "preview.validate".to_string(), reason: Some("验证可恢复试玩回执".to_string()), input: serde_json::json!({}), }; - let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task); - let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint); + let action_fingerprint = + agent_runtime_tool_action_fingerprint(&action, &playtest_state.current_task); + let action_id = + agent_runtime_tool_action_id(&playtest_state.run_id, 1, 0, 1, &action_fingerprint); let current = write_autonomous_playtest_receipt_at( &root, &contract, + &playtest_state, &action_id, &action_fingerprint, revision, @@ -5673,6 +5872,31 @@ fn stale_scenario_receipt_reads_as_missing_and_can_be_replaced() { ) .expect("persist current autonomous playtest receipt"); + let receipt_path = + autonomous_playtest_receipt_relative_path(&contract.agent_id, &contract.run_id); + let mut legacy_v1 = serde_json::to_value(¤t).expect("serialize legacy v1 fixture"); + let legacy_v1_object = legacy_v1.as_object_mut().expect("legacy v1 receipt object"); + legacy_v1_object.insert( + "schemaVersion".to_string(), + serde_json::Value::String("game-creator-autonomous-playtest-receipt.v1".to_string()), + ); + for field in [ + "executorAgentId", + "executorRunId", + "executorSource", + "executorRunProfileBindingFingerprint", + ] { + legacy_v1_object.remove(field); + } + write_agent_runtime_json_sidecar(&root, &receipt_path, "v1 自主试玩回执 fixture", &legacy_v1) + .expect("persist v1 autonomous playtest receipt fixture"); + assert!( + read_autonomous_playtest_receipt(&root, &contract) + .expect("v1 receipt must be recoverable as missing") + .is_none(), + "v1 receipt without executor identity must require a fresh playtest" + ); + let mut stale = current.clone(); stale.scenario_fingerprint = "b48e3189a0765d82b84b56ce88cff8d05db7d1c0cc218a5d068e6470b83010d3".to_string(); @@ -5692,8 +5916,6 @@ fn stale_scenario_receipt_reads_as_missing_and_can_be_replaced() { "stale scenario receipt must behave like missing evidence" ); - let receipt_path = - autonomous_playtest_receipt_relative_path(&contract.agent_id, &contract.run_id); let mut digest_tampered = stale.clone(); digest_tampered.report.sha256 = "0".repeat(64); write_agent_runtime_json_sidecar( @@ -5733,6 +5955,7 @@ fn stale_scenario_receipt_reads_as_missing_and_can_be_replaced() { let replacement = write_autonomous_playtest_receipt_at( &root, &contract, + &playtest_state, &action_id, &action_fingerprint, revision, @@ -6215,6 +6438,7 @@ fn canvas_visual_gate_resolves_numeric_constants_by_symbol_scope() { ); let html = ""; advance_game_index_revision(&root, &code_state, html); + mark_verification_passed(&root, &code_state, "game.static_smoke"); assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none()); } @@ -6271,6 +6495,7 @@ fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_co assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_some()); advance_game_index_revision(&root, &code_state, cropped_spritesheet_game_html()); + mark_verification_passed(&root, &code_state, "game.static_smoke"); assert!(autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none()); } @@ -6292,6 +6517,7 @@ fn cli_code_prototype_accepts_linked_inline_and_external_modules_for_canvas_atla &code_state, "", ); + mark_verification_passed(&root, &code_state, "game.static_smoke"); assert!( autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none(), "a reachable inline module atlas crop must satisfy the visual asset gate" @@ -6307,6 +6533,7 @@ fn cli_code_prototype_accepts_linked_inline_and_external_modules_for_canvas_atla &code_state, "", ); + mark_verification_passed(&root, &code_state, "game.static_smoke"); assert!( autonomous_game_build_completion_blocker_at_locked(&root, &code_state).is_none(), "a reachable external module atlas crop must satisfy the visual asset gate" @@ -6530,9 +6757,15 @@ fn current_game_chat_code_child_survives_pending_manifest_drift_and_projects_com code_gate.last_verification_tool.as_deref(), Some("game.static_smoke") ); + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + &root, + "test.game-chat-root-completion-after-main-terminal", + ) + .expect("wait for main-terminal next-wave scheduling to release the project lock"); + let root_blocker = autonomous_game_build_completion_blocker_at_locked(&root, &parent_state); assert!( - autonomous_game_build_completion_blocker_at_locked(&root, &parent_state).is_none(), - "the root must accept the same main Run's smoke and desktop/mobile playtest evidence" + root_blocker.is_none(), + "the root must accept the same main Run's smoke and desktop/mobile playtest evidence: {root_blocker:?}" ); } @@ -7039,6 +7272,97 @@ fn autonomous_root_creation_waits_for_the_project_write_lock() { assert_eq!(created.run_id, requested_run_id); } +fn assert_autonomous_contract_rebuild_waits_for_project_lock( + root: &Path, + record: &AgentRuntimeTaskRecord, + command_id: &str, +) { + let relative_path = + autonomous_completion_contract_relative_path(&record.agent_id, &record.run_id); + fs::remove_file( + resolve_local_project_path(root, &relative_path) + .expect("resolve autonomous completion contract path"), + ) + .expect("remove autonomous completion contract before deterministic rebuild"); + let project_lock = + acquire_game_creator_agent_runtime_project_write_lock_with_wait(root, command_id) + .expect("hold project lock before rebuilding completion contract"); + let root_for_thread = root.to_path_buf(); + let record_for_thread = record.clone(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (result_tx, result_rx) = std::sync::mpsc::channel(); + let rebuilder = std::thread::spawn(move || { + started_tx + .send(()) + .expect("announce completion contract rebuild attempt"); + let result = + ensure_autonomous_completion_contract_for_task_at(&root_for_thread, &record_for_thread); + result_tx + .send(result) + .expect("return completion contract rebuild result"); + }); + started_rx + .recv_timeout(std::time::Duration::from_secs(1)) + .expect("completion contract rebuilder started"); + std::thread::sleep(std::time::Duration::from_millis(50)); + assert!(matches!( + result_rx.try_recv(), + Err(std::sync::mpsc::TryRecvError::Empty) + )); + drop(project_lock); + result_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .expect("completion contract rebuild finishes after lock release") + .expect("rebuild autonomous completion contract after lock release"); + rebuilder + .join() + .expect("join completion contract rebuilder"); + assert!( + read_autonomous_completion_contract(root, &record.agent_id, &record.run_id) + .expect("read rebuilt autonomous completion contract") + .is_some() + ); +} + +#[test] +fn autonomous_completion_contract_reset_waits_for_incidental_project_write_lock() { + let (_temporary, root, initial_state, _contract) = autonomous_fixture_with_source( + "创建一轮星空收集游戏", + "completion-contract-lock-initial", + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, + ); + let initial_record = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &initial_state.run_id, + ) + .expect("read initial root before contract lock regression") + .expect("initial root exists before contract lock regression"); + assert_autonomous_contract_rebuild_waits_for_project_lock( + &root, + &initial_record, + "test.hold-before-initial-contract-reset", + ); + + append_failed_autonomous_root_projection(&root, &initial_record, "failed"); + let continuation = append_unique_game_creator_agent_runtime_pending_task( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &initial_record.session_id, + "继续", + "completion-contract-lock-continuation", + AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("create continuation before contract lock regression"); + assert_autonomous_contract_rebuild_waits_for_project_lock( + &root, + &continuation, + "test.hold-before-continuation-contract-reset", + ); +} + #[test] fn gui_ready_child_still_rejects_pending_manifest_status() { let (_temporary, root, parent_state, _contract) = @@ -7138,6 +7462,699 @@ fn autonomous_ready_child_missing_or_invalid_owner_artifact_is_blocked() { ); } +#[test] +fn autonomous_owner_artifact_runtime_validation_unblocks_real_new_project_without_smoke() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-validation-real-project-parent"); + assert!(!root.join("package.json").exists()); + assert!(run_limited_local_command_at(&root, "game.static_smoke") + .expect_err("initial placeholder must fail real game smoke") + .contains("画布")); + + fs::remove_file(root.join("memory/project.md")).expect("remove prepared project memory"); + fs::remove_file(root.join("game/game_design.md")).expect("remove prepared game design"); + update_manifest_task_status_at( + &root, + "design-foundation", + GameCreationAppTaskStatus::Running, + ) + .expect("mark design foundation running"); + update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Pending) + .expect("keep downstream code prototype pending"); + let record = queue_autonomous_manifest_child_fixture(&root, &parent_state, "design-foundation"); + let mut state = agent_runtime_state_from_task_record(&record); + state.status = "running".to_string(); + state.phase = "planning".to_string(); + append_game_creator_agent_runtime_task(&root, &state) + .expect("persist running design foundation child"); + + advance_owner_artifact_revision( + &root, + &state, + "memory/project.md", + "# 项目记忆\n\n核心目标与约束。\n", + ); + let missing = + project_verification_completion_blocker_at(&root, &state.agent_id, &state.run_id, &[]) + .expect("missing second owner artifact must block"); + assert!(missing + .detail + .as_deref() + .is_some_and(|detail| detail.contains("game/game_design.md"))); + + let verified_revision = advance_owner_artifact_revision( + &root, + &state, + "game/game_design.md", + "# 游戏设计\n\n核心循环、胜负条件与双视口交互。\n", + ); + let pending_owner_gate = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read pending owner verification gate"); + let non_progress_observations = (0..AGENT_RUNTIME_AUTONOMOUS_LIVENESS_OBSERVATION_LIMIT) + .map(|index| AgentRuntimeToolObservation { + tool: "file.read".to_string(), + status: "ok".to_string(), + summary: format!("owner read tail {index}"), + detail: None, + }) + .collect::>(); + validate_agent_runtime_autonomous_plan_liveness_at( + &root, + &state.agent_id, + &state.run_id, + AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1, + verified_revision, + &pending_owner_gate, + &non_progress_observations, + &AgentRuntimeToolPlan { + response: "固定 owner 产物已经完成。".to_string(), + ..AgentRuntimeToolPlan::default() + }, + false, + ) + .expect("owner response must reach Runtime validation after a long read tail"); + mark_verification_passed(&root, &state, "game.static_smoke"); + let obsolete_smoke_gate = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read obsolete owner smoke credential"); + assert_eq!( + obsolete_smoke_gate.last_verification_tool.as_deref(), + Some("game.static_smoke") + ); + assert_eq!( + obsolete_smoke_gate.static_smoke_verified_revision, + Some(verified_revision) + ); + assert!( + project_verification_completion_blocker_at(&root, &state.agent_id, &state.run_id, &[],) + .is_none() + ); + let gate = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read Runtime owner artifact verification gate"); + assert_eq!(gate.mutation_revision, Some(verified_revision)); + assert_eq!(gate.verified_revision, Some(verified_revision)); + assert_eq!( + gate.last_verification_tool.as_deref(), + Some(AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL) + ); + assert_eq!( + gate.last_verification_status.as_deref(), + Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED) + ); + assert_eq!(gate.static_smoke_verified_revision, None); + let owner_audits = || { + read_agent_db_records_bounded(&root, AGENT_RUNTIME_ACTION_HISTORY_MAX_DB_BYTES) + .expect("read owner artifact validation audits") + .0 + .into_iter() + .filter(|record| { + record.get("recordType").and_then(serde_json::Value::as_str) + == Some("agent.runtime.owner_artifacts.validated") + && record.get("agentId").and_then(serde_json::Value::as_str) + == Some(state.agent_id.as_str()) + && record.get("runId").and_then(serde_json::Value::as_str) + == Some(state.run_id.as_str()) + && record.get("revision").and_then(serde_json::Value::as_u64) + == Some(verified_revision) + }) + .collect::>() + }; + assert_eq!(owner_audits().len(), 1); + assert!( + project_verification_completion_blocker_at(&root, &state.agent_id, &state.run_id, &[],) + .is_none() + ); + assert_eq!(owner_audits().len(), 1, "owner audit must be idempotent"); + + let mut missing_credential = gate.clone(); + missing_credential.verified_revision = None; + missing_credential.last_verification_tool = None; + missing_credential.last_verification_status = None; + missing_credential.static_smoke_verified_revision = None; + missing_credential.updated_at = unix_timestamp(); + write_game_creator_agent_runtime_verification_gate(&root, &missing_credential) + .expect("remove owner credential while preserving mutation identity"); + assert!( + project_verification_completion_blocker_at(&root, &state.agent_id, &state.run_id, &[],) + .is_none(), + "same active owner run must deterministically recover a missing credential" + ); + let recovered = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read recovered owner credential"); + assert_eq!( + recovered.last_verification_tool.as_deref(), + Some(AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL) + ); + assert_eq!(recovered.verified_revision, Some(verified_revision)); + assert_eq!(owner_audits().len(), 1); + let manifest = read_manifest_for_project(&root).expect("read project manifest"); + assert!(manifest + .command_runs + .iter() + .all(|run| run.command_id != "game.static_smoke")); + assert!(!root.join(".agent/runtime/browser-validations").exists()); + assert_eq!( + manifest + .tasks + .iter() + .find(|task| task.id == "code-prototype") + .map(|task| &task.status), + Some(&GameCreationAppTaskStatus::Pending) + ); + + advance_owner_artifact_revision(&root, &state, "game/game_design.md", "# 游戏设计\n\nTODO\n"); + let incomplete = + project_verification_completion_blocker_at(&root, &state.agent_id, &state.run_id, &[]) + .expect("later incomplete mutation must invalidate owner credential"); + assert!(incomplete + .detail + .as_deref() + .is_some_and(|detail| detail.contains("incomplete-marker"))); + let invalidated = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read invalidated owner artifact gate"); + assert_eq!(invalidated.verified_revision, None); + assert_eq!(invalidated.static_smoke_verified_revision, None); +} + +#[test] +fn autonomous_owner_artifact_validation_recovers_prepared_finalization_without_observations() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-finalization-recovery-parent"); + let mut state = start_autonomous_owner_child(&root, &parent_state, "design-foundation"); + advance_owner_artifact_revision( + &root, + &state, + "memory/project.md", + "# 项目记忆\n\n恢复测试的正式项目约束。\n", + ); + let response_revision = advance_owner_artifact_revision( + &root, + &state, + "game/game_design.md", + "# 游戏设计\n\n恢复测试的完整玩法规格。\n", + ); + state.status = "running".to_string(); + state.phase = "finalizing".to_string(); + state.current_action = "恢复固定 owner prepared finalization".to_string(); + append_game_creator_agent_runtime_task(&root, &state).expect("append finalizing owner task"); + write_game_creator_agent_runtime_state(&root, &state).expect("persist finalizing owner state"); + + let response = "固定玩法规格已经完成。"; + let journal = build_game_creator_agent_runtime_finalization_journal( + &root, + &state, + response, + response_revision, + ) + .expect("build owner prepared finalization"); + write_game_creator_agent_runtime_finalization_journal(&root, &journal) + .expect("write owner prepared finalization"); + append_game_creator_agent_runtime_finalization_lifecycle_stage( + &root, + &journal, + "prepared", + journal.prepared_at, + ) + .expect("append owner prepared lifecycle"); + + assert_eq!( + resume_game_creator_agent_finalization_for_test_at(&root, &state.agent_id) + .expect("resume owner prepared finalization"), + "recovered" + ); + let gate = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read recovered owner verification gate"); + assert_eq!( + gate.last_verification_tool.as_deref(), + Some(AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL) + ); + assert_eq!(gate.verified_revision, Some(response_revision)); + assert_eq!(gate.static_smoke_verified_revision, None); + assert!(read_game_creator_agent_runtime_finalization_journal( + &root, + &state.agent_id, + &state.run_id, + ) + .expect("read recovered owner finalization") + .is_none()); + let conversation = read_local_conversation_for_session_at( + &root, + Some(&state.agent_id), + Some(&state.session_id), + ) + .expect("read recovered owner conversation"); + assert!(conversation + .messages + .iter() + .any(|message| message.role == "assistant" && message.content == response)); + assert!(!root.join(".agent/runtime/browser-validations").exists()); +} + +#[test] +fn autonomous_owner_artifact_validation_and_path_matrix_is_role_scoped() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-validation-matrix-parent"); + for (agent_id, allowed_path) in [ + ("design-foundation", "memory/project.md"), + ("balance-seed", "game/balance.json"), + ("art-asset-plan", "assets/manifest.art.json"), + ("audio-asset-plan", "assets/manifest.audio.json"), + ] { + let record = queue_autonomous_manifest_child_fixture(&root, &parent_state, agent_id); + assert!(autonomous_owner_artifact_validation_available_for_run_at( + &root, + agent_id, + &record.run_id, + ) + .expect("resolve owner artifact validation role")); + assert!(agent_role_project_path_mutation_block( + &root, + agent_id, + &record.run_id, + "file.write", + allowed_path, + ) + .is_none()); + let blocked = agent_role_project_path_mutation_block( + &root, + agent_id, + &record.run_id, + "file.write", + "game/index.html", + ); + if agent_id == "design-foundation" { + assert!(blocked + .as_ref() + .is_some_and(|value| value.summary.contains("design-foundation"))); + } else { + assert!(blocked + .as_ref() + .is_some_and(|value| value.summary.contains("固定正式产物"))); + } + } + + for agent_id in ["code-prototype", "publish-package"] { + let record = queue_autonomous_manifest_child_fixture(&root, &parent_state, agent_id); + assert!(!autonomous_owner_artifact_validation_available_for_run_at( + &root, + agent_id, + &record.run_id, + ) + .expect("resolve excluded owner artifact validation role")); + } +} + +#[test] +fn autonomous_gui_cli_code_prototype_terminal_projection_requires_own_static_smoke() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + + for (root_source, suffix) in [ + (AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, "gui"), + (AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, "cli"), + ] { + let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source( + "实现一个完整可玩小游戏", + &format!("code-terminal-smoke-{suffix}-parent"), + root_source, + ); + let _code_runtime_lane = + try_acquire_game_creator_agent_runtime_task_lock(&root, "code-prototype") + .expect("acquire code-prototype terminal projection runtime lane") + .expect("code-prototype terminal projection runtime lane is free"); + let state = start_autonomous_owner_child(&root, &parent_state, "code-prototype"); + let mutation_revision = advance_game_index_revision( + &root, + &state, + "", + ); + mark_verification_passed(&root, &state, "project.verify"); + let project_verified_gate = read_game_creator_agent_runtime_verification_gate( + &root, + &state.agent_id, + &state.run_id, + ) + .expect("read project.verify-only code gate"); + assert_eq!( + project_verified_gate.mutation_revision, + Some(mutation_revision) + ); + assert_eq!( + project_verified_gate.verified_revision, + Some(mutation_revision) + ); + assert_eq!(project_verified_gate.static_smoke_verified_revision, None); + + let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state) + .expect("project.verify-only code child must remain blocked"); + assert!(blocker.summary.contains("game.static_smoke")); + + let mut completed = state.clone(); + completed.status = "completed".to_string(); + completed.phase = "completed".to_string(); + completed.current_action = "尝试投影 project.verify-only 终态".to_string(); + append_game_creator_agent_runtime_task(&root, &completed) + .expect("persist deliberately under-verified code terminal"); + let error = project_autonomous_manifest_ready_task_terminal_at_locked(&root, &completed) + .expect_err("terminal projection must defend the static smoke owner contract"); + assert!( + error.contains("game.static_smoke"), + "unexpected error: {error}" + ); + let manifest = read_manifest_for_project(&root) + .expect("read manifest after rejected code terminal projection"); + assert_eq!( + manifest + .tasks + .iter() + .find(|task| task.id == "code-prototype") + .expect("code-prototype seed task") + .status, + GameCreationAppTaskStatus::Running + ); + + mark_verification_passed(&root, &state, "game.static_smoke"); + let smoke_gate = read_game_creator_agent_runtime_verification_gate( + &root, + &state.agent_id, + &state.run_id, + ) + .expect("read code static smoke gate"); + assert_eq!( + smoke_gate.static_smoke_verified_revision, + Some(mutation_revision) + ); + assert!(autonomous_game_build_completion_blocker_at_locked(&root, &completed).is_none()); + let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + &root, + "test.code-prototype-terminal-root-revalidation", + ) + .expect("acquire code-prototype terminal root revalidation project lock"); + assert!( + project_autonomous_manifest_ready_task_terminal_at_locked(&root, &completed) + .expect("project code terminal after static smoke") + ); + let manifest = read_manifest_for_project(&root) + .expect("read manifest after accepted code terminal projection"); + assert_eq!( + manifest + .tasks + .iter() + .find(|task| task.id == "code-prototype") + .expect("completed code-prototype seed task") + .status, + GameCreationAppTaskStatus::Completed + ); + + let mut legacy_gate = smoke_gate; + legacy_gate.last_verification_tool = Some("project.verify".to_string()); + legacy_gate.last_verification_status = + Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string()); + legacy_gate.static_smoke_verified_revision = None; + write_game_creator_agent_runtime_verification_gate(&root, &legacy_gate) + .expect("persist legacy project.verify-only completed code gate"); + let root_blocker = autonomous_game_build_completion_blocker_at_locked(&root, &parent_state) + .expect("root completion must revalidate a manifest-completed code child"); + assert!( + root_blocker + .detail + .as_deref() + .is_some_and(|detail| detail.contains("code-static-smoke")), + "unexpected root completion blocker: {root_blocker:?}" + ); + } +} + +#[test] +fn autonomous_owner_artifact_validation_rejects_noncanonical_or_inactive_identity() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + + { + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-identity-wrong-run-parent"); + let state = start_autonomous_owner_child(&root, &parent_state, "design-foundation"); + assert!(validate_autonomous_owner_artifacts_for_run_at_locked( + &root, + &state.agent_id, + "owner-identity-other-run", + ) + .is_err()); + assert!(validate_autonomous_owner_artifacts_for_run_at_locked( + &root, + "balance-seed", + &state.run_id, + ) + .is_err()); + } + + { + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-identity-delegated-parent"); + let session_id = + resolve_agent_conversation_session_id_at(&root, "design-foundation", None, true) + .expect("resolve delegated owner session"); + let delegated = append_unique_game_creator_agent_runtime_pending_task( + &root, + "design-foundation", + &session_id, + "伪造的 delegated owner", + "owner-identity-delegated-run", + "agent-delegate", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(parent_state.agent_id.clone()), + parent_run_id: Some(parent_state.run_id.clone()), + delegation_id: Some("owner-identity-delegation".to_string()), + }), + ) + .expect("queue delegated owner identity"); + assert!(!autonomous_owner_artifact_validation_available_for_run_at( + &root, + "design-foundation", + &delegated.run_id, + ) + .expect("resolve delegated owner route")); + assert!(validate_autonomous_owner_artifacts_for_run_at_locked( + &root, + "design-foundation", + &delegated.run_id, + ) + .is_err()); + let index_path = root.join(AGENT_RUNTIME_GAME_INDEX_PATH); + let index_before = + fs::read(&index_path).expect("read game index before forged owner write"); + let observation = observe_agent_runtime_file_write( + &root, + "design-foundation", + &delegated.run_id, + &AgentRuntimeToolAction { + tool: "file.write".to_string(), + reason: Some("验证错误 lineage 不能先越权写入".to_string()), + input: serde_json::json!({ + "path": AGENT_RUNTIME_GAME_INDEX_PATH, + "content": "forged owner mutation", + }), + }, + &"0".repeat(64), + None, + ); + assert_eq!(observation.status, "blocked"); + assert!(observation.summary.contains("lineage")); + assert_eq!( + fs::read(&index_path).expect("read game index after forged owner write"), + index_before, + "an untrusted autonomous fixed owner must be rejected before file mutation", + ); + } + + { + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-identity-wrong-parent-root"); + let code_parent = + queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype"); + let session_id = + resolve_agent_conversation_session_id_at(&root, "design-foundation", None, true) + .expect("resolve wrong-parent owner session"); + let wrong_parent = append_unique_game_creator_agent_runtime_pending_task( + &root, + "design-foundation", + &session_id, + "错误父节点下的 owner", + "owner-identity-wrong-parent-run", + "agent-ready-task-scheduler", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(code_parent.agent_id.clone()), + parent_run_id: Some(code_parent.run_id.clone()), + delegation_id: None, + }), + ) + .expect("queue owner with wrong direct parent"); + assert!(!autonomous_owner_artifact_validation_available_for_run_at( + &root, + "design-foundation", + &wrong_parent.run_id, + ) + .expect("resolve wrong-parent owner route")); + assert!(validate_autonomous_owner_artifacts_for_run_at_locked( + &root, + "design-foundation", + &wrong_parent.run_id, + ) + .is_err()); + } + + { + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-identity-old-root-parent"); + let state = start_autonomous_owner_child(&root, &parent_state, "design-foundation"); + append_unique_game_creator_agent_runtime_pending_task( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &parent_state.session_id, + "启动新的完整小游戏构建", + "owner-identity-new-root", + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("create newer autonomous root"); + let error = validate_autonomous_owner_artifacts_for_run_at_locked( + &root, + &state.agent_id, + &state.run_id, + ) + .expect_err("old root owner must not validate"); + assert!( + error.contains("取代") || error.contains("当前根") || error.contains("活跃"), + "unexpected inactive-root error: {error}" + ); + } + + { + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-identity-terminal-parent"); + let state = start_autonomous_owner_child(&root, &parent_state, "design-foundation"); + let latest = read_latest_game_creator_agent_runtime_task_by_run_id( + &root, + &state.agent_id, + &state.run_id, + ) + .expect("read running owner before terminal transition") + .expect("running owner exists"); + append_game_creator_agent_runtime_task_record( + &root, + &AgentRuntimeTaskRecord { + status: "completed".to_string(), + phase: "completed".to_string(), + current_action: "owner terminal fixture".to_string(), + ..latest + }, + ) + .expect("append terminal owner record"); + assert!(validate_autonomous_owner_artifacts_for_run_at_locked( + &root, + &state.agent_id, + &state.run_id, + ) + .is_err()); + } +} + +#[test] +fn autonomous_owner_artifact_credential_cannot_be_borrowed_across_agents() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-credential-isolation-parent"); + let design = start_autonomous_owner_child(&root, &parent_state, "design-foundation"); + advance_owner_artifact_revision( + &root, + &design, + "memory/project.md", + "# 项目记忆\n\n隔离凭证测试。\n", + ); + advance_owner_artifact_revision( + &root, + &design, + "game/game_design.md", + "# 游戏设计\n\n隔离凭证测试。\n", + ); + assert!(project_verification_completion_blocker_at( + &root, + &design.agent_id, + &design.run_id, + &[], + ) + .is_none()); + let design_gate = + read_game_creator_agent_runtime_verification_gate(&root, &design.agent_id, &design.run_id) + .expect("read design owner credential"); + + let balance = start_autonomous_owner_child(&root, &parent_state, "balance-seed"); + fs::remove_file(root.join("game/balance.json")).expect("remove balance owner artifact"); + let mut borrowed = design_gate; + borrowed.agent_id = balance.agent_id.clone(); + borrowed.run_id = balance.run_id.clone(); + borrowed.updated_at = unix_timestamp(); + write_game_creator_agent_runtime_verification_gate(&root, &borrowed) + .expect("install structurally valid borrowed credential fixture"); + let blocker = + project_verification_completion_blocker_at(&root, &balance.agent_id, &balance.run_id, &[]) + .expect("borrowed owner credential must not pass"); + assert!(blocker + .detail + .as_deref() + .is_some_and(|detail| detail.contains("game/balance.json"))); +} + +#[test] +fn owner_artifact_verification_revision_drift_fails_closed() { + let _config_guard = crate::tests::write_test_local_config("{}".to_string()); + let (_temporary, root, parent_state, _contract) = + autonomous_fixture("做一个完整小游戏", "owner-revision-drift-parent"); + let state = start_autonomous_owner_child(&root, &parent_state, "design-foundation"); + advance_owner_artifact_revision( + &root, + &state, + "memory/project.md", + "# 项目记忆\n\n并发 revision 测试。\n", + ); + let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait( + &root, + "test.owner-verification-revision-drift", + ) + .expect("acquire owner verification drift lock"); + let (expected_revision, gate) = begin_agent_runtime_project_verification_locked( + &root, + &state.agent_id, + &state.run_id, + AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL, + ) + .expect("begin owner verification before drift"); + advance_agent_runtime_project_revision_locked(&root) + .expect("advance project revision during verification fixture"); + let error = + finish_agent_runtime_project_verification_locked(&root, &expected_revision, gate, true) + .expect_err("revision drift must invalidate owner verification"); + assert!(error.contains("结果不再有效")); + let failed = + read_game_creator_agent_runtime_verification_gate(&root, &state.agent_id, &state.run_id) + .expect("read failed owner verification after drift"); + assert_eq!(failed.verified_revision, None); + assert_eq!( + failed.last_verification_status.as_deref(), + Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED) + ); + assert_eq!(failed.static_smoke_verified_revision, None); +} + #[test] fn autonomous_completion_blocks_outer_e2e_incomplete_markers_in_owner_markdown_and_html() { let (_temporary, root, parent_state, _contract) = autonomous_fixture( @@ -7801,9 +8818,10 @@ fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest( .exists() ); + let playtest_state = start_autonomous_playtest_child(&root, &state); let result = browser_result_fixture( &root, - &state, + &playtest_state, revision, BrowserPlaytestScenario::LaneDefenseV1, ); @@ -7812,17 +8830,30 @@ fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest( reason: Some("验证真实可玩闭环".to_string()), input: serde_json::json!({}), }; - let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task); - let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint); + let action_fingerprint = + agent_runtime_tool_action_fingerprint(&action, &playtest_state.current_task); + let action_id = + agent_runtime_tool_action_id(&playtest_state.run_id, 1, 0, 1, &action_fingerprint); write_autonomous_playtest_receipt_at( &root, &contract, + &playtest_state, &action_id, &action_fingerprint, revision, &result, ) .expect("persist autonomous playtest receipt"); + let mut completed_playtest = playtest_state.clone(); + completed_playtest.status = "completed".to_string(); + completed_playtest.phase = "completed".to_string(); + completed_playtest.current_action = "已完成独立桌面与移动试玩".to_string(); + append_game_creator_agent_runtime_task(&root, &completed_playtest) + .expect("persist completed preview-playtest child"); + assert!( + project_autonomous_manifest_ready_task_terminal_at(&root, &completed_playtest) + .expect("project completed preview-playtest child") + ); assert!(autonomous_game_build_completion_blocker_at_locked(&root, &state).is_none()); advance_game_index_revision( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/verification.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/verification.rs index 2ee35f3bf..b31ae6420 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/verification.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/verification.rs @@ -154,6 +154,7 @@ pub(in crate::agent) fn validate_agent_runtime_verification_gate( tool, "project.verify" | "game.static_smoke" + | AGENT_RUNTIME_OWNER_ARTIFACT_VERIFICATION_TOOL | "command.exec" | "preview.validate" | "canvas.asset_generate" diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/command_ops.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/command_ops.rs index c86494dab..15e3382d1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/command_ops.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/command_ops.rs @@ -730,6 +730,28 @@ pub(crate) fn observe_agent_runtime_limited_command( detail: None, }; } + match autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id) { + Ok(true) => { + return AgentRuntimeToolObservation { + tool: "command.run_limited".to_string(), + status: "failed".to_string(), + summary: "固定 owner 产物不能使用 game.static_smoke 验证".to_string(), + detail: Some( + "Runtime 会在收束门内验证当前 Agent/run 的固定正式产物;game.static_smoke 只验证可玩入口" + .to_string(), + ), + }; + } + Ok(false) => {} + Err(error) => { + return AgentRuntimeToolObservation { + tool: "command.run_limited".to_string(), + status: "failed".to_string(), + summary: "无法核对 game.static_smoke 的当前 run 身份".to_string(), + detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), + }; + } + } let _lock = match acquire_project_write_lock(root, "command.run_limited") { Ok(lock) => lock, Err(error) => { 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 a68fe2100..231e095e4 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 @@ -1,42 +1,25 @@ use super::*; -fn design_foundation_owned_project_path(path: &str) -> bool { - matches!(path, "memory/project.md" | "game/game_design.md") -} - fn game_chat_delegated_art_agent_is_assets_only_at( root: &Path, agent_id: &str, run_id: &str, ) -> Result { - if !matches!(agent_id, "art-director" | "art-asset-plan") { - return Ok(false); - } - let Some(binding) = - read_game_creator_agent_runtime_run_profile_binding(root, agent_id, run_id)? - else { - return Ok(false); + let canonical_output = match agent_id { + "art-director" => AGENT_RUNTIME_ART_SPEC_PATH, + "art-asset-plan" => AGENT_RUNTIME_ART_SPRITESHEET_PATH, + _ => return Ok(false), }; - if binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - || binding.source != "agent-delegate" - || binding.parent_agent_id.as_deref() != Some("code-prototype") - || binding.parent_run_id.as_deref().is_none_or(str::is_empty) - || binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID - { - return Ok(false); - } - let root_binding = read_game_creator_agent_runtime_run_profile_binding( + // `assets/**` is a broad write scope. Grant it only after the same + // canonical-output authorization used by Canvas replacement has verified + // the live root, main parent, child binding, durable delivery and audited + // generate-missing route. + game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at( root, - &binding.root_agent_id, - &binding.root_run_id, - )? - .ok_or_else(|| "美术 Agent 写入路径校验缺少根 Run Profile 绑定".to_string())?; - if root_binding.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE - || root_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD - { - return Ok(false); - } - Ok(true) + agent_id, + run_id, + canonical_output, + ) } fn game_chat_art_child_tool_is_read_only(tool: &str) -> bool { @@ -161,21 +144,121 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_input_mutation_block( } pub(in crate::agent) fn agent_role_project_path_mutation_block( + root: &Path, agent_id: &str, + run_id: &str, tool: &str, path: &str, ) -> Option { - if agent_id != "design-foundation" || design_foundation_owned_project_path(path) { - return None; + match autonomous_owner_artifact_validation_available_for_run_at(root, agent_id, run_id) { + Ok(true) => { + let allowed = autonomous_manifest_owner_artifact_paths(agent_id); + if allowed.contains(&path) { + None + } else { + Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: if agent_id == "design-foundation" { + "design-foundation 只能修改项目基础设计文档".to_string() + } else { + "autonomous owner 只能修改本人固定正式产物".to_string() + }, + detail: Some(format!("path={path} · allowed={}", allowed.join(","))), + }) + } + } + Ok(false) => { + match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) { + Ok(true) if path == "assets" || path.starts_with("assets/") => return None, + Ok(true) => { + return Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "game-chat 临时美术 Agent 只能修改 assets/**".to_string(), + detail: Some(format!( + "path={path} · allowed=assets/** · parentAgentId=code-prototype" + )), + }); + } + Ok(false) => {} + Err(error) => { + return Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "无法校验 game-chat 临时美术 Agent 的写入边界".to_string(), + detail: Some(sanitize_agent_runtime_text(&error, 240)), + }); + } + } + + let autonomous_fixed_owner = + if agent_runtime_autonomous_uses_owner_artifact_validation(agent_id) { + match read_game_creator_agent_runtime_run_profile_binding( + root, agent_id, run_id, + ) { + Ok(Some(binding)) => { + binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + } + Ok(None) => match read_latest_game_creator_agent_runtime_task_by_run_id( + root, agent_id, run_id, + ) { + Ok(Some(task)) => { + task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + } + Ok(None) => false, + Err(error) => { + return Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "无法核对 autonomous owner 写入边界".to_string(), + detail: Some(sanitize_agent_runtime_text(&error, 240)), + }); + } + }, + Err(error) => { + return Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "无法核对 autonomous owner 写入边界".to_string(), + detail: Some(sanitize_agent_runtime_text(&error, 240)), + }); + } + } + } else { + false + }; + if autonomous_fixed_owner { + Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "autonomous fixed owner 的当前 lineage 不受信任,禁止修改项目" + .to_string(), + detail: Some(format!("agentId={agent_id} · runId={run_id} · path={path}")), + }) + } else if agent_id == "design-foundation" { + let allowed = autonomous_manifest_owner_artifact_paths(agent_id); + if allowed.contains(&path) { + None + } else { + Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "design-foundation 只能修改项目基础设计文档".to_string(), + detail: Some(format!("path={path} · allowed={}", allowed.join(","))), + }) + } + } else { + None + } + } + Err(error) => Some(AgentRuntimeToolObservation { + tool: tool.to_string(), + status: "blocked".to_string(), + summary: "无法核对 autonomous owner 写入边界".to_string(), + detail: Some(sanitize_agent_runtime_text(&error, 240)), + }), } - Some(AgentRuntimeToolObservation { - tool: tool.to_string(), - status: "blocked".to_string(), - summary: "design-foundation 只能修改项目基础设计文档".to_string(), - detail: Some(format!( - "path={path} · allowed=memory/project.md,game/game_design.md" - )), - }) } pub(in crate::agent) fn observe_agent_runtime_file( @@ -337,7 +420,9 @@ pub(in crate::agent) fn observe_agent_runtime_file_write( }; } }; - if let Some(blocked) = agent_role_project_path_mutation_block(agent_id, "file.write", &path) { + if let Some(blocked) = + agent_role_project_path_mutation_block(root, agent_id, run_id, "file.write", &path) + { return blocked; } if let Some(blocked) = game_chat_delegated_art_agent_project_path_mutation_block( @@ -450,7 +535,9 @@ pub(in crate::agent) fn observe_agent_runtime_file_delete( }; } }; - if let Some(blocked) = agent_role_project_path_mutation_block(agent_id, "file.delete", &path) { + if let Some(blocked) = + agent_role_project_path_mutation_block(root, agent_id, run_id, "file.delete", &path) + { return blocked; } if let Some(blocked) = game_chat_delegated_art_agent_project_path_mutation_block( @@ -669,7 +756,9 @@ pub(in crate::agent) fn observe_agent_runtime_file_patch( }; } }; - if let Some(blocked) = agent_role_project_path_mutation_block(agent_id, "file.patch", &path) { + if let Some(blocked) = + agent_role_project_path_mutation_block(root, agent_id, run_id, "file.patch", &path) + { return blocked; } if let Some(blocked) = game_chat_delegated_art_agent_project_path_mutation_block( diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs index 16720f2ca..ba28c035f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs @@ -7,29 +7,23 @@ fn autonomous_game_build_agent_can_execute_canvas_asset_generate(agent_id: &str) ) } -fn autonomous_design_foundation_command_is_allowed(command_id: &str) -> bool { - matches!( - command_id, - "memory.read" - | "conversation.read" - | "asset.list" - | "project.index" - | "project.search" - | "file.read" - | "project.diff" - | "git.inspect" - | "file.list" - | "file.write" - | "file.delete" - | "project.patchset" - | "task.list" - | "command.run_limited" - | "image.inspect" - | "canvas.asset_generate" - | "agent.audit" - | "agent.action_history" - | "agent.run_status" - ) +fn autonomous_owner_manual_verification_command_is_denied( + agent_id: &str, + command_id: &str, +) -> bool { + agent_runtime_autonomous_uses_owner_artifact_validation(agent_id) + && matches!(command_id, "project.verify" | "command.run_limited") +} + +fn autonomous_art_director_non_canvas_validation_command_is_denied( + agent_id: &str, + command_id: &str, +) -> bool { + agent_id == "art-director" + && matches!( + command_id, + "project.verify" | "command.run_limited" | "preview.start" | "preview.validate" + ) } pub(in crate::agent) fn refresh_game_creator_agent_runtime_tool_policy( @@ -71,6 +65,20 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_rule_for_run( Ok(identity) => identity, Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), }; + if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && autonomous_owner_manual_verification_command_is_denied(agent_id, command_id) + { + return Some(AgentRuntimeToolPolicyBlock::Denied(format!( + "自主构建固定 owner {agent_id} 的正式产物只允许由 Runtime 内部验证,拒绝回退执行:{command_id}" + ))); + } + if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD + && autonomous_art_director_non_canvas_validation_command_is_denied(agent_id, command_id) + { + return Some(AgentRuntimeToolPolicyBlock::Denied(format!( + "自主构建 art-director 只负责条件 Canvas 规范图或无 Key 只读结论,拒绝执行非 Canvas 验证:{command_id}" + ))); + } if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && command_id == "canvas.asset_generate" && !autonomous_game_build_agent_can_execute_canvas_asset_generate(agent_id) @@ -81,10 +89,10 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_rule_for_run( } if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD && agent_id == "design-foundation" - && !autonomous_design_foundation_command_is_allowed(command_id) + && !agent_runtime_autonomous_design_foundation_command_is_allowed(command_id) { return Some(AgentRuntimeToolPolicyBlock::Denied(format!( - "design-foundation 只允许只读工具、受限设计文档写入、UI 原型画布生成和 game.static_smoke:{command_id}" + "design-foundation 只允许只读工具、受限设计文档写入、UI 原型画布生成和 Runtime 内部产物验证:{command_id}" ))); } match blocked { @@ -590,20 +598,17 @@ mod tests { "conversation.read", "asset.list", "project.index", - "project.search", "file.read", "project.diff", - "git.inspect", + "project.git_inspect", "file.list", "file.write", "file.delete", "project.patchset", "task.list", - "command.run_limited", "image.inspect", "canvas.asset_generate", "agent.audit", - "agent.action_history", "agent.run_status", ] { assert!(game_creator_agent_runtime_tool_policy_rule_for_run( @@ -617,6 +622,90 @@ mod tests { .is_none()); } + let snapshot = agent_runtime_tool_policy_snapshot_for_run_at( + &root, + "design-foundation", + "policy-design-foundation-run", + Some(&binding.profile), + Some(&binding.binding_fingerprint), + ) + .expect("read design-foundation policy snapshot"); + for (tool, command_id) in [ + ("project.search", "file.read"), + ("git.inspect", "project.git_inspect"), + ("file.patch", "file.write"), + ("agent.action_history", "agent.audit"), + ] { + assert!(game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + "design-foundation", + "policy-design-foundation-run", + Some(&binding.profile), + Some(&binding.binding_fingerprint), + command_id, + ) + .is_none()); + assert!(snapshot + .auto_tools + .iter() + .any(|candidate| candidate == tool)); + assert!(!snapshot + .confirm_tools + .iter() + .any(|candidate| candidate == tool)); + assert!(!snapshot + .denied_tools + .iter() + .any(|candidate| candidate == tool)); + } + for (tool, command_id) in [ + ("memory.write", "memory.write"), + ("project.verify", "project.verify"), + ("project.restore", "project.restore"), + ("project.git_commit", "project.git_commit"), + ("command.exec", "command.exec"), + ("command.output_read", "command.output_read"), + ("command.start", "command.start"), + ("command.poll", "command.poll"), + ("command.stdin", "command.stdin"), + ("command.terminate", "command.terminate"), + ("command.run_limited", "command.run_limited"), + ("preview.start", "preview.start"), + ("preview.validate", "preview.validate"), + ("agent.message", "conversation.write"), + ("agent.delegate", "agent.delegate"), + ("agent.spawn_isolated", "agent.spawn_isolated"), + ("agent.schedule_ready", "agent.schedule_ready"), + ("agent.route_manifest", "agent.route_manifest"), + ] { + assert!(matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + "design-foundation", + "policy-design-foundation-run", + Some(&binding.profile), + Some(&binding.binding_fingerprint), + command_id, + ), + Some(AgentRuntimeToolPolicyBlock::Denied(_)) + )); + assert!( + snapshot + .denied_tools + .iter() + .any(|candidate| candidate == tool), + "{tool} must be visible as denied for design-foundation" + ); + assert!(!snapshot + .auto_tools + .iter() + .any(|candidate| candidate == tool)); + assert!(!snapshot + .confirm_tools + .iter() + .any(|candidate| candidate == tool)); + } + let code_binding = bind_delegated_autonomous_run(&root, supervisor_run_id, "code-prototype"); assert!(game_creator_agent_runtime_tool_policy_rule_for_run( @@ -629,4 +718,134 @@ mod tests { ) .is_none()); } + + #[test] + fn autonomous_fixed_owners_deny_manual_verification_without_trusted_dag_fallback() { + let temporary = tempfile::tempdir().expect("create owner verification policy root"); + let root = temporary.path().join("project"); + init_local_game_project_at( + &root, + "project-owner-verification-policy", + "固定 owner 验证执行边界测试", + ) + .expect("project init"); + let supervisor_run_id = "policy-owner-verification-parent-run"; + bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + supervisor_run_id, + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("bind supervisor autonomous run"); + + for agent_id in [ + "design-foundation", + "balance-seed", + "art-asset-plan", + "audio-asset-plan", + ] { + let binding = bind_delegated_autonomous_run(&root, supervisor_run_id, agent_id); + assert!(!autonomous_owner_artifact_validation_available_for_run_at( + &root, + agent_id, + &binding.run_id, + ) + .expect("evaluate deliberately non-scheduler owner binding")); + for command_id in ["project.verify", "command.run_limited"] { + assert!(matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + agent_id, + &binding.run_id, + Some(&binding.profile), + Some(&binding.binding_fingerprint), + command_id, + ), + Some(AgentRuntimeToolPolicyBlock::Denied(reason)) + if reason.contains("固定 owner") + && reason.contains(agent_id) + && reason.contains(command_id) + )); + } + + assert!(matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + agent_id, + &binding.run_id, + Some(&binding.profile), + Some("forged-recovery-fingerprint"), + "project.verify", + ), + Some(AgentRuntimeToolPolicyBlock::Denied(reason)) + if reason.contains("持久绑定不一致") + )); + } + + assert!(matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + "balance-seed", + "missing-recovered-owner-run", + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + Some("missing-binding-fingerprint"), + "command.run_limited", + ), + Some(AgentRuntimeToolPolicyBlock::Denied(reason)) + if reason.contains("绑定缺失") && reason.contains("拒绝降级") + )); + + for agent_id in ["code-prototype", "publish-package"] { + let binding = bind_delegated_autonomous_run(&root, supervisor_run_id, agent_id); + for command_id in ["project.verify", "command.run_limited"] { + assert!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + agent_id, + &binding.run_id, + Some(&binding.profile), + Some(&binding.binding_fingerprint), + command_id, + ) + .is_none(), + "{agent_id} must remain outside the fixed pre-code owner verification rule" + ); + } + } + + let art_director_binding = + bind_delegated_autonomous_run(&root, supervisor_run_id, "art-director"); + for command_id in [ + "project.verify", + "command.run_limited", + "preview.start", + "preview.validate", + ] { + assert!(matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + "art-director", + &art_director_binding.run_id, + Some(&art_director_binding.profile), + Some(&art_director_binding.binding_fingerprint), + command_id, + ), + Some(AgentRuntimeToolPolicyBlock::Denied(reason)) + if reason.contains("art-director") + && reason.contains("非 Canvas 验证") + && reason.contains(command_id) + )); + } + assert!(game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + "art-director", + &art_director_binding.run_id, + Some(&art_director_binding.profile), + Some(&art_director_binding.binding_fingerprint), + "canvas.asset_generate", + ) + .is_none()); + } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/preview.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/preview.rs index 3fd2ab07a..5de1fca43 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/preview.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/preview.rs @@ -175,13 +175,14 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate( } }; let completion_contract = - match autonomous_playtest_completion_contract_for_state_at(root, &runtime) { + match autonomous_playtest_execution_contract_for_state_at(root, &runtime) { Ok(contract) => contract, Err(error) => { return AgentRuntimeToolObservation { tool: "preview.validate".to_string(), status: "failed".to_string(), - summary: "自主构建完成合同不可用,未执行浏览器试玩".to_string(), + summary: "preview.validate 执行身份或自主构建完成合同不可用,未执行浏览器试玩" + .to_string(), detail: Some(redact_agent_runtime_project_paths(root, &error, 500)), }; } @@ -248,14 +249,10 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate( } }, }; - let (evidence_agent_id, evidence_run_id) = completion_contract - .as_ref() - .map(|contract| (contract.agent_id.as_str(), contract.run_id.as_str())) - .unwrap_or((agent_id, run_id)); let evidence_relative_root = format!( ".agent/runtime/browser-validations/{}/{}/{}", - agent_runtime_confirmation_path_component(evidence_agent_id, "agent"), - agent_runtime_confirmation_path_component(evidence_run_id, "run"), + agent_runtime_confirmation_path_component(agent_id, "agent"), + agent_runtime_confirmation_path_component(run_id, "run"), revision_before.revision, ); let evidence_root = match resolve_local_project_path(root, &evidence_relative_root) { @@ -379,6 +376,7 @@ pub(in crate::agent) async fn observe_agent_runtime_preview_validate( let receipt = match write_autonomous_playtest_receipt_at( root, contract, + &runtime, action_id, action_fingerprint, revision_after.revision, diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/project_ops.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/project_ops.rs index 2817a85ae..d0c3a8d73 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/project_ops.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/project_ops.rs @@ -193,13 +193,10 @@ where }; } }; - if agent_id == "design-foundation" { - if let Some(summary) = prepared.summaries().iter().find(|summary| { - agent_role_project_path_mutation_block(agent_id, tool, summary.path()).is_some() - }) { - return agent_role_project_path_mutation_block(agent_id, tool, summary.path()) - .expect("design-foundation unauthorized patchset path must be blocked"); - } + if let Some(blocked) = prepared.summaries().iter().find_map(|summary| { + agent_role_project_path_mutation_block(root, agent_id, run_id, tool, summary.path()) + }) { + return blocked; } if let Some(summary) = prepared.summaries().iter().find(|summary| { game_chat_delegated_art_agent_project_path_mutation_block( diff --git a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs index 365b5ae72..2249043c7 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/delegation.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/delegation.rs @@ -1008,10 +1008,16 @@ pub(crate) fn game_chat_art_delivery_gap_at( || delivery.structured_result.as_ref().is_none_or(|result| { result.contract_status != StaticDelegateContractStatus::EvidenceReady || !result.missing_expected_artifacts.is_empty() + || !result.verification_required + || result.verified_revision.is_none() + || !result + .evidence + .iter() + .any(|evidence| evidence.kind == "canvas.asset_generate") }) { return Ok(Some(format!( - "target={target_agent_id} delivery={} 尚未形成 EvidenceReady 美术回执", + "target={target_agent_id} delivery={} 尚未形成本人 canvas.asset_generate 的 EvidenceReady 美术回执", delivery.delegation_id ))); } diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs index 86e58bedc..4a5b23809 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/collaboration/static_deliveries.rs @@ -65,7 +65,8 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() { "referenceImageSrcs 第一项", "POST /api/external/v1/editor/images/generations(kind=ui-design)", "不得误用 POST /api/external/v1/editor/ui-designs/assets/extractions", - "canvas.asset_generate 成功动作本身就是当前 revision 的验证", + "canvas.asset_generate 成功只表示候选图片已生成并登记,不等于视觉验收完成", + "由 Runtime 在收束门内同时核对固定 owner 文档", "已有同路径画布资产时先核对登记", "检查已通过时不得重复生成或再次扣费", "纯场景图", @@ -85,6 +86,8 @@ fn visual_specialist_prompts_require_real_registered_image_deliveries() { "POST /api/external/v1/editor/images/generations(kind=spec)", "后续 UI 和透明图集共同引用", "不得用 generationInputs.artSpec JSON", + "成功只表示固定候选已生成并登记,不等于视觉门已经通过", + "由 Runtime 在收束时核对当前 revision", "缺少 resourceId 时不得提交最终回复", ] { assert!( diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs index 51ed3113a..3cdc569e9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/action_execution.rs @@ -600,20 +600,23 @@ async fn background_agent_runtime_can_run_limited_static_smoke() { ) .expect("start background task"); - let plan_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("plan llm request"); + let plan_request = wait_for_captured_mock_request(&receiver, "plan llm request").await; assert!(plan_request.contains("file.write")); assert!(plan_request.contains("command.run_limited")); - let final_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("final reply llm request"); + let final_request = wait_for_captured_mock_request(&receiver, "final reply llm request").await; assert!(final_request.contains("game.static_smoke 已完成")); assert!(final_request.contains("通过:game/index.html")); let root_display = root.to_string_lossy(); assert!(!final_request.contains(root_display.as_ref())); - let runtime = wait_for_agent_runtime_idle(&root, "code-prototype"); + let runtime = wait_for_agent_runtime_terminal_and_lane_release( + &root, + "code-prototype", + "code-smoke-run", + "idle", + "completed", + ) + .state; assert_eq!(runtime.status, "idle"); assert_eq!(runtime.phase, "completed"); assert!(runtime diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_build.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_build.rs index d3ec1a800..0776ea2b4 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_build.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/planning_strategy/autonomous_build.rs @@ -1,18 +1,21 @@ use super::super::support::*; use crate::{ - active_static_delegate_delivery_count_at, autonomous_game_build_root_run_active_at, - bind_supervisor_collaboration_policy_snapshot_at, build_static_delegate_structured_result_at, - claim_ready_static_delegate_receipts_at, create_or_read_static_delegate_delivery_at, - mark_static_delegate_claim_observed_at, mark_static_delegate_delivery_ready_at, - mark_static_delegate_delivery_ready_with_result_at, new_game_creation_app_seed_tasks, - new_static_delegate_delivery, new_static_delegate_delivery_with_contract, - observe_agent_runtime_run_status, record_command_run, record_preview_state, + active_static_delegate_delivery_count_at, + append_unique_game_creator_agent_runtime_pending_task, + autonomous_game_build_root_run_active_at, bind_supervisor_collaboration_policy_snapshot_at, + build_static_delegate_structured_result_at, claim_ready_static_delegate_receipts_at, + create_or_read_static_delegate_delivery_at, mark_static_delegate_claim_observed_at, + mark_static_delegate_delivery_ready_at, mark_static_delegate_delivery_ready_with_result_at, + new_game_creation_app_seed_tasks, new_static_delegate_delivery, + new_static_delegate_delivery_with_contract, observe_agent_runtime_run_status, + record_command_run, record_preview_state, refresh_agent_runtime_autonomous_convergence_snapshot_after_provider_at, start_game_creator_supervisor_background_task_for_session_at, static_delegate_completion_barrier_at, validate_agent_runtime_autonomous_plan_liveness, GameCreationAppCommandRunState, GameCreationAppCommandRunStatus, GameCreationAppPreviewStatus, StaticDelegateContractStatus, }; +use sha2::{Digest as _, Sha256}; fn write_autonomous_editor_api_config_for_test(config_dir: &Path, api_key: &str) { fs::create_dir_all(config_dir).expect("create autonomous runtime config dir"); @@ -605,6 +608,84 @@ fn bind_autonomous_specialist_runtime_for_test( runtime } +fn bind_autonomous_manifest_owner_runtime_for_test( + root: &Path, + parent_run_id: &str, + agent_id: &str, + task: &str, +) -> AgentRuntimeState { + let parent_session_id = resolve_agent_conversation_session_id_at( + root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + None, + true, + ) + .expect("resolve autonomous manifest parent session"); + append_unique_game_creator_agent_runtime_pending_task( + root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + &parent_session_id, + task, + parent_run_id, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("queue autonomous manifest parent and freeze its completion baseline"); + start_game_creator_agent_runtime_task_at( + root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + task, + parent_run_id, + AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE, + "调度固定 owner", + Vec::new(), + ) + .expect("start durable autonomous manifest parent"); + + update_manifest_task_status_at(root, agent_id, GameCreationAppTaskStatus::Running) + .expect("mark autonomous manifest owner running"); + let child_run_id = autonomous_manifest_ready_task_run_id_for_test(parent_run_id, agent_id); + let child_link = AgentRuntimeTaskLink { + parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()), + parent_run_id: Some(parent_run_id.to_string()), + delegation_id: None, + }; + let child_session_id = resolve_agent_conversation_session_id_at(root, agent_id, None, true) + .expect("resolve autonomous manifest owner session"); + append_unique_game_creator_agent_runtime_pending_task( + root, + agent_id, + &child_session_id, + task, + &child_run_id, + "agent-ready-task-scheduler", + None, + Some(&child_link), + ) + .expect("queue autonomous manifest owner"); + start_game_creator_agent_runtime_task_at( + root, + agent_id, + task, + &child_run_id, + "agent-ready-task-scheduler", + "执行固定 owner 交付", + Vec::new(), + ) + .expect("start autonomous manifest owner") +} + +fn autonomous_manifest_ready_task_run_id_for_test(parent_run_id: &str, task_id: &str) -> String { + let identity = format!("{parent_run_id}\n{task_id}\nagent-ready-task-scheduler"); + let fingerprint = format!("{:x}", Sha256::digest(identity.as_bytes())); + format!( + "autonomous-ready-{}-{}", + task_id, + fingerprint.chars().take(20).collect::() + ) +} + #[tokio::test] async fn autonomous_game_build_non_read_only_code_first_round_repairs_response_into_mutation_only() { @@ -838,6 +919,198 @@ async fn autonomous_game_build_unverified_mutation_immediately_repairs_into_veri fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn autonomous_manifest_code_prototype_requires_its_own_static_smoke_after_project_verify() { + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-autonomous-code-owner-smoke", + "自主构建程序 owner 静态验收测试", + ) + .expect("project init"); + fs::write( + root.join("package.json"), + br#"{"scripts":{"check":"node --check game/index.html"}}"#, + ) + .expect("write package manifest so project.verify is advertised diagnostically"); + let parent_run_id = "autonomous-code-owner-smoke-parent"; + let child_run_id = + autonomous_manifest_ready_task_run_id_for_test(parent_run_id, "code-prototype"); + let task = "实现可玩 game/index.html,并由 code-prototype 本人完成 game.static_smoke。"; + let response_arguments = serde_json::json!({ + "response": "可玩入口已经通过项目检查,可以交付。" + }) + .to_string(); + let smoke_arguments = serde_json::json!({ + "reason": "补齐 code-prototype 本人静态验收凭证", + "input": {"commandId": "game.static_smoke"} + }) + .to_string(); + let (sender, receiver) = mpsc::channel(); + let base_url = spawn_mock_llm_raw_responses_with_capture( + vec![ + native_agent_tool_plan_chat_response( + "call-autonomous-code-owner-project-verified-response", + AGENT_RUNTIME_RESPOND_FUNCTION_NAME, + response_arguments.clone(), + ), + native_agent_tool_plan_chat_response( + "call-autonomous-code-owner-static-smoke", + &native_runtime_function_name("command.run_limited") + .expect("limited command function"), + smoke_arguments, + ), + native_agent_tool_plan_chat_response( + "call-autonomous-code-owner-smoke-verified-response", + AGENT_RUNTIME_RESPOND_FUNCTION_NAME, + response_arguments, + ), + ], + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "agentLlm": {{ + "code-prototype": {{ + "apiKey": "autonomous-code-owner-smoke-key", + "baseUrl": {base_url:?}, + "model": "autonomous-code-owner-smoke-model", + "apiKind": "openai_chat", + "maxRetries": 0 + }} + }} +}}"# + )); + let runtime = bind_autonomous_manifest_owner_runtime_for_test( + &root, + parent_run_id, + "code-prototype", + task, + ); + assert_eq!(runtime.run_id, child_run_id); + let mutation_revision = prepare_agent_runtime_project_mutation_locked( + &root, + "code-prototype", + &child_run_id, + "file.patch", + ) + .expect("record code-prototype mutation"); + persist_project_verification_for_test( + &root, + "code-prototype", + &child_run_id, + "project.verify", + true, + ); + let project_verified_gate = + read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", &child_run_id) + .expect("read project.verify gate"); + assert_eq!( + project_verified_gate.mutation_revision, + Some(mutation_revision) + ); + assert_eq!( + project_verified_gate.verified_revision, + Some(mutation_revision) + ); + assert_eq!( + project_verified_gate.last_verification_tool.as_deref(), + Some("project.verify") + ); + assert_eq!(project_verified_gate.static_smoke_verified_revision, None); + let blocker = + project_verification_completion_blocker_at(&root, "code-prototype", &child_run_id, &[]) + .expect("project.verify alone must not complete code-prototype"); + assert!(blocker.summary.contains("game.static_smoke")); + + let repair_plan = request_game_creator_agent_background_tool_plan_for_test( + &root, + "code-prototype", + &runtime.session_id, + &child_run_id, + task, + &[], + 2, + 0, + ) + .await + .expect("repair project.verify-only delivery") + .expect("static-smoke repair plan"); + assert!(repair_plan.response.is_empty()); + assert_eq!(repair_plan.actions.len(), 1); + assert_eq!(repair_plan.actions[0].tool, "command.run_limited"); + assert_eq!( + repair_plan.actions[0].input["commandId"], + "game.static_smoke" + ); + + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("initial project.verify-only response request"); + let repair_request = receiver + .recv_timeout(Duration::from_secs(2)) + .expect("static-smoke-only repair request"); + assert!(repair_request.contains("project.verify 不能满足 code-prototype")); + assert_eq!( + captured_native_function_names_for_test(&repair_request), + ["command.run_limited"] + .into_iter() + .map(|tool| native_runtime_function_name(tool).expect("static smoke function")) + .collect::>() + ); + + persist_project_verification_for_test( + &root, + "code-prototype", + &child_run_id, + "game.static_smoke", + true, + ); + let smoke_gate = + read_game_creator_agent_runtime_verification_gate(&root, "code-prototype", &child_run_id) + .expect("read static smoke gate"); + assert_eq!( + smoke_gate.static_smoke_verified_revision, + Some(mutation_revision) + ); + let completed_plan = request_game_creator_agent_background_tool_plan_for_test( + &root, + "code-prototype", + &runtime.session_id, + &child_run_id, + task, + &[AgentRuntimeToolObservation { + tool: "command.run_limited".to_string(), + status: "ok".to_string(), + summary: "game.static_smoke 已通过当前 revision".to_string(), + detail: None, + }], + 3, + 0, + ) + .await + .expect("request delivery after static smoke") + .expect("code-prototype may deliver after its own static smoke"); + assert_eq!( + completed_plan.response, + "可玩入口已经通过项目检查,可以交付。" + ); + assert!(completed_plan.actions.is_empty()); + assert!(project_verification_completion_blocker_at( + &root, + "code-prototype", + &child_run_id, + &[], + ) + .is_none()); + receiver + .recv_timeout(Duration::from_secs(2)) + .expect("post-smoke final response request"); + assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); + + fs::remove_dir_all(root).ok(); +} + async fn assert_autonomous_repair_waits_for_receipt_observation_for_test(unobserved_claim: bool) { let root = unique_project_path(); let case_name = if unobserved_claim { @@ -3293,7 +3566,7 @@ async fn autonomous_game_build_verified_revision_forces_response_only_delivery() } #[tokio::test] -async fn autonomous_static_art_asset_verification_does_not_require_node_project() { +async fn autonomous_static_art_asset_delivery_uses_runtime_owner_validation_without_node_project() { let root = unique_project_path(); init_local_game_project_at( &root, @@ -3305,39 +3578,21 @@ async fn autonomous_static_art_asset_verification_does_not_require_node_project( !root.join("package.json").exists(), "static HTML fixture must not gain a Node project manifest" ); - fs::write( - root.join("assets/manifest.art.json"), - br#"{"assets":[{"path":"assets/art-spritesheet.png","kind":"art-spritesheet"}]}"#, - ) - .expect("write static art manifest"); - register_canvas_visual_asset_fixture(&root, "assets/art-spritesheet.png", "art-spritesheet"); - - let child_run_id = "autonomous-static-art-verification-child"; + let parent_run_id = "autonomous-static-art-verification-parent"; + let child_run_id = + autonomous_manifest_ready_task_run_id_for_test(parent_run_id, "art-asset-plan"); let task = "为纯静态 HTML 游戏生成并登记 assets/manifest.art.json 与 assets/art-spritesheet.png;项目不使用 Node 或 npm。"; let response_arguments = serde_json::json!({ "response": "美术清单和精灵图已经生成,可以交付。" }) .to_string(); - let smoke_arguments = serde_json::json!({ - "reason": "使用静态 smoke 验证当前纯静态项目 revision", - "input": {"commandId": "game.static_smoke"} - }) - .to_string(); let (sender, receiver) = mpsc::channel(); let base_url = spawn_mock_llm_raw_responses_with_capture( - vec![ - native_agent_tool_plan_chat_response( - "call-autonomous-static-art-response", - AGENT_RUNTIME_RESPOND_FUNCTION_NAME, - response_arguments, - ), - native_agent_tool_plan_chat_response( - "call-autonomous-static-art-smoke", - &native_runtime_function_name("command.run_limited") - .expect("limited command function"), - smoke_arguments, - ), - ], + vec![native_agent_tool_plan_chat_response( + "call-autonomous-static-art-response", + AGENT_RUNTIME_RESPOND_FUNCTION_NAME, + response_arguments, + )], Some(sender), ); let _config_guard = write_test_local_config(format!( @@ -3353,17 +3608,22 @@ async fn autonomous_static_art_asset_verification_does_not_require_node_project( }} }}"# )); - let runtime = bind_autonomous_specialist_runtime_for_test( + let runtime = bind_autonomous_manifest_owner_runtime_for_test( &root, - "autonomous-static-art-verification-parent", + parent_run_id, "art-asset-plan", - child_run_id, task, ); + fs::write( + root.join("assets/manifest.art.json"), + br#"{"assets":[{"path":"assets/art-spritesheet.png","kind":"art-spritesheet"}]}"#, + ) + .expect("write static art manifest after the parent completion baseline"); + register_canvas_visual_asset_fixture(&root, "assets/art-spritesheet.png", "art-spritesheet"); prepare_agent_runtime_project_mutation_locked( &root, "art-asset-plan", - child_run_id, + &child_run_id, "canvas.asset_generate", ) .expect("record generated art mutation"); @@ -3378,43 +3638,21 @@ async fn autonomous_static_art_asset_verification_does_not_require_node_project( &root, "art-asset-plan", &runtime.session_id, - child_run_id, + &child_run_id, task, &observations, 2, 0, ) .await - .expect("repair static art delivery into static verification") - .expect("static smoke verification plan"); - assert!(plan.response.is_empty()); - assert_eq!(plan.actions.len(), 1); - assert_eq!(plan.actions[0].tool, "command.run_limited"); - assert_eq!(plan.actions[0].input["commandId"], "game.static_smoke"); + .expect("accept static art delivery for runtime owner validation") + .expect("static art delivery plan"); + assert_eq!(plan.response, "美术清单和精灵图已经生成,可以交付。"); + assert!(plan.actions.is_empty()); receiver .recv_timeout(Duration::from_secs(2)) .expect("initial static art response request"); - let repair_request = receiver - .recv_timeout(Duration::from_secs(2)) - .expect("static art verification-only repair request"); - let repair_function_names = captured_native_function_names_for_test(&repair_request); - assert_eq!( - repair_function_names, - BTreeSet::from([ - native_runtime_function_name("command.run_limited").expect("static smoke function") - ]), - "pure static asset delivery must not expose package.json-backed project.verify" - ); - let repair_request_json = mock_http_request_json(&repair_request); - let repair_instruction = repair_request_json["messages"] - .as_array() - .and_then(|messages| messages.last()) - .and_then(|message| message.get("content")) - .and_then(serde_json::Value::as_str) - .expect("static art repair instruction"); - assert!(repair_instruction.contains("game.static_smoke")); - assert!(!repair_instruction.contains("project.verify")); assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err()); fs::remove_dir_all(root).ok(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs index f852192e0..a17612f30 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests/runtime_actions/policy.rs @@ -1569,15 +1569,125 @@ async fn autonomous_design_foundation_denies_indirect_execution_before_side_effe let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); assert!(!agent_db.contains("agent.runtime.project.verify")); assert!(!agent_db.contains("agent.runtime.mcp.call")); - assert!(game_creator_agent_runtime_tool_policy_rule_for_run( + assert!(matches!( + game_creator_agent_runtime_tool_policy_rule_for_run( + &root, + "design-foundation", + run_id, + Some(&binding.profile), + Some(&binding.binding_fingerprint), + "command.run_limited", + ), + Some(AgentRuntimeToolPolicyBlock::Denied(_)) + )); + let smoke = execute_game_creator_agent_runtime_tool_action( &root, "design-foundation", run_id, - Some(&binding.profile), - Some(&binding.binding_fingerprint), - "command.run_limited", + "不得借游戏 smoke 验证策划文档", + &AgentRuntimeToolAction { + tool: "command.run_limited".to_string(), + reason: Some("验证角色门禁先于静态检查".to_string()), + input: serde_json::json!({ "commandId": "game.static_smoke" }), + }, ) - .is_none()); + .await; + assert_eq!(smoke.status, "blocked"); + assert!(smoke.summary.contains("design-foundation")); + assert!(!root.join(".agent/logs/command.log").exists()); + + fs::remove_dir_all(root).ok(); +} + +#[tokio::test] +async fn autonomous_fixed_pre_code_owners_deny_manual_verification_before_side_effects() { + let root = unique_project_path(); + init_local_game_project_at( + &root, + "project-fixed-owner-manual-verification-policy", + "固定 owner 手工验证门禁测试", + ) + .expect("project init"); + let supervisor_run_id = "fixed-owner-manual-verification-parent-run"; + bind_game_creator_agent_runtime_run_profile_at( + &root, + GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID, + supervisor_run_id, + AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE, + Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD), + None, + ) + .expect("bind supervisor autonomous run"); + let verify_command = + "node -e \"require('fs').writeFileSync('owner-project-verify-executed.txt','executed')\""; + fs::write( + root.join("package.json"), + serde_json::json!({ + "scripts": { + "check": verify_command, + } + }) + .to_string(), + ) + .expect("write project verify canary script"); + + for agent_id in ["balance-seed", "art-asset-plan", "audio-asset-plan"] { + let run_id = format!("fixed-owner-{agent_id}-run"); + bind_game_creator_agent_runtime_run_profile_at( + &root, + agent_id, + &run_id, + "agent-delegate", + None, + Some(&AgentRuntimeTaskLink { + parent_agent_id: Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string()), + parent_run_id: Some(supervisor_run_id.to_string()), + delegation_id: Some(format!("fixed-owner-{agent_id}-delegation")), + }), + ) + .expect("bind delegated fixed owner autonomous run"); + + let project_verify = execute_game_creator_agent_runtime_tool_action( + &root, + agent_id, + &run_id, + "不得回退执行项目脚本", + &AgentRuntimeToolAction { + tool: "project.verify".to_string(), + reason: Some("验证固定 owner 门禁先于项目命令执行".to_string()), + input: serde_json::json!({ + "script": "check", + "expectedCommand": verify_command, + "timeoutSeconds": 30, + }), + }, + ) + .await; + let smoke = execute_game_creator_agent_runtime_tool_action( + &root, + agent_id, + &run_id, + "不得借游戏 smoke 验证固定 owner 产物", + &AgentRuntimeToolAction { + tool: "command.run_limited".to_string(), + reason: Some("验证固定 owner 门禁先于静态检查".to_string()), + input: serde_json::json!({ "commandId": "game.static_smoke" }), + }, + ) + .await; + + for observation in [&project_verify, &smoke] { + assert_eq!(observation.status, "blocked"); + assert!(observation.summary.contains("固定 owner")); + assert!(observation.summary.contains(agent_id)); + } + } + + assert!(!root.join("owner-project-verify-executed.txt").exists()); + assert!(!root.join(".agent/logs/command.log").exists()); + let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db"); + assert!(!agent_db.contains("agent.runtime.project.verify")); + assert!(!agent_db.contains("agent.runtime.command.run_limited")); fs::remove_dir_all(root).ok(); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 0659af6ae..2f72789c4 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -1,5 +1,17 @@ # 决策记录 +## 2026-08-11 固定 owner 产物验证与可玩验收分离 + +- 背景:真实新项目初始化后没有 `package.json`,默认 `game/index.html` 只是无 `` 的占位页。`design-foundation`、`balance-seed`、`art-asset-plan`、`audio-asset-plan` 位于 `code-prototype` 上游,只负责策划、数值、美术清单和音频清单;若要求它们执行 `project.verify` 或 `game.static_smoke`,前者没有可执行合同,后者只能检查尚未生成的占位游戏并必然失败。曾在测试中预先写入 `fake_llm_game_draft()` 会把占位入口替换成可玩页面,从而掩盖这条真实新项目死锁。 +- 决策:完整 GUI / CLI 16 任务 DAG 的四个 pre-code artifact-only owner 在尝试收束时,由 Runtime 内部按固定 owner 合同验证正式产物,不向 Provider 新增工具或 commandId,也不要求模型自行调用验证命令。内部验证类型固定为 `runtime.owner_artifacts_validate`;它写入现有 verification gate 的普通 `verifiedRevision`,不写 `staticSmokeVerifiedRevision`,也不生成 smoke / preview command trace。 +- 固定 owner 合同:`design-foundation` 对应 `memory/project.md` 与 `game/game_design.md`,`balance-seed` 对应 `game/balance.json`,`art-asset-plan` 对应 `assets/manifest.art.json`,`audio-asset-plan` 对应 `assets/manifest.audio.json`。同一份 canonical owner 映射同时驱动文件 write / patch / delete / patchset 边界、正式产物完成检查和 Runtime 内部验证,避免路径权限与完成合同漂移。验证要求普通文件有界读取且非空、JSON 可解析、文本不存在 incomplete marker,并相对根完成合同的 baseline 确认本轮产物确有变化。 +- 身份与失效:内部验证只接受完整 16 任务 DAG 中由 `agent-ready-task-scheduler` 启动的确定性直接 child、当前活跃 GUI / CLI Supervisor 根、正确 project / source / profile / Agent / run / parent / root / binding。错误 source、delegated run、历史或终态根、非当前活跃根、跨 Agent/run 凭证和身份不完整一律失败关闭。owner 再次 mutation 后旧 `verifiedRevision` 立即失效,恢复只能在相同完整身份和当前事实下确定性重验。 +- 可玩验收边界:`code-prototype` 继续对真实可玩入口执行 `game.static_smoke`;即使 `project.verify` 已通过,也必须保留覆盖本人 `mutationRevision` 的 `staticSmokeVerifiedRevision`,后续 preview 更新 last verification tool 不删除这份凭证。`preview-readiness` 继续以自己的 child run 对当时的最终 project revision 执行真实 `game.static_smoke`;`preview-playtest` 继续独立执行浏览器验收。GUI / CLI 试玩回执绑定确定性 `preview-playtest` child 的 agent/run/source/binding;game-chat 回执绑定唯一主 `code-prototype`,上游节点、根 Supervisor 和旧 v1 回执不能替代。较早 owner 的内部产物凭证不能替代任一可玩验收。`project-supervisor-game-chat` 的 source-bound 根 `code-prototype` 仍按现行单主合同负责同 run/current revision 的 smoke 与 desktop/mobile `preview.validate`。 +- Canvas 条件角色:未配置 External Editor API Key 时,`art-director` 保持只读协调;配置 Key 时,它是条件 Canvas owner,必须生成并登记 `assets/art-spec.png`,由成功的 `canvas.asset_generate` 形成普通验证凭证。它不属于上述四个 artifact-only owner,不能被只读分类吞掉;Provider 广告与执行 policy 均拒绝其 `project.verify`、`command.run_limited` 和 preview 工具。配置 Key 时 `design-foundation` 与 `art-asset-plan` 的固定图片、登记和视觉门仍按既有合同执行;内部 owner 验证不替代 Canvas 证据。game-chat 临时 `art-asset-plan` 只有在当前 `code-prototype -> agent-delegate`、durable delivery 与 binding 全链一致时才保留 Canvas 例外,并且完成门只认本人 `canvas.asset_generate` 的通过凭证。 +- 阶段边界:M0A-2 / M0-3 只统一前置固定 owner 产物验证与可玩验收,不扩到后置 `publish-package`,不实现 M1 plan source,也不改变 16 任务拓扑、game-chat 单主结构或项目 schema。2026-07-26 关于 `design-foundation` 不得调用 smoke、preview、进程工具且不得修改 `game/index.html` 的职责隔离继续有效,不被本条取代。 +- 验证方式:使用真实 `init_local_game_project_at` 证明无 `package.json`、占位入口 smoke 失败、owner 产物不齐时阻断、产物齐全后 Runtime 内部验证收束、无 smoke trace 且 `staticSmokeVerifiedRevision` 为空;再覆盖四个 owner 的路径矩阵、再次 mutation 失效、错误 source/run/root/parent/终态拒绝、恢复重验、跨 Agent/run 不可借用、`code-prototype` / `preview-readiness` smoke 非回归,以及 `art-director` 有/无 Key 的条件角色分类。 +- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。 + ## 2026-08-10 立项策划 Agent 使用 Fast GDD 版本审批作为完整构建的可选基线 - 背景:当前普通完整构建从简短需求直接进入 autonomous manifest,缺少用户在消耗完整构建成本前确认玩法方向、MVP 范围和原型验证项的正式环节;现有 `design-director` 是只读协调任务,`design-foundation` 又会自行补齐玩法定位,用户意图与实现之间没有可版本化、可审批、可恢复的信任根。 @@ -9,7 +21,7 @@ - 对话 checkpoint:plan 专用 `user.input_request` 保持现役 questions-only sole action;Runtime 先创建或复用与 v4 batch identity 全等的 pending sidecar,再把完整问题、hash 与 request/action/provider identity 写入 session.activeQuestion,最后展示决策卡。activeQuestion 尚未落时,session 仍等于 batch binding 才安装同一问题;session 已是合法 successor 且尚无 standalone pending/card/answer 时,旧问题未被消费,必须先补 lifecycle completed、supersede 并回读旧 batch、删除 exact pending sidecar并确认 absent,再清理 batch和从 successor 请求新问题,不能把合法 steer 一律送入 reconciliation。activeQuestion durable 后只允许 v4 batch `ready/nextActionIndex=0/唯一 approved member`,standalone pending 从 absent 直接进入 exact `auto + waiting-for-user-input + observation:null`。若在 runtime/card 发布前崩溃,只能按相同身份补齐或按上述 successor 清理顺序前滚;其它 sidecar/member/cursor/pending 状态和无法证明的 session 漂移失败关闭。用户回答先停在 `answer-prepared`;启动 checkpoint 前还必须重验 exact v4/standalone waiting anchor。随后同一 run 发起只广告 plan 专用 strict `update_agent_plan` 的 `plan-decision-checkpoint` Provider turn;Agent 用 `plan-decision-checkpoint.v1` 形成 answerSummary 和需要时的微型原型项。success handoff durable 后,Runtime 以 session revision/fingerprint CAS 追加 decisionsSummary/prototypeValidationItems/appliedAnswers 并增加 roundsUsed;新 session primary durable 是该轮解释的线性化点,此后才发布原 input observation。handoff 与 current session binding 相等时只重放 handoff;若 current 已是仍保留同题/答案的合法 successor,旧 handoff 不能跨 context 应用,replacement binding 必须以 `supersededCheckpointProviderRequestIds` 传递闭包记录旧 request;最终 appliedAnswers 根据完整 handoff 生成并保存对应 `supersededCheckpointHandoffs` 最小摘要后,旧 handoff 才稳定收口为被替换历史并可清理。长期读取只信 sessionFingerprint 保护的摘要,不要求历史 handoff 文件存在;链缺口、分叉或 identity 不同失败关闭。answerResponseId 只在所属 requestId 域内幂等,不同 request 合法复用同一文本值;下一题/submit 使用新 session 的另一普通 tool-plan 请求。原始回答、选项说明或聊天正文单独都不能猜解释,同 ID 同 checkpoint 只补投影,不同 answer/checkpoint identity 失败关闭。 - 审批与恢复:Runtime 在 GDD 提交前冻结 `approvalRequestId`,UI 为一次 GDD 审批决定生成并在传输重试中复用 `gdd-response-` responseId;它与现役 user-input answer transport 的同名 responseId 属于不同幂等域,不能跨域比较或恢复。approvalResponseId 只在所属 GDD ref/action 域内幂等,decision audit 使用 `(recordType,gddId,version,responseId)` 复合键,不同版本允许复用同一文本值。审批等待只写 `.agent/planning/pending.json`,不升级或重写 `game-creator-pending-action.v5`;该投影可由唯一待审 GDD 重建,receipt 后 observation 可确定性重建。最新版本已有任一 approve/revise/reject receipt 后才允许下一版本;revise/reject 由原 run 继续,approve 后必须由用户显式开始新 plan continuation,旧批准在新版本 approve 前继续有效。receipt 后固定修复 index/Markdown、`agent-runtime-plan-gdd-decided.v1` 专用幂等 decision audit、原 action terminal observation 和 session;原 run durable 消费 observation 后才清理 planning pending。提交点之后的投影失败仍返回成功 outcome,并以 `recoveryPending=true` 表示派生投影未齐,不回滚、覆盖或重编号不可变事实。 - 安全不变量:plan run 必须通过 durable top-level `project-supervisor + standard + project-supervisor-plan-chat` exact binding,action tool 仅 `file.read`、`file.list`、`user.input_request`、`plan.submit_gdd`,MCP 为空且 `webSearchEnabled=false`;Prompt/tool-plan/checkpoint/action batch/batch recovery/repair/context/completion 都跳过 Supervisor collaboration 合同。submit 是 sole action,并通过 main-loop 专用审批等待分支;checkpoint 无 action batch。每个 Provider request 的 effective model、api kind、stream、当前 apiKind 实际生效的 official fallback/Anthropic strict/OpenAI Chat token budget field、输出 token、reasoning/verbosity、tool choice、messages、结构化注入、实际工具目录、requestContextFingerprint 与 durable session binding 必须来自同一 captured session;不适用于当前 apiKind 的 adapter 字段固定为 null,除明确排除的 timeout/retry/backoff/log/URL/header/秘密外,任一实际请求语义变化都产生新 context fingerprint 与 base ID。Provider request ID、handoff ID 和 superseded 控制元数据只进 binding/base identity,禁止进入 messages/tools/structured injection;同 session protocol repair 原样继承 superseded 数组。协议无效且已通过 handoff storage 安全/容量门的 Provider 成功响应仍先持久化 raw response handoff 并补 lifecycle completed;恢复只重放验证并进入同一 repair,不重发原 request,而该 raw handoff 未通过 checkpoint validator,不能追加到 superseded 数组。raw handoff 因超限、敏感键、绝对路径、容量、identity 或 durable write 失败而无法安全提交时,不保存正文、不补 completed、不自动 repair/retry,只写安全诊断并进入 reconciliation。同 session/context 的下一 attempt 也必须先把旧 attempt durable 闭合为已知 failure 的 failed,或在 owner/lease/boot 证明物理请求已终止后闭合为 interrupted;合法 successor 的 replacement 同样只能在 handler 已取得并丢弃旧 response,或证明旧调用终止并回读 interrupted 后启动。任何旧终态回读前都不得新建 started。started 后只有在不存在后续业务消费证明时,合法 session successor 才会 interrupt 旧请求、supersede 尚未执行的 ready batch,或为 checkpoint 使用 `decision-round-{N}-session-{revision}-repair-0` 与新 base attempt 0 自动前滚;同 submission GDD、精确 activeQuestion 和精确 appliedAnswers 是优先于 stale 的三类消费证明,`started + ready batch` 先补真实 completed,binding 损坏则只进入 reconciliation。Runtime 在 session revision 1 固定写入不可由 Provider 改写的 `initial-request`,后续决定和最多 3 个原型项逐项匹配 session 真相。plan 无 command、smoke、preview、canvas、任务图、委派或通用文件写能力;retry 只在旧 run 已终态且无 pending 时保留原 plan source/profile,不能降级为 background。`game/fast_gdd.md` 是 Runtime 内部投影,不推进代码 mutation revision。前端只经 hydrate command 从严格 GDD/receipt/session 推导 `not_started|draft|ready_for_approval|revision_requested|approved|rejected`;receipt 隐藏 stale pending,合法投影未齐只返回 `recoveryPending=true`。完整构建仍由用户动作启动,直接开建必须显式声明,不能因 ref 缺失静默降级。 -- 分期边界:本条与关联方案完成 M0A-1 非交付阶段设计检查,不表示 M0A、M1 入口门或 M0 全部完成;`M0A-*` / `M0B-*` 只作为 PR 工作包标签。M1 策划闭环、M2 approvedGddRef 构建绑定和 M3 试玩/game-chat 复用尚未实现。M0/M1 不改变当前 16-task topology 或现役 game-chat 单主结构;M0-3 尚未开始,需新增 dated 决策显式取代 2026-07-26 的旧 smoke 排他表述,再对齐 foundation 写后固定 smoke 与 preview-readiness 最终验收。M1 可以并行做详细设计或技术 spike,但 checkpoint handoff 私有持久化决策未冻结前对应代码不得合入,M0-3 合入前依赖 Fast GDD source/tool policy 的 M1 代码也不得合入主线。M0-4 尚未开始,需封闭动态美术 retry 的 `assets/**` 边界并修复单主前端投影;它不阻塞 M1/M2,只阻塞 M3-4 和“M0 全部完成”。 +- 分期边界:本条与关联方案完成 M0A-1 非交付阶段设计检查,不表示 M0A、M1 入口门或 M0 全部完成;`M0A-*` / `M0B-*` 只作为 PR 工作包标签。M1 策划闭环、M2 approvedGddRef 构建绑定和 M3 试玩/game-chat 复用尚未实现。M0/M1 不改变当前 16-task topology 或现役 game-chat 单主结构;M0-3 正在以“固定 owner 产物验证与可玩验收分离”的后续 dated 决策对齐 Runtime、Prompt、policy 与测试,且保留 2026-07-26 对 `design-foundation` 禁止 smoke 的职责隔离。M1 可以并行做详细设计或技术 spike,但 checkpoint handoff 私有持久化决策未冻结前对应代码不得合入,M0-3 合入前依赖 Fast GDD source/tool policy 的 M1 代码也不得合入主线。M0-4 尚未开始,需封闭动态美术 retry 的 `assets/**` 边界并修复单主前端投影;它不阻塞 M1/M2,只阻塞 M3-4 和“M0 全部完成”。 - 影响范围:AI 游戏创作客户端、Project Supervisor、Agent Runtime、Prompt Bundle、本地项目 sidecar、项目开发工作台和后续完整构建准入。 - 验证方式:M0 验证 tracked 技术方案、索引、注册表、golden 指纹、提交/恢复合同和决策记录自包含一致;M1~M3 分别按关联技术方案的阶段门禁执行,不能以文档合入冒充功能完成。 - 关联文档:`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`、`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/prd/【AI游戏创作】项目开发工作台PRD-2026-07-20.md`。 @@ -5875,7 +5887,7 @@ - 固定输出合同:`art-director` 的视觉规范图固定为 `assets/art-spec.png / icon-spec`,`design-foundation` 的规范界面图固定为 `assets/ui-prototype.png / 16:9 / 2K / ui-prototype`,`art-asset-plan` 的首版美术图固定为 `assets/art-spritesheet.png / 1:1 / 1K / art-spritesheet`。普通生成始终 `replaceExisting=false`,已有有效登记时复用,不得删除后重生、改路径、改规格或重复扣费。 - 覆盖授权:`replaceExisting=true` 只允许来自 Project Supervisor 建立的唯一静态 repair delivery;当前 run 必须绑定带 `repairOfDelegationId` 的静态专业 Agent,原 delivery 已由同一父 Agent / 父 run 认领,目标 Agent 与固定 `expectedArtifacts` 逐项一致。普通首轮、动态 child、Supervisor 直接动作、未认领原回执、返工的再次返工或不在原合同内的路径一律失败关闭。 - stale 防护:发起外部生成前冻结待替换固定路径与原文件 SHA-256;下载完成并取得项目写锁后,提交前重新解析相同路径并复算 fingerprint。路径、文件内容或 fingerprint 在请求期间发生变化时拒绝覆盖,保留并发产生的当前文件;不能因远端生成已经计费或成功就用过期结果覆盖新 revision。 -- `design-foundation` 边界:该 Agent 只拥有 `memory/project.md`、`game/game_design.md`,以及配置画布 Key 时固定的 `assets/ui-prototype.png`。Runtime 文件写入 / patchset / delete 门禁必须阻止其修改 `game/index.html` 或其它程序、发布、音频和美术文件;它不得调用 `game.static_smoke`、`preview.start`、`preview.validate`、进程工具、整项目恢复或自行进行桌面 / 移动试玩。只有 `preview-readiness` 可额外执行固定 `game.static_smoke`,只有 `preview-playtest` 可额外执行 `preview.validate`;预览与 playtest 不能仅依赖 prompt 自律。 +- `design-foundation` 边界:该 Agent 只拥有 `memory/project.md`、`game/game_design.md`,以及配置画布 Key 时固定的 `assets/ui-prototype.png`。Runtime 文件写入 / patchset / delete 门禁必须阻止其修改 `game/index.html` 或其它程序、发布、音频和美术文件;它不得调用 `game.static_smoke`、`preview.start`、`preview.validate`、进程工具、整项目恢复或自行进行桌面 / 移动试玩。在只读质量任务中,只有 `preview-readiness` 可额外执行固定 `game.static_smoke`,只有 `preview-playtest` 可额外执行 `preview.validate`;程序写入 owner `code-prototype` 仍必须对本人可玩入口执行 `game.static_smoke`。预览与 playtest 不能仅依赖 prompt 自律。 ## 2026-07-26 完成基线、画布审计与并发补验失败关闭 diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 68dd5e621..0f1d0eb3b 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -14,6 +14,18 @@ - 关联:相关文件、文档、提交或 Issue ``` +## 不能用可玩游戏 smoke 验证 code-prototype 上游的固定文档产物 + +- 现象:真实新项目的 `design-foundation`、`balance-seed`、`art-asset-plan` 或 `audio-asset-plan` 已写完自己的固定文件,却始终无法收束;`project.verify` 因项目没有 `package.json` 不可用,`game.static_smoke` 又报告缺少活动 ``。测试若先调用 `fake_llm_game_draft()`,同一路径却会“通过”。 +- 原因:初始化 `game/index.html` 只是无 `` 的占位页,真正游戏要到下游 `code-prototype` 才生成。把所有 mutation verification 都等同于可玩游戏 smoke,会让上游 artifact-only owner 在依赖顺序上自锁;预写 fake game 的夹具提前完成了下游职责,掩盖了真实新项目路径。 +- 处理:四个 pre-code 固定 owner 在最终收束门由 Runtime 内部验证 canonical 产物:普通文件有界读取、非空,JSON 可解析,无 incomplete marker,且相对根完成合同 baseline 已变化。该能力不进入 Provider 工具目录,不新增 commandId;凭证类型为 `runtime.owner_artifacts_validate`,只写普通 `verifiedRevision`,不得写 `staticSmokeVerifiedRevision` 或制造 smoke / preview trace。文件路径门与验证必须复用同一 canonical owner 映射。`code-prototype` 和 `preview-readiness` 继续执行真实 `game.static_smoke`,`preview-playtest` 继续独立浏览器验收;`publish-package` 不借本修复扩入内部验证。 +- 身份与恢复:只允许完整 GUI / CLI 16 任务 DAG 的 `agent-ready-task-scheduler` 确定性直接 child、当前活跃根和正确 parent/binding;错误 source、delegated run、历史/终态根、非当前 root、跨 Agent/run 一律失败关闭。owner 再次 mutation 必须令旧凭证失效;相同身份恢复时可按当前磁盘事实确定性重验。 +- Canvas 补充:未配置 External Editor API Key 时 `art-director` 是只读协调;配置 Key 时它是条件 Canvas owner,只广告并执行 `canvas.asset_generate`,拒绝 `project.verify`、`command.run_limited` 和 preview。game-chat 临时 `art-asset-plan` 必须同时满足当前 `code-prototype -> agent-delegate`、durable delivery 与 binding 身份,并只认本人 Canvas 生成凭证,不能借普通验证。四个 fixed owner 配置 Key 后仍须满足既有图片、Canvas 登记和视觉门;内部文件验证不替代这些证据。 +- 可玩凭证补充:完整 DAG 的 `code-prototype` 不能用已通过的 `project.verify` 代替本人 `game.static_smoke`;完成门要求 `staticSmokeVerifiedRevision >= mutationRevision`。GUI / CLI 的 preview 回执只能由确定性 `preview-playtest` child 生成,game-chat 只由唯一主 `code-prototype` 生成;回执必须冻结 executor Agent/run/source/binding,旧 v1 回执按缺失处理并重跑。 +- 并发恢复补充:自主根任务 journal 写入后建立或重建 completion contract 时,初始 manifest reset 与 continuation reconciliation reset 不能重新使用 fail-fast 项目锁。异步 child finalization 可以合法插入两次取锁之间,使已入 journal 的新根被误记为 `completion-contract-failed`。这两条 reset 必须使用现有有界等待项目锁,超时仍失败关闭;只验证 scheduler 合同的测试应预占 child Runtime lane,不能真实启动后台 worker 后再手工改 manifest。确定性回归要显式持锁,分别证明初始合同与 continuation 合同等待释放后成功落盘。 +- 验证:夹具必须从 `init_local_game_project_at` 开始,先断言无 `package.json` 且占位入口 smoke 失败,再证明产物不齐阻断、齐全后内部验证通过、无 smoke trace、再次 mutation 失效;另覆盖四个 owner 路径矩阵、错误身份、恢复、跨 run 凭证、`art-director` 有/无 Key、动态美术借凭证拒绝、`code-prototype` project.verify-only 阻断和试玩 executor 身份。 +- 关联:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`、`docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md`。 + ## 派生 Debug 会让完整配置经应用状态递归进入日志 - 现象:配置和状态当前没有直接日志调用,但新增一行 `debug!(?state, ...)` 或 `format!("{config:?}")` 就能把 JWT、后台口令、支付私钥、OSS / provider key 与 SpacetimeDB token 一次性写入日志及 OTel 留存面。 @@ -3942,7 +3954,7 @@ - 现象:视觉 Agent 发现候选图不合格后,可能先删除 `assets/ui-prototype.png` 或 `assets/art-spritesheet.png`,再用猜测的尺寸、比例或另一条路径重新生成;远端生成期间项目文件又可能被其它 Agent 更新,迟到结果覆盖较新的文件。`design-foundation` 为了让静态或浏览器检查通过,也可能顺手改写 `game/index.html` 或自行启动 preview。 - 原因:把“允许一次语义返工”误解成“视觉 Agent 可以任意覆盖”,且只在 prompt 中描述角色职责,没有在 replacement 授权、文件写入、工具策略和提交时 fingerprint 上强制执行。 - 处理:固定 UI 与 spritesheet 路径、比例、尺寸、kind 和 label;普通生成 `replaceExisting=false`。只有 Project Supervisor 对已认领原 delivery 建立的唯一静态 repair,且父 run、目标 Agent 与 `expectedArtifacts` 全部匹配时,才允许 `replaceExisting=true` 原位替换;不得先删除固定正式产物,也不得对 repair 再 repair。请求外部生成前记录原路径 SHA-256,取得写锁准备提交时复算;不一致即按 stale fingerprint 失败关闭并保留当前文件。 -- 职责隔离:`design-foundation` 只写 `memory/project.md`、`game/game_design.md` 和可选固定 UI 原型。Runtime 必须同时在单文件写入、patchset、delete 与工具 policy 层拒绝其修改 `game/index.html`、其它实现文件、启动 preview / playtest、运行进程、调用 `game.static_smoke` 或整项目恢复;只有 `preview-readiness` 可执行固定 smoke,只有 `preview-playtest` 可执行浏览器验收。 +- 职责隔离(2026-08-11 补充):`design-foundation` 只写 `memory/project.md`、`game/game_design.md` 和可选固定 UI 原型。Runtime 必须同时在单文件写入、patchset、delete 与工具 policy 层拒绝其修改 `game/index.html`、其它实现文件、启动 preview / playtest、运行进程、执行 `project.verify`、`game.static_smoke` 或整项目恢复。它的两个固定文本产物由 Runtime 在收束门内验证;这不向模型开放验证工具,也不替代 `preview-readiness` 的最终静态 smoke 或 `preview-playtest` 的独立浏览器验收。 - 画布配置一致性:未配置 External Editor API Key 时,`design-foundation / art-asset-plan` 的委派合同与 manifest 终态投影必须一起降级为文本产物,不能仍把 `assets/ui-prototype.png / assets/art-spritesheet.png` 作为完成条件;配置 Key 时两张固定图片继续是严格必需产物。委派、完成合同和 manifest 投影必须读取同一配置事实,禁止一层降级、另一层仍要求图片。 - 验证与状态:当前回归已覆盖固定合同拒绝漂移、已登记 spritesheet 删除保护、静态 repair 授权、并发修改触发 stale fingerprint、`design-foundation` 的 write / patchset / delete 和 preview 工具拒绝。2026-07-27 的独立 75 分钟上限外部 E2E 已在同一轮完成两张真实画布图片、固定 `16` 任务、当前 revision 静态与双视口试玩并安全清理,当前状态为 **PASS**;后续改动仍须新轮复验。 diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index b2716c956..11126b180 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -855,7 +855,11 @@ game-project/ - 2026-07-28 Windows Provider retry 恢复修正:`provider_retry::list_at` 从绝对路径剥离项目 root 后,按路径组件重组成 `/` 分隔的 portable UTF-8 相对路径,再交给 Runtime JSON sidecar 读取器。不能直接使用 Windows `Path::to_str()` 的反斜杠文本,否则应用重启、Runner recovery scan 和正式 `--agent-resume` 都无法推进已到期的 `waiting-for-provider-retry` run。全部 provider retry 列举、previous 恢复、去重和路径冲突回归必须在真实 Windows 通过。 - `npm run agc:test:chat` 未显式指定配置且找不到 AppData 配置时,只在 stdin / stdout 都是 TTY 时询问并启动同一 `agc:config --configure-only` 向导,非 TTY 或显式无效 `--config-dir` 直接失败。测试环境只把主配置和存在时的 local overlay 复制到带随机 sentinel 的单次隔离 AppData;副本必须是独立的无符号链接普通文件,POSIX 权限为目录 `0700` / 文件 `0600`,不复制正式 Runner endpoint、lock 或其它 AppData。自动任务默认 50 分钟且可用 `--timeout-minutes` 显式设置;超时或信号会终止独立子进程树,POSIX 先向进程组发送 `SIGTERM`、等待 10 秒后发送 `SIGKILL` 并再等待 5 秒,Windows 使用 `taskkill /T` 并在强制阶段追加 `/F`。超时和信号分别以 `124 / 130 / 143` 失败退出,隔离 Runner 收束另有 20 秒上限;Runner 未空闲或收束失败时保留隔离配置和项目,验收未完成但 Runner 已安全退出时只保留一次性项目证据,不把中断报告为成功,也不误删正式 AppData。 - 自动验收现在严格要求 manifest 恰好包含固定 16 个不重复 task ID 且全部为 `completed`,并逐任务核对当前父 Run 下唯一 logical run、一次 started、一次 completed、零 failed / cancelled 和一次 manifest projection;七份基础正式产物存在并满足文件 / JSON / 非占位入口检查,配置画布 API Key 时再增加 `art-spec / ui-prototype / art-spritesheet` 三张图片。PNG 验收不止检查 magic / IHDR / 比例,还会校验 chunk CRC、zlib 解压、scanline 长度、索引色 PLTE 和未知 critical chunk。Runtime 根 Supervisor 的完成合同已升级为 `game-creator-autonomous-completion-contract.v2`,`baselineArtifacts` 必填并纳入指纹,旧 v1 或缺基线合同失败关闭;最终门禁要求最后一次验证工具是 `game.static_smoke`、状态通过且 `verifiedRevision == currentRevision`。`preview.validate` 回执必须绑定同一 Agent、run、current revision、当前 `game/index.html` 摘要、固定试玩场景、持久浏览器报告以及 desktop / mobile 两张截图的路径、摘要和 PNG 身份,任一证据缺失、变化、过期或来自其它 run / revision 都阻止最终回复。旧两图合同的确定性证据不替代新三图 DAG 验收;新合同实现后必须新起独立单轮。 -- `design-foundation` 已增加专属职责边界:项目文件只允许写 `memory/project.md` 与 `game/game_design.md`;配置 External Editor API Key 且合同要求界面原型时,只额外允许固定 `assets/ui-prototype.png`。它不得创建、修改、删除或补丁 `game/index.html`,不得改动其它程序实现、发布、音频或美术素材,也不得调用 `preview.start`、`preview.validate`、`game.static_smoke`,或借 `command.exec / command.start / command.run_limited` 启动预览服务、浏览器、Playwright 和桌面 / 移动试玩。程序和质量 Agent 的共享 Runtime 工具合同不因此缩减;有 / 无画布配置和其它 Agent 不受影响的聚焦回归为 `3/3` 通过。 +- 2026-08-11 M0-3 将固定 owner 产物验证与可玩验收分离。真实 `init_local_game_project_at` 项目没有 `package.json`,默认 `game/index.html` 是无活动 `` 的占位页;`design-foundation / balance-seed / art-asset-plan / audio-asset-plan` 又全部位于 `code-prototype` 上游,因此 `project.verify` 不可用,`game.static_smoke` 只能检查尚未生成的游戏并必然失败。测试不得预写 `fake_llm_game_draft()` 把占位页替换成可玩页面后再证明活性;该夹具会提前完成下游职责并掩盖真实新项目死锁。 +- 四个 pre-code artifact-only owner 在尝试收束时,由 Runtime 内部按 canonical owner 映射验证固定正式产物:`design-foundation -> memory/project.md + game/game_design.md`、`balance-seed -> game/balance.json`、`art-asset-plan -> assets/manifest.art.json`、`audio-asset-plan -> assets/manifest.audio.json`。同一映射还必须驱动 file write / patch / delete / patchset 边界和根完成检查。内部验证执行普通文件有界读取、非空、JSON 可解析、无 incomplete marker 和相对根完成合同 baseline 已变化;Provider 不获得新工具或 commandId,owner 也不调用 `project.verify`、`game.static_smoke` 或 preview。验证类型固定为 `runtime.owner_artifacts_validate`,只更新现有 gate 的普通 `verifiedRevision`,不写 `staticSmokeVerifiedRevision`,不生成 smoke / preview trace。 +- 内部 owner 验证只接受 GUI / CLI 完整 16 任务 DAG 中 `agent-ready-task-scheduler` 启动的确定性直接 child、当前活跃根和完整 project/source/profile/Agent/run/parent/root/binding 身份。错误 source、delegated run、历史或终态根、非当前活跃根、跨 Agent/run 凭证均失败关闭;再次 mutation 使旧凭证失效,相同身份恢复可按当前事实确定性重验。本阶段不扩到后置 `publish-package`。`code-prototype` 与 `preview-readiness` 继续执行真实 `game.static_smoke`,`preview-playtest` 继续独立执行浏览器验收;任何 owner 文件凭证都不能替代可玩证据。 +- M0-3 可玩与条件 Canvas 凭证继续按执行 owner 隔离:完整 DAG 的 `code-prototype` 即使 `project.verify` 已通过,仍须由本人取得覆盖 `mutationRevision` 的 `staticSmokeVerifiedRevision`;GUI / CLI 的试玩回执只能由当前确定性 `preview-playtest` child 写入 executor Agent/run/source/binding,game-chat 只保留唯一主 `code-prototype` 路径,旧 v1 回执按缺失处理。game-chat 临时 `art-asset-plan` 只有在当前 `code-prototype -> agent-delegate`、durable delivery 与 binding 全链一致时保留 Canvas 例外,并且只认本人 `canvas.asset_generate` 凭证;`art-director` 有 Key 时也只广告并执行 Canvas 生图,无 `project.verify`、command 或 preview 权限。 +- `design-foundation` 的 2026-07-26 职责隔离继续有效:项目文件仍只允许 `memory/project.md`、`game/game_design.md` 和配置 Key 时的固定 `assets/ui-prototype.png`,禁止修改 `game/index.html`、调用 smoke / preview / process 或恢复整项目。未配置 External Editor API Key 时 `art-director` 保持只读协调;配置 Key 时它是条件 Canvas owner,必须生成并登记 `assets/art-spec.png`,成功 `canvas.asset_generate` 为本人当前 revision 形成普通验证凭证,不能被只读分类吞掉。配置 Key 时 UI 原型、透明图集、Canvas 登记和视觉门仍按既有合同执行,内部 owner 文件验证不替代图片证据。 - `canvas.asset_generate.replaceExisting` 默认并必须保持 `false`;只有静态专业 Agent 的 `delegated-*` 唯一 repair run 才能申请 `true`。Runtime 要求当前 delivery 带 `repairOfDelegationId`,原 delivery 已被同一父 Agent / 父 run 认领,原始与返工合同的目标 Agent 和精确 `expectedArtifacts` 路径一致;普通 run、未声明路径、错误 Agent、未认领原交付或缺失原图都失败关闭。图片生成仍服从 `art-director` / `design-foundation` / `art-asset-plan` 的固定输出路径、比例、尺寸、kind 和 label,禁止先删除正式图片;请求前记录旧文件 SHA-256,外部生成返回后在项目写锁内复核,旧图在网络请求期间变化即拒绝覆盖。授权替换先写私有临时文件,再以备份 / rename 切换;落盘或 manifest 登记失败时恢复旧图,不把新旧文件并存状态当作成功。 - 2026-07-28 起,在既有 16-task manifest 内固定正式视觉 DAG,不新增平行任务系统:`art-director` 先通过 `/api/external/v1/editor/images/generations` 的 `kind=spec` 生成 `assets/art-spec.png`,并登记为 `assetKind=icon-spec`;`design-foundation` 使用该规范图的 External Editor 稳定资源 ID 作为 `referenceImageSrcs` 中的视觉规范参考,再通过同一 images 接口的 `kind=ui-design` 生成完整 `assets/ui-prototype.png`;`art-asset-plan` 以同一 `assets/art-spec.png` 资源 ID 作为必填 `referenceId` 调用 `/api/external/v1/editor/icon-spritesheets/generations`,传入具体 `iconDescriptions`、`screenColor=auto`、同名画布 / 素材库与 `canvasCompletion`,生成透明 `assets/art-spritesheet.png`。`generationInputs.artSpec` 只是辅助结构化上下文,不能代替真实 `art-spec.png`;严禁把 `assets/ui-prototype.png` 当作图集规范图。规范图缺失、未登记为当前画布的 `icon-spec` 或缺少稳定资源 ID 时,两个下游任务均等待 `art-director`,不得退回普通生图。UI extraction 只适用于已有且带红框标注的 UI 设计图,不用于生成完整 UI,也不进入本次 canonical DAG。单波最多 `3` 个静态职责的资源上限保持不变,只调整现有任务的依赖边与就绪顺序。图集返回 `warning` 时以 `postprocess-failed-source-preserved` 源图保留语义失败关闭,不把不透明源图登记为正式 spritesheet,也不自动重试;仅有 `sliceWarning` 时完整透明图集仍可登记,但必须原样保留切片失败原因。客户端下载后还要解码 PNG 并确认至少存在一个 alpha 小于 255 的像素,未形成真实透明像素时拒绝落盘和 manifest 登记。 - 旧项目已有同路径派生图但缺少上述 provenance 时,一律标记为 legacy,不得只因文件、kind 或通用视觉检查存在就完成。原位替换仍走显式 repair:`design-foundation` 与 `art-asset-plan` 先在同一 Supervisor 批次分别建立 owner 精确原合同并交付 `needs-repair`,父 run 认领后再在同一批次分别发起各自唯一 repair;两个 repair 合称一个显式视觉返工阶段。`art-director` 不得跨 owner 声明或替换 UI / spritesheet,Runtime 在委派落盘前就拒绝这类合同,不再等到生图阶段才失败。 diff --git a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md index 56e53a0cb..87ad95ae3 100644 --- a/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md +++ b/docs/technical/【技术方案】立项策划Agent(Fast GDD)-2026-08-10.md @@ -1,7 +1,7 @@ # 立项策划 Agent(Fast GDD)技术方案 - 日期:2026-08-10 -- 状态:M0A-1 非交付阶段设计检查通过;M0A-2 / M0-3 尚未开始,M1~M3 功能尚未实现 +- 状态:M0A-1 非交付阶段设计检查通过;M0A-2 / M0-3 合同对齐实施中,M1~M3 功能尚未实现 - 适用范围:AI 游戏创作独立 App、Project Supervisor、Agent Runtime、本地项目策划 sidecar 与后续完整构建准入 - 当前实现边界:本文件是后续详细设计与实现的仓库内阶段基线;合入本文只代表 M0A-1 设计检查点,不代表立项策划入口、审批 UI、Runtime 持久化或构建绑定已经可用 @@ -1329,15 +1329,22 @@ receipt 永远压过 stale pending:一旦该版本存在有效 receipt,pendi 6. 所有 mutation 在项目锁内重新核对当前 root/project/run identity;跨项目复制、旧 run、错误 source/profile/binding 失败关闭。 7. 构建始终由用户动作启动;plan tool、approval observation 和 final reply 都不能直接播种 DAG。 -`game.static_smoke` 的目标能力与职责固定区分如下;正式任务 M0-3(PR 工作包标签 `M0A-2`)负责新增 dated 决策,明确取代 2026-07-26 旧决策中的排他性 smoke 表述,并把 tracked 文档、Prompt、policy 与测试统一到该裁决。在 M0-3 合入前,本表是待实施目标,不能声称旧冲突已经消失: +正式任务 M0-3(PR 工作包标签 `M0A-2`)统一“owner 固定产物验证”与“可玩验收”的边界。真实新项目没有 `package.json`,默认 `game/index.html` 是无活动 `` 的占位页;在 `code-prototype` 之前运行 `project.verify` 或 `game.static_smoke` 都不能验证策划/JSON,后者只会对尚未生成的占位游戏失败。测试不得用预写 `fake_llm_game_draft()` 的夹具替换这条真实初始化路径。2026-07-26 对 `design-foundation` 禁止 smoke、preview、进程工具和 `game/index.html` 写入的职责隔离继续有效,不被 M0-3 取代。 -| Runtime 路径 | Agent | 工具边界 | 职责 | +| Runtime 路径 | Agent / task | 验证边界 | 阶段职责 | | --- | --- | --- | --- | | 立项策划 | 立项策划 persona | command/smoke/preview 全部不可见且执行拒绝 | 无构建或验证职责 | -| 完整 16 任务 DAG | `design-foundation` | 只允许固定 `game.static_smoke` 验证本人当前 mutation revision;拒绝 preview/process/其它 command | 写后自检,不是最终验收 | -| 完整 16 任务 DAG | `preview-readiness` | 允许固定 `game.static_smoke` | 唯一正式最终静态验收任务 | +| 完整 16 任务 DAG | `design-foundation`、`balance-seed`、`art-asset-plan`、`audio-asset-plan` | Provider 不获得验证工具;收束时由 Runtime 内部以 `runtime.owner_artifacts_validate` 验证 canonical 固定文件,只写普通 `verifiedRevision` | pre-code owner 产物验真,不是可玩验收 | +| 完整 16 任务 DAG | `art-director` | 无 Editor Key 时只读;有 Key 时为条件 Canvas owner,成功 `canvas.asset_generate` 形成普通验证凭证 | 只交付并登记 `assets/art-spec.png`,无 smoke / preview 权限 | +| 完整 16 任务 DAG | `code-prototype` | 对真实可玩入口执行 `game.static_smoke` | 代码原型写入与本人可玩静态自检 | +| 完整 16 任务 DAG | `preview-readiness` | 以自己的 child run 对最终 project revision 执行 `game.static_smoke` | 正式最终静态验收 | +| 完整 16 任务 DAG | `preview-playtest` | 独立执行 `preview.validate` | 正式浏览器试玩验收 | | game-chat 单主 | 根 `code-prototype` | source-bound 固定 smoke + desktop/mobile `preview.validate` | 对当前单主可玩 revision 负责 | -| game-chat 动态美术 child | `assets/**` 范围内交付;无根最终验收权 | 不得写 game/memory/.agent 或执行 command/preview | 只交付美术回执 | +| game-chat 动态美术 child | `art-director` / `art-asset-plan` | 只在 `assets/**` 范围交付,不得执行 command/preview | 只交付美术回执,无根最终验收权 | + +四个固定 owner 的 canonical 路径分别是 `memory/project.md + game/game_design.md`、`game/balance.json`、`assets/manifest.art.json`、`assets/manifest.audio.json`。同一映射必须同时驱动写入边界、完成检查与内部验证;验证要求有界读取、非空、JSON 可解析、无 incomplete marker,并相对根完成合同 baseline 已变化。内部验证不新增 Provider 可见 tool / commandId,不写 `staticSmokeVerifiedRevision`,不产生 smoke / preview trace;错误 source、delegated run、错误或终态 root、错误 parent/binding、跨 Agent/run 和非当前活跃根全部失败关闭,再次 mutation 必须使旧凭证失效。本阶段明确不扩到后置 `publish-package`;配置 Key 时的 UI 原型、透明图集、Canvas 登记和视觉验收仍是附加必需证据,不能被固定文件验证替代。 + +Canvas 是条件外部能力,不是 M0-3 四个固定 owner 的通用前置:没有 Editor Key 时,固定 owner 仍按 canonical 文件由 Runtime 内部验证;有 Key 时,`art-director` 只获得 `canvas.asset_generate` 的规范图职责,Provider 广告与执行 policy 均拒绝 `project.verify`、`command.run_limited` 和 preview 工具。game-chat 临时 `art-asset-plan` 只有在当前根 `code-prototype -> agent-delegate`、durable delivery 与 Run Profile binding 全链一致时才可沿用 Canvas 路径,并且完成门只认本人 `canvas.asset_generate` 的通过凭证,不能借用 smoke 或 `project.verify`。完整 DAG 的 `code-prototype` 即使已经通过 `project.verify`,仍必须保留覆盖本人 `mutationRevision` 的 `staticSmokeVerifiedRevision`;后续 preview 更新 last verification tool 不删除这份 smoke 凭证。GUI/CLI 的试玩回执必须绑定确定性 `preview-playtest` child 的 agent/run/source/binding,game-chat 则继续绑定唯一主 `code-prototype`;上游代码节点、根 Supervisor 或旧 v1 回执均不能替代当前执行 owner,旧 v1 回执按缺失处理并要求重新试玩。 正式任务 M0-4 的 PR 工作包 `M0B-1` 还必须让 `agent-delegate` 与 `agent-delegate-retry` 共用严格动态美术 lineage predicate;当前 retry source 不能绕过 `assets/**`。该修复不属于本文档 PR 的功能实现,但在 `M0B-2` 收敛 game-chat 前端投影前必须完成。 @@ -1416,7 +1423,7 @@ M0 文档 PR 本身最低验证:Markdown 结构与三张 Mermaid 图可解析 | 16 任务 DAG | `server-rs/crates/shared-contracts/src/game_creation_app.rs:263-425` | M0/M1 不改拓扑 | | design-director 现状 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs:70-85`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/task_start.rs:1480-1506` | mutation owner 清单不含 design-director,因此当前落入只读协调 Prompt;M2 才确定性化 | | scheduler delivery | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/delivery.rs:171-185` | 下游不能依赖普通 durable delivery,必须读权威文件/ref | -| foundation smoke | `apps/ai-game-creator-shell/src-tauri/src/agent/prompt.rs:487-499`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs:10-32,82-89` | 现行写后自检能力与旧文字有漂移,M0-3 / 工作包 M0A-2 显式裁决并对齐 | +| owner 产物验证 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/autonomous_completion.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs` | M0-3 / M0A-2 让四个 pre-code fixed owner 复用 canonical 路径映射,由 Runtime 内部验固定产物;Provider 不见验证工具,code / preview 节点继续真实 smoke | | approvedGddRef | 当前仓库无匹配实现 | M2 从 command 到 task/run/completion/context 全链新增 | | game-chat retry 边界 | `apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs:7-39,120-134`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/lifecycle_control.rs:740-772,885-905` | M0-4 / 工作包 M0B-1 统一 delegate/retry assets-only lineage | @@ -1431,11 +1438,11 @@ M0 文档 PR 本身最低验证:Markdown 结构与三张 Mermaid 图可解析 - 三张 Mermaid 图分别覆盖运行拓扑、版本/审批状态、提交/恢复时序。 - 合入只表示 M0A-1 非交付阶段设计检查通过,不表示 M0A、M1 入口门或 M0 全部完成,也不表示任何功能已上线。 -M0A-1 是 M1 详细设计与技术 spike 的输入,不是功能交付。M1 可以与 M0-3 并行准备;但 checkpoint handoff 私有持久化决策未冻结前,对应 checkpoint 代码不得合入,M0-3 的 dated 决策、Prompt、source/tool policy 与测试未全部合入前,依赖 Fast GDD source/tool policy 的 M1 代码也不得合入主线。M0-4 不阻塞 M1/M2,仅阻塞 M3-4 和“M0 全部完成”;不得因为并行关系把工作包标签误报为阶段完成。 +M0A-1 是 M1 详细设计与技术 spike 的输入,不是功能交付。M1 可以与 M0-3 并行准备;但 checkpoint handoff 私有持久化决策未冻结前,对应 checkpoint 代码不得合入,M0-3 的 dated 决策、Prompt、source/tool policy 与测试未全部合入前,依赖 Fast GDD source/tool policy 的 M1 代码也不得合入主线。M0-3 采用 Runtime 内部 owner 验证,不为未来 plan source 暴露 command、smoke 或替代验证工具。M0-4 不阻塞 M1/M2,仅阻塞 M3-4 和“M0 全部完成”;不得因为并行关系把工作包标签误报为阶段完成。 -### 23.2 M0-3(PR 工作包 `M0A-2`) +### 23.2 M0-3:统一 owner 产物验证与可玩验收边界(PR 工作包 `M0A-2`) -新增 2026-08-10 dated 决策,明确只取代 2026-07-26 条目中“`design-foundation` 不得调用固定 smoke / 只有 `preview-readiness` 可调用”的排他性表述;随后对齐 `design-foundation` 本人写后固定 smoke、`preview-readiness` 最终静态验收、plan source 零 smoke 的文档、Prompt、policy 与测试。M0-3 本轮尚未开始,不得用第 19 节目标表冒充已完成裁决。 +以 2026-08-11 dated 决策确认四个 pre-code fixed owner 由 Runtime 内部验证 canonical 正式产物,Provider 不获得新工具或 commandId;`code-prototype / preview-readiness` 保留真实 `game.static_smoke`,`preview-playtest` 保留独立浏览器验收,后置 `publish-package` 不在本阶段扩展。实现与回归必须覆盖真实新项目占位入口、canonical owner 路径矩阵、mutation 后凭证失效、恢复和错误 source/run/root/parent 失败关闭,以及 `art-director` 有/无 Editor Key 的条件 Canvas owner 分类。只有文档、Prompt、工具广告、执行 policy、完成门与上述测试全部通过并合入,才可把 M0-3 计入“M1 入口门完成”;M1 plan source 本身仍属于后续功能实现。 ### 23.3 M0-4(PR 工作包 `M0B-1` / `M0B-2`)