立项策划:收口 M1A 复核残余,retry 强判据前置且身份哨兵改为编译期约束

retry 强判据必须排在全部分支之前。plan 分支原先排在 delegated 与
autonomous-game-build 之后,两支都能绕开
reject_supervisor_plan_root_retry_without_identity:

一是 plan source 配伪造 parent 会落 delegated 支,直接返回
agent-delegate-retry,强判据根本不执行。二是 binding.source 是 plan 配
autonomous profile 会落 autonomous 支,因 plan 在可信集合内而被原样取回,
复活启动路径 reject_supervisor_plan_autonomous_profile 明令禁止的组合。

两者都要 durable 状态先畸变才可达,但强判据存在的意义正是对畸变状态
fail closed。守卫提到函数开头无条件执行,合法 plan 根 run 对它恒真;
plan 分支不再重复读 durable 状态。autonomous 支另加一次
reject_supervisor_plan_autonomous_profile,兜住顶部守卫按 task.source
判定所挡不住的那一种。回归已用变异测试确认去掉任一守卫即变红。

__all_agents__ 身份哨兵改为编译期约束。四个不带 agentId 的 wrapper 会以
哨兵跳过按身份的工具面收窄与原始工具 identity 复核,M1A-2 之后调用点只剩
测试,但将来新增生产调用点漏改是静默拿全量目录而非编译失败。四个 wrapper
与对应 re-export 一并加 cfg(test)。该哨兵已扩散到两个文件,是正在复制的
模式而非单点遗留。

