修复:收紧 game-chat 美术重试结构判定
入口结构身份改用不可变 binding 链,避免 task journal 缺失漏过 typed retry 守卫 无法完整证明的 retry-source 美术候选保持 mutation 失败关闭,同时保留完整 DAG 语义 补齐缺失 journal、无完整 binding、公开错误 kind 与全链非回归测试
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user