修复:收紧 game-chat 美术重试结构判定

入口结构身份改用不可变 binding 链,避免 task journal 缺失漏过 typed retry 守卫

无法完整证明的 retry-source 美术候选保持 mutation 失败关闭,同时保留完整 DAG 语义

补齐缺失 journal、无完整 binding、公开错误 kind 与全链非回归测试
This commit is contained in:
2026-08-11 11:22:20 +00:00
parent 4d0c8e40b3
commit 203ebba9b4
2 changed files with 156 additions and 46 deletions
@@ -464,6 +464,27 @@ fn game_chat_dynamic_art_terminal_children_reject_generic_retry_without_durable_
.expect("persist terminal game-chat art child task");
write_game_creator_agent_runtime_state(&root, &child)
.expect("persist terminal game-chat art child state");
fs::remove_file(game_creator_agent_runtime_task_path(
&root,
"code-prototype",
))
.expect("remove parent task journal without removing immutable binding lineage");
fs::remove_file(game_creator_agent_runtime_task_path(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
))
.expect("remove root task journal without removing immutable binding lineage");
let terminal_child = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
&child.agent_id,
&child.run_id,
)
.expect("read terminal game-chat art child")
.expect("terminal game-chat art child exists");
assert!(
game_chat_dynamic_art_child_structural_identity_at(&root, &terminal_child)
.expect("immutable binding lineage still identifies terminal art child")
);
let durable_before = snapshot_agent_durable_files(&root);
let retry_run_id = format!("blocked-game-chat-art-retry-{case_index}");
@@ -476,10 +497,7 @@ fn game_chat_dynamic_art_terminal_children_reject_generic_retry_without_durable_
.expect_err("game-chat dynamic art child retry must be rejected");
assert!(
error.contains(&format!(
"kind={}",
AgentRuntimeRetryErrorKind::GameChatDynamicArtRetryUnsupported.as_str()
)),
error.contains("kind=game-chat-dynamic-art-retry-unsupported"),
"unexpected typed retry error: {error}"
);
assert!(error.contains("code-prototype"), "{error}");
@@ -605,6 +623,58 @@ async fn legacy_game_chat_dynamic_art_retry_runs_are_read_only_and_fail_closed()
.is_some());
assert!(!root.join("assets/retry-must-not-write.txt").exists());
assert!(!root.join("assets/retry-patchset.txt").exists());
fs::remove_file(game_creator_agent_runtime_task_path(
&root,
"code-prototype",
))
.expect("remove parent task journal for forged legacy fixture");
let forged_task = read_latest_game_creator_agent_runtime_task_by_run_id(
&root,
&retry.agent_id,
&retry.run_id,
)
.expect("read forged art retry")
.expect("forged art retry exists");
assert!(
game_chat_dynamic_art_child_structural_identity_at(&root, &forged_task)
.expect("immutable bindings still prove structural identity")
);
assert!(
!game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at(
&root,
&retry.agent_id,
&retry.run_id,
AGENT_RUNTIME_ART_SPRITESHEET_PATH,
)
.expect("missing parent task cannot prove strict authorization"),
"retry without a live parent task must not acquire strict authorization"
);
let forged_action = AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: None,
input: serde_json::json!({
"path":"assets/forged-retry-must-not-write.txt",
"content":"blocked",
}),
};
let forged_observation = execute_game_creator_agent_runtime_tool_action(
&root,
&retry.agent_id,
&retry.run_id,
&retry.current_task,
&forged_action,
)
.await;
assert_eq!(
forged_observation.status, "blocked",
"{forged_observation:?}"
);
assert!(
forged_observation.summary.contains("不支持通用 retry"),
"{forged_observation:?}"
);
assert!(!root.join("assets/forged-retry-must-not-write.txt").exists());
}
#[test]
@@ -708,6 +778,25 @@ fn game_chat_dynamic_art_retry_guard_preserves_dag_art_and_non_art_delegated_ret
&serde_json::json!({"path":AGENT_RUNTIME_ART_SPEC_PATH,"content":"unchanged semantics"}),
)
.is_none());
fs::remove_file(game_creator_agent_runtime_run_profile_binding_path(
&dag_root,
"art-director",
"full-dag-art-retry-run",
))
.expect("remove full DAG retry binding for unproven-lineage fixture");
let unproven_block = game_chat_delegated_art_agent_project_path_mutation_block(
&dag_root,
"art-director",
"full-dag-art-retry-run",
"file.write",
"assets/unproven-retry-must-not-write.txt",
)
.expect("unproven retry-source art lineage must fail closed");
assert!(
unproven_block.summary.contains("不支持通用 retry"),
"{unproven_block:?}"
);
drop(dag_art_lane);
let delegated_temporary = tempfile::tempdir().expect("create non-art delegated retry root");
@@ -12,6 +12,10 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
task: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
if !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan")
|| !matches!(
task.source.as_str(),
"agent-delegate" | "agent-delegate-retry"
)
|| task.parent_agent_id.as_deref() != Some("code-prototype")
{
return Ok(false);
@@ -28,14 +32,6 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
else {
return Ok(false);
};
let Some(parent) = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
"code-prototype",
parent_run_id,
)?
else {
return Ok(false);
};
let Some(parent_binding) =
read_game_creator_agent_runtime_run_profile_binding(root, "code-prototype", parent_run_id)?
else {
@@ -49,14 +45,6 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
else {
return Ok(false);
};
let Some(root_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&root_binding.agent_id,
&root_binding.run_id,
)?
else {
return Ok(false);
};
Ok(child_binding.agent_id == task.agent_id
&& child_binding.run_id == task.run_id
@@ -67,20 +55,13 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
&& child_binding.parent_run_id == task.parent_run_id
&& child_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& child_binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& parent.agent_id == "code-prototype"
&& parent.run_id == parent_run_id
&& parent.source == "agent-ready-task-scheduler"
&& parent.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& parent.parent_agent_id.as_deref() == Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
&& parent.parent_run_id.as_deref() == Some(child_binding.root_run_id.as_str())
&& parent.delegation_id.is_none()
&& parent_binding.agent_id == parent.agent_id
&& parent_binding.run_id == parent.run_id
&& parent_binding.source == parent.source
&& parent_binding.profile == parent.run_profile
&& parent_binding.binding_fingerprint == parent.run_profile_binding_fingerprint
&& parent_binding.parent_agent_id == parent.parent_agent_id
&& parent_binding.parent_run_id == parent.parent_run_id
&& parent_binding.agent_id == "code-prototype"
&& parent_binding.run_id == parent_run_id
&& parent_binding.source == "agent-ready-task-scheduler"
&& parent_binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& parent_binding.parent_agent_id.as_deref()
== Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
&& parent_binding.parent_run_id.as_deref() == Some(child_binding.root_run_id.as_str())
&& parent_binding.project_id == child_binding.project_id
&& parent_binding.root_agent_id == child_binding.root_agent_id
&& parent_binding.root_run_id == child_binding.root_run_id
@@ -95,16 +76,58 @@ pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
&& root_binding.root_run_id == root_binding.run_id
&& root_binding.parent_agent_id.is_none()
&& root_binding.parent_run_id.is_none()
&& root_binding.parent_binding_fingerprint.is_none()
&& parent_binding.parent_binding_fingerprint.as_deref()
== Some(root_binding.binding_fingerprint.as_str())
&& root_task.agent_id == root_binding.agent_id
&& root_task.run_id == root_binding.run_id
&& root_task.source == root_binding.source
&& root_task.run_profile == root_binding.profile
&& root_task.run_profile_binding_fingerprint == root_binding.binding_fingerprint
&& root_task.parent_agent_id.is_none()
&& root_task.parent_run_id.is_none()
&& root_task.delegation_id.is_none())
== Some(root_binding.binding_fingerprint.as_str()))
}
fn game_chat_dynamic_art_retry_claims_game_chat_lineage_at(
root: &Path,
task: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
if task.source != "agent-delegate-retry"
|| !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan")
{
return Ok(false);
}
if task.parent_agent_id.as_deref() == Some("code-prototype") {
return Ok(true);
}
let child_binding = match read_game_creator_agent_runtime_run_profile_binding(
root,
&task.agent_id,
&task.run_id,
) {
Ok(Some(binding)) => binding,
// A retry-source art task without a complete immutable binding chain
// cannot be downgraded to ordinary unrestricted art permissions.
Ok(None) | Err(_) => return Ok(true),
};
if child_binding.agent_id != task.agent_id
|| child_binding.run_id != task.run_id
|| child_binding.source != task.source
|| child_binding.profile != task.run_profile
|| child_binding.binding_fingerprint != task.run_profile_binding_fingerprint
|| child_binding.parent_agent_id != task.parent_agent_id
|| child_binding.parent_run_id != task.parent_run_id
{
return Ok(true);
}
if child_binding.parent_agent_id.as_deref() == Some("code-prototype") {
return Ok(true);
}
let child_root_binding = match read_game_creator_agent_runtime_run_profile_binding(
root,
&child_binding.root_agent_id,
&child_binding.root_run_id,
) {
Ok(Some(binding)) => binding,
Ok(None) | Err(_) => return Ok(true),
};
Ok(
child_root_binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& child_root_binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
)
}
fn game_chat_delegated_art_agent_mutation_scope_at(
@@ -120,9 +143,7 @@ fn game_chat_delegated_art_agent_mutation_scope_at(
if let Some(task) =
read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, run_id)?
{
if task.source == "agent-delegate-retry"
&& game_chat_dynamic_art_child_structural_identity_at(root, &task)?
{
if game_chat_dynamic_art_retry_claims_game_chat_lineage_at(root, &task)? {
return Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked);
}
}