订正 M1A-3 决策条里「agent-background-task 唯一构造点」的错误结论:
task_start 与 recovery_scan 各还有一处同形状的空 source 兜底,且
recovery_scan 那条不经过 plan 根强判据。经复核有意不改,理由随条记录。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 05:20:18 +00:00
parent c22b929080
commit 5fa662be30
6 changed files with 137 additions and 2 deletions
@@ -135,10 +135,15 @@ pub(crate) use structured_plan::{
complete_agent_runtime_remaining_plan_steps, retry_agent_runtime_active_plan_step,
sanitize_agent_runtime_plan_update,
};
// 不带 agentId 的三个解析入口走 `"__all_agents__"` 哨兵、跳过按身份的工具面
// 复核,只对测试开放;生产代码必须用 `_for_agent`。
#[cfg(test)]
pub(crate) use tool_plan_protocol::{
parse_game_creator_agent_tool_plan_llm_response,
parse_game_creator_agent_tool_plan_llm_response_with_catalog,
parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified,
};
pub(crate) use tool_plan_protocol::{
parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified_for_agent,
parse_game_creator_agent_tool_plan_response,
};
@@ -20,6 +20,8 @@ pub(in crate::agent) fn parse_game_creator_agent_tool_plan_response_classified(
parse_game_creator_agent_tool_plan_payload(payload, false)
}
/// 只允许测试使用(沿用下方 `_classified` 的哨兵约束)。
#[cfg(test)]
pub(crate) fn parse_game_creator_agent_tool_plan_llm_response(
response: &platform_llm::LlmRunResponse,
) -> Result<ParsedAgentRuntimeToolPlan, String> {
@@ -33,6 +35,8 @@ pub(crate) fn parse_game_creator_agent_tool_plan_llm_response(
)
}
/// 只允许测试使用(沿用下方 `_classified` 的哨兵约束)。
#[cfg(test)]
pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_with_catalog(
response: &platform_llm::LlmRunResponse,
mcp_catalog: &GameCreatorMcpCatalog,
@@ -41,6 +45,12 @@ pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_with_catalog(
.map_err(|error| error.to_string())
}
/// 不带身份的解析入口,**只允许测试使用**。
///
/// `"__all_agents__"` 哨兵会跳过按身份的工具面复核;生产代码必须走
/// `_for_agent` 并传真实 `agentId`。`#[cfg(test)]` 让漏改在编译期就失败,
/// 而不是在运行时静默放行本该被收窄的调用。
#[cfg(test)]
pub(crate) fn parse_game_creator_agent_tool_plan_llm_response_with_catalog_classified(
response: &platform_llm::LlmRunResponse,
mcp_catalog: &GameCreatorMcpCatalog,
@@ -749,6 +749,12 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
Some(&task.run_profile),
Some(&task.run_profile_binding_fingerprint),
)?;
// 声称是 plan 的 task 必须先过强判据,且这道守卫要排在全部分支之前。
// 否则「plan source + 伪造 parent」会落进 delegated 支、「plan source +
// autonomous profile」会落进 autonomous 支,两条都绕开强判据——而强判据
// 存在的意义正是对畸变 durable 状态 fail closed。合法 plan 根 run 无
// parent、profile 为 standard,本守卫对它是恒真的。
reject_supervisor_plan_root_retry_without_identity(root, task)?;
let source = if delegated {
"agent-delegate-retry".to_string()
} else if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
@@ -765,9 +771,13 @@ pub(crate) fn resolve_game_creator_agent_runtime_retry_configuration_at(
{
return Err("自主构建 Agent Runtime 重试绑定不是可信 Supervisor 根 Run".to_string());
}
// 上面的守卫按 task.source 判定,挡不住「task.source 已损坏但
// binding.source 是 plan」这一种:plan 在可信集合内,会被原样取回,
// 复活启动路径明令禁止的 plan + autonomous 组合。
reject_supervisor_plan_autonomous_profile(&binding.source, &run_profile)?;
binding.source
} else if agent_runtime_supervisor_source_is_plan(&task.source) {
reject_supervisor_plan_root_retry_without_identity(root, task)?;
// 强判据已由函数开头的守卫执行过,这里不重复读 durable 状态。
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE.to_string()
} else {
"agent-background-task".to_string()
@@ -9464,3 +9464,93 @@ fn autonomous_playtest_liveness_only_enforces_the_latest_preview_result() {
error.starts_with(AGENT_RUNTIME_AUTONOMOUS_DELEGATED_PLAYTEST_REPAIR_LIVENESS_ERROR_PREFIX)
);
}
/// `M1A-4` 收口:强判据必须排在 `delegated` 与 `autonomous-game-build` 两支之前。
///
/// 这两条路径原先都能绕开 `reject_supervisor_plan_root_retry_without_identity`:
/// 「plan source + 伪造 parent」落 delegated 支直接返回 `agent-delegate-retry`;
/// 「binding.source 是 plan + autonomous profile」落 autonomous 支,因为 plan 在
/// 可信集合内而被原样取回,复活启动路径明令禁止的组合。两者都要 durable 状态先
/// 畸变才可达,但强判据存在的意义正是对畸变状态 fail closed。
#[test]
fn plan_root_retry_identity_guard_precedes_delegated_and_autonomous_branches() {
let temporary = crate::tests::canonical_test_tempdir("plan-root-retry-guard-");
let root = temporary.path().join("project");
init_local_game_project_at(&root, "plan-root-retry-guard", "守卫前置").expect("init");
let session_id = resolve_agent_conversation_session_id_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
None,
true,
)
.expect("session");
// ① delegated=true:伪造 parent 的 plan source task 不得静默变成
// agent-delegate-retry,必须先被强判据拒绝。
let binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"plan-retry-guard-delegated",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
None,
)
.expect("bind plan root");
let mut forged_parent = failed_supervisor_task(
"plan-retry-guard-delegated",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
&binding.binding_fingerprint,
&session_id,
);
forged_parent.parent_agent_id = Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string());
forged_parent.parent_run_id = Some("forged-parent-run".to_string());
let delegated_error =
resolve_game_creator_agent_runtime_retry_configuration_at(&root, &forged_parent, true)
.expect_err("delegated 支不得绕开 plan 根强判据");
assert!(
delegated_error.contains(AGENT_RUNTIME_PLAN_ROOT_RETRY_IDENTITY_UNSUPPORTED_KIND),
"{delegated_error}"
);
// ② autonomous 支:task.source 已损坏成非 plan,但 binding.source 是 plan。
// 顶部守卫按 task.source 判定,挡不住这一种,必须由 autonomous 支内的
// reject_supervisor_plan_autonomous_profile 兜住。
let autonomous_binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
"plan-retry-guard-autonomous",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind plan+autonomous");
let corrupted_source = failed_supervisor_task(
"plan-retry-guard-autonomous",
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD,
&autonomous_binding.binding_fingerprint,
&session_id,
);
let autonomous_error =
resolve_game_creator_agent_runtime_retry_configuration_at(&root, &corrupted_source, false)
.expect_err("autonomous 支不得取回 plan source 复活被禁组合");
assert!(
autonomous_error.contains(AGENT_RUNTIME_PLAN_AUTONOMOUS_PROFILE_UNSUPPORTED_KIND),
"{autonomous_error}"
);
// 对照:合法 plan 根 run 不受本守卫影响,仍然保源。
let healthy = failed_supervisor_task(
"plan-retry-guard-delegated",
AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE,
AGENT_RUNTIME_RUN_PROFILE_STANDARD,
&binding.binding_fingerprint,
&session_id,
);
let (profile, source) =
resolve_game_creator_agent_runtime_retry_configuration_at(&root, &healthy, false)
.expect("合法 plan 根 run 必须仍然保源");
assert_eq!(profile, AGENT_RUNTIME_RUN_PROFILE_STANDARD);
assert_eq!(source, AGENT_RUNTIME_SUPERVISOR_PLAN_SOURCE);
}
@@ -277,6 +277,14 @@ pub(crate) fn native_mcp_function_name(server_id: &str, tool_name: &str) -> Stri
)
}
/// 不带身份的全量目录,**只允许测试使用**。
///
/// `"__all_agents__"` 是个不对应任何真实 Agent 的哨兵:走这条路径拿到的是
/// 未按身份收窄的完整函数目录。生产代码必须调用 `_for_agent` 版本并传入真实
/// `agentId`,否则按身份收窄的工具面(如 `project-planning` 的 exact
/// allowlist)会被静默绕开。这里用 `#[cfg(test)]` 把「忘记改用 `_for_agent`」
/// 从运行时静默扩权变成编译期错误。
#[cfg(test)]
pub(crate) fn build_agent_runtime_native_function_tools(
mcp_catalog: &GameCreatorMcpCatalog,
) -> Result<Vec<LlmFunctionTool>, String> {
@@ -385,6 +393,10 @@ fn validate_native_tool_identity(
Ok(())
}
/// 不带身份的解析入口,**只允许测试使用**(理由同
/// `build_agent_runtime_native_function_tools`:哨兵会跳过按身份的原始工具
/// identity 复核)。
#[cfg(test)]
pub(crate) fn parse_agent_runtime_native_tool_calls(
calls: &[LlmToolCall],
mcp_catalog: &GameCreatorMcpCatalog,