统一M0固定产物验证与可玩验收边界
新增四个固定 owner 的 Runtime 内部产物验证、身份校验与恢复约束 收紧 code-prototype 静态检查、试玩执行者与动态 Canvas 委派谱系 补齐项目锁竞争、完成门、路径策略和失败关闭回归测试 同步 M0A-2 决策、技术方案与项目记忆
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+180
-10
@@ -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<str>) -> 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 {
|
||||
|
||||
@@ -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<Option<AgentRuntimeToolObservation>, 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(
|
||||
|
||||
+346
-13
@@ -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<LlmFunctionTool>,
|
||||
) -> 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<LlmFunctionTool>,
|
||||
) -> 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::<Result<BTreeSet<_>, _>>()?;
|
||||
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:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本;冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点,并且 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:<name>、test:<name>(例如 test:unit)、lint:<name>、typecheck:<name>、build:<name>、verify:<name>、validate:<name> 形式的命名脚本;冒号后的每个非空段必须以字母或数字开头且只能包含字母、数字、连字符、下划线或点,并且 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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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::<Vec<_>>()
|
||||
.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");
|
||||
|
||||
+52
-11
@@ -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"
|
||||
|
||||
+55
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
+237
-4
@@ -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<bool, String> {
|
||||
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(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+434
-4
File diff suppressed because it is too large
Load Diff
+1071
-40
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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<bool, String> {
|
||||
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<AgentRuntimeToolObservation> {
|
||||
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(
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
)));
|
||||
}
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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
|
||||
|
||||
+303
-65
@@ -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::<String>()
|
||||
)
|
||||
}
|
||||
|
||||
#[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::<BTreeSet<_>>()
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user