修复:封闭 game-chat 美术重试写入边界

在 successor durable 写入前类型化拒绝动态美术 child 通用 retry
将遗留 retry-source 美术 run 收敛为只读并阻断全部 mutation
补齐终态零副作用、工具矩阵及 DAG/非美术 retry 非回归测试
同步技术方案、决策记录与踩坑说明
This commit is contained in:
2026-08-11 10:40:12 +00:00
parent e613b07fb5
commit 4d0c8e40b3
7 changed files with 722 additions and 40 deletions
@@ -772,6 +772,23 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
Ok((run_profile, source))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(in crate::agent) enum AgentRuntimeRetryErrorKind {
GameChatDynamicArtRetryUnsupported,
}
impl AgentRuntimeRetryErrorKind {
pub(in crate::agent) const fn as_str(self) -> &'static str {
match self {
Self::GameChatDynamicArtRetryUnsupported => "game-chat-dynamic-art-retry-unsupported",
}
}
fn wire_error(self, message: &str) -> String {
format!("kind={} {message}", self.as_str())
}
}
pub(crate) fn retry_game_creator_agent_runtime_task_at(
root: &Path,
agent_id: &str,
@@ -787,6 +804,13 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
let task =
read_latest_game_creator_agent_runtime_task_by_run_id(root, &agent_id, &target_run_id)?
.ok_or_else(|| format!("未找到 Agent Runtime 任务:{target_run_id}"))?;
if game_chat_dynamic_art_child_structural_identity_at(root, &task)? {
return Err(
AgentRuntimeRetryErrorKind::GameChatDynamicArtRetryUnsupported.wire_error(
"game-chat 动态美术 child 不支持通用 Agent Runtime retry;请回到当前 code-prototype,先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派",
),
);
}
if task.goal_id.is_some() {
return Err("持久 Goal 任务不能使用普通 retry;请清理后创建新 Goal".to_string());
}
File diff suppressed because it is too large Load Diff
@@ -1,27 +1,161 @@
use super::*;
fn game_chat_delegated_art_agent_is_assets_only_at(
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GameChatDelegatedArtAgentMutationScope {
Unrestricted,
AssetsOnly,
RetryLineageBlocked,
}
pub(in crate::agent) fn game_chat_dynamic_art_child_structural_identity_at(
root: &Path,
task: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
if !matches!(task.agent_id.as_str(), "art-director" | "art-asset-plan")
|| task.parent_agent_id.as_deref() != Some("code-prototype")
{
return Ok(false);
}
let Some(parent_run_id) = task
.parent_run_id
.as_deref()
.filter(|value| !value.is_empty())
else {
return Ok(false);
};
let Some(child_binding) =
read_game_creator_agent_runtime_run_profile_binding(root, &task.agent_id, &task.run_id)?
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 {
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);
};
Ok(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
&& 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.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_none()
&& root_binding.parent_run_id.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())
}
fn game_chat_delegated_art_agent_mutation_scope_at(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<bool, String> {
) -> Result<GameChatDelegatedArtAgentMutationScope, String> {
let canonical_output = match agent_id {
"art-director" => AGENT_RUNTIME_ART_SPEC_PATH,
"art-asset-plan" => AGENT_RUNTIME_ART_SPRITESHEET_PATH,
_ => return Ok(false),
_ => return Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted),
};
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)?
{
return Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked);
}
}
// `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,
agent_id,
run_id,
canonical_output,
Ok(
if game_chat_fast_path_scheduled_art_contract_repair_is_authorized_at(
root,
agent_id,
run_id,
canonical_output,
)? {
GameChatDelegatedArtAgentMutationScope::AssetsOnly
} else {
GameChatDelegatedArtAgentMutationScope::Unrestricted
},
)
}
fn game_chat_dynamic_art_retry_mutation_block(tool: &str) -> AgentRuntimeToolObservation {
AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "blocked".to_string(),
summary: "game-chat 动态美术 child 不支持通用 retry 写入".to_string(),
detail: Some(
"必须回到当前 code-prototype 先完成 asset.list 审计,再按真实缺口建立新的 durable 美术委派"
.to_string(),
),
}
}
fn game_chat_art_child_tool_is_read_only(tool: &str) -> bool {
matches!(
tool,
@@ -48,17 +182,26 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_project_path_mutation_bloc
tool: &str,
path: &str,
) -> Option<AgentRuntimeToolObservation> {
match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id) {
Ok(false) => None,
Ok(true) if path == "assets" || path.starts_with("assets/") => None,
Ok(true) => 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"
)),
}),
match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) {
Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted) => None,
Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked) => {
Some(game_chat_dynamic_art_retry_mutation_block(tool))
}
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly)
if path == "assets" || path.starts_with("assets/") =>
{
None
}
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) => {
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"
)),
})
}
Err(error) => Some(AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "blocked".to_string(),
@@ -100,21 +243,28 @@ pub(in crate::agent) fn game_chat_delegated_art_agent_input_mutation_block(
});
}
}
let assets_only = match game_chat_delegated_art_agent_is_assets_only_at(root, agent_id, run_id)
{
Ok(value) => value,
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)),
});
}
};
if !assets_only || game_chat_art_child_tool_is_read_only(tool) {
let mutation_scope =
match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) {
Ok(value) => value,
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)),
});
}
};
if game_chat_art_child_tool_is_read_only(tool) {
return None;
}
match mutation_scope {
GameChatDelegatedArtAgentMutationScope::Unrestricted => return None,
GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked => {
return Some(game_chat_dynamic_art_retry_mutation_block(tool));
}
GameChatDelegatedArtAgentMutationScope::AssetsOnly => {}
}
let mut block_path = |path: &str| {
game_chat_delegated_art_agent_project_path_mutation_block(
root, agent_id, run_id, tool, path,
@@ -169,9 +319,16 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block(
}
}
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) => {
match game_chat_delegated_art_agent_mutation_scope_at(root, agent_id, run_id) {
Ok(GameChatDelegatedArtAgentMutationScope::RetryLineageBlocked) => {
return Some(game_chat_dynamic_art_retry_mutation_block(tool));
}
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly)
if path == "assets" || path.starts_with("assets/") =>
{
return None;
}
Ok(GameChatDelegatedArtAgentMutationScope::AssetsOnly) => {
return Some(AgentRuntimeToolObservation {
tool: tool.to_string(),
status: "blocked".to_string(),
@@ -181,7 +338,7 @@ pub(in crate::agent) fn agent_role_project_path_mutation_block(
)),
});
}
Ok(false) => {}
Ok(GameChatDelegatedArtAgentMutationScope::Unrestricted) => {}
Err(error) => {
return Some(AgentRuntimeToolObservation {
tool: tool.to_string(),