修复自主构建等待确认导致的循环中断

将自主模式剩余确认策略统一失败关闭并保持标准模式语义
迁移旧自主确认批次并在同一 Session 和 run 重新规划
补齐零执行审计、恢复回归和真实可玩验收记录
This commit is contained in:
AIGameCreator App
2026-07-22 20:57:37 +08:00
parent 64ca2f99b2
commit 7b145b3067
14 changed files with 599 additions and 30 deletions
@@ -25,17 +25,21 @@ pub(in crate::agent) fn agent_runtime_tool_policy_block_observation(
tool: &str,
blocked: AgentRuntimeToolPolicyBlock,
) -> AgentRuntimeToolObservation {
let (status, summary) = match blocked {
AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary),
let (status, summary, detail) = match blocked {
AgentRuntimeToolPolicyBlock::Denied(summary) => (
"blocked",
summary,
Some("runtimePolicyDecision=denied · zeroExecution=true".to_string()),
),
AgentRuntimeToolPolicyBlock::RequiresConfirmation(summary) => {
("waiting-for-confirmation", summary)
("waiting-for-confirmation", summary, None)
}
};
AgentRuntimeToolObservation {
tool: tool.to_string(),
status: status.to_string(),
summary,
detail: None,
detail,
}
}
@@ -101,9 +101,15 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
game_creator_mcp_action_policy_block_at(root, agent_id, action, confirmation_approved)
.await
};
if let Some(blocked) =
strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block)
{
let policy_block = fail_closed_agent_runtime_confirmation_for_run(
root,
agent_id,
run_id,
pending_action.map(|pending| pending.run_profile.as_str()),
pending_action.map(|pending| pending.run_profile_binding_fingerprint.as_str()),
strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block),
);
if let Some(blocked) = policy_block {
return agent_runtime_tool_policy_block_observation(tool, blocked);
}
} else {
@@ -418,7 +418,15 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch(
} else {
game_creator_mcp_action_policy_block_at(root, &runtime.agent_id, action, false).await
};
match strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block) {
let policy_block = fail_closed_agent_runtime_confirmation_for_run(
root,
&runtime.agent_id,
&runtime.run_id,
Some(&runtime.run_profile),
Some(&runtime.run_profile_binding_fingerprint),
strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block),
);
match policy_block {
Some(blocked @ AgentRuntimeToolPolicyBlock::Denied(_)) => {
let observation =
agent_runtime_tool_policy_block_observation(action.tool.trim(), blocked);
@@ -150,5 +150,14 @@ pub(crate) fn agent_runtime_tool_policy_snapshot_for_run_at(
snapshot.auto_tools.push(tool.to_string());
}
}
for tool in std::mem::take(&mut snapshot.confirm_tools) {
if !snapshot
.denied_tools
.iter()
.any(|candidate| candidate == &tool)
{
snapshot.denied_tools.push(tool);
}
}
Ok(snapshot)
}
@@ -1803,8 +1803,14 @@ pub(in crate::agent) async fn run_game_creator_agent_background_task_pass_with_c
)
.await
};
let policy_block =
strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block);
let policy_block = fail_closed_agent_runtime_confirmation_for_run(
&root,
&agent_id,
&runtime.run_id,
Some(&runtime.run_profile),
Some(&runtime.run_profile_binding_fingerprint),
strictest_agent_runtime_tool_policy_block(local_policy_block, mcp_policy_block),
);
let mut durable_action = None;
let observation = if let Some(blocked) = policy_block {
agent_runtime_tool_policy_block_observation(action.tool.trim(), blocked)
@@ -430,8 +430,16 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
);
runtime.status = "running".to_string();
runtime.phase = "observation".to_string();
let runtime_policy_rejected = auto_execution
&& pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED
&& observation
.detail
.as_deref()
.is_some_and(|detail| detail.contains("runtimePolicyDecision=denied"));
runtime.current_action = if observation.is_repository_context_drift() {
format!("已作废启动上下文漂移前的旧工具 {}", observation.tool)
} else if runtime_policy_rejected {
format!("Runtime 策略已拒绝工具 {}", observation.tool)
} else if auto_execution {
format!("已执行自动工具 {}", observation.tool)
} else if approved {
@@ -505,7 +513,15 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
"summary": observation.summary,
"actionId": pending.action_id,
"actionFingerprint": pending.action_fingerprint,
"decision": if auto_execution { "auto" } else if approved { "approved" } else { "rejected" },
"decision": if runtime_policy_rejected {
"runtime-policy-rejected"
} else if auto_execution {
"auto"
} else if approved {
"approved"
} else {
"rejected"
},
}),
);
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
@@ -522,12 +538,16 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
));
}
}
let continued_event_type = if auto_execution {
let continued_event_type = if runtime_policy_rejected {
"tool_action.rejected_by_policy"
} else if auto_execution {
"tool_action.continued"
} else {
"tool_confirmation.continued"
};
let continued_summary = if auto_execution {
let continued_summary = if runtime_policy_rejected {
"Runtime 策略拒绝观察已持久化,Agent 将在同一 run 重新规划。"
} else if auto_execution {
"自动工具动作观察已持久化,Agent 将在同一 run 继续规划。"
} else {
"待确认动作观察已持久化,Agent 将在同一 run 继续规划。"
@@ -563,7 +583,11 @@ pub(crate) async fn continue_game_creator_agent_pending_tool_action(
let batch_observation = AgentRuntimeToolObservation {
tool: "runtime.provider_action_batch".to_string(),
status: "blocked".to_string(),
summary: "开发者拒绝批次确认后,其余 Provider action 保持零执行".to_string(),
summary: if pending.is_auto() {
"Runtime 策略拒绝批次动作后,其余 Provider action 保持零执行".to_string()
} else {
"开发者拒绝批次确认后,其余 Provider action 保持零执行".to_string()
},
detail: Some(format!(
"batchId={} · actionCount={} · rejectedActionIndex={}",
batch.batch_id,
@@ -296,6 +296,114 @@ pub(in crate::agent) fn suppress_static_delegate_reservation_for_rejected_pendin
suppress_static_delegate_delivery_at(root, &expected).map(|_| ())
}
fn migrate_legacy_autonomous_confirmation_at(
root: &Path,
runtime: &AgentRuntimeState,
pending: &mut AgentRuntimePendingToolAction,
) -> Result<bool, String> {
let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at(
root,
&runtime.agent_id,
&runtime.run_id,
Some(&runtime.run_profile),
Some(&runtime.run_profile_binding_fingerprint),
)?;
if run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| pending.status != AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING
|| pending.execution_mode != AGENT_RUNTIME_ACTION_EXECUTION_MODE_CONFIRMATION
{
return Ok(false);
}
if runtime.run_profile != run_profile
|| runtime.run_profile_binding_fingerprint != binding_fingerprint
|| pending.agent_id != runtime.agent_id
|| pending.session_id != runtime.session_id
|| pending.run_id != runtime.run_id
|| pending.run_profile != run_profile
|| pending.run_profile_binding_fingerprint != binding_fingerprint
{
return Err("自主构建旧确认动作的 Runtime/Run Profile 身份不一致".to_string());
}
let observation = AgentRuntimeToolObservation {
tool: pending.action.tool.trim().to_string(),
status: "blocked".to_string(),
summary: "自主构建模式不能等待人工确认;旧确认动作未执行".to_string(),
detail: Some(
"runtimePolicyDecision=denied · legacyAutonomousConfirmation=true · zeroExecution=true · 请改用 auto-safe 工具或省略该动作"
.to_string(),
),
};
pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_AUTO.to_string();
pending.status = AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED.to_string();
pending.observation = Some(observation);
pending.updated_at = unix_timestamp();
if game_creator_agent_runtime_provider_action_batch_exists(
root,
&pending.agent_id,
&pending.run_id,
) {
let mut batch = read_game_creator_agent_runtime_provider_action_batch(
root,
&pending.agent_id,
&pending.run_id,
)?;
let action_index = usize::try_from(pending.action_index).unwrap_or(usize::MAX);
let stored = batch
.actions
.get(action_index)
.ok_or_else(|| "自主构建旧确认批次缺少对应 actionIndex".to_string())?
.clone();
if stored.action_id != pending.action_id
|| stored.action_fingerprint != pending.action_fingerprint
|| stored.action != pending.action
{
return Err("自主构建旧确认批次与 pending action 身份不一致".to_string());
}
match batch.status.as_str() {
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_WAITING_CONFIRMATION => {
batch.actions[action_index] = pending.clone();
batch.status = AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_ABORTED.to_string();
batch.updated_at = unix_timestamp();
write_game_creator_agent_runtime_provider_action_batch(root, &batch)?;
}
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_ABORTED
if stored.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED
&& stored.observation == pending.observation =>
{
*pending = stored;
}
status => {
return Err(format!(
"自主构建旧确认 pending 所属批次状态不能迁移:{status}"
));
}
}
}
write_game_creator_agent_runtime_pending_tool_action(root, pending)?;
append_game_creator_agent_runtime_action_event(
root,
runtime,
"tool_confirmation.autonomous_legacy_rejected",
"running",
"observation",
"自主构建旧确认动作已在零执行状态下拒绝,原 run 将自动重新规划。",
Some(&format!(
"tool={} · providerBatchAborted={}",
pending.action.tool,
game_creator_agent_runtime_provider_action_batch_exists(
root,
&pending.agent_id,
&pending.run_id,
)
)),
&pending.action_id,
)?;
Ok(true)
}
pub(in crate::agent) fn resume_game_creator_agent_parallel_read_batch_at(
root: &Path,
agent_id: &str,
@@ -574,6 +682,9 @@ pub(crate) fn resume_game_creator_agent_pending_tool_action_at(
return read_game_creator_agent_runtime_at(root, agent_id)
.map(AgentRuntimePendingActionResume::Handled);
}
if migrate_legacy_autonomous_confirmation_at(root, &runtime, &mut pending)? {
can_repair_terminal_receipt = true;
}
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT {
if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
let _ = cancel_game_creator_agent_user_input_request_for_pending_at(root, &pending);
@@ -876,6 +987,18 @@ pub(in crate::agent) fn resume_game_creator_agent_provider_action_batch_at(
remove_game_creator_agent_runtime_provider_action_batch(root, agent_id, &runtime.run_id)?;
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
}
if batch.status == AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_WAITING_CONFIRMATION
&& batch.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
{
let mut pending = batch
.actions
.iter()
.find(|pending| pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING)
.ok_or_else(|| "自主构建旧确认批次缺少 pending-confirmation 成员".to_string())?
.clone();
migrate_legacy_autonomous_confirmation_at(root, &runtime, &mut pending)?;
return resume_game_creator_agent_pending_tool_action_at(root, agent_id, runtime_lock);
}
match batch.status.as_str() {
AGENT_RUNTIME_PROVIDER_ACTION_BATCH_STATUS_WAITING_CONFIRMATION => {
let pending = batch
@@ -58,6 +58,7 @@ pub(crate) use media::{
AGENT_RUNTIME_UI_PROTOTYPE_VALIDATION_PROFILE,
};
pub(crate) use policy::{
fail_closed_agent_runtime_confirmation_for_run,
game_creator_agent_runtime_tool_policy_block_after_lock,
game_creator_agent_runtime_tool_policy_rule_for_run,
};
@@ -38,15 +38,50 @@ pub(crate) fn game_creator_agent_runtime_tool_policy_rule_for_run(
};
match blocked {
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS.contains(&command_id) =>
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD =>
{
None
if AGENT_RUNTIME_AUTONOMOUS_GAME_BUILD_AUTO_COMMAND_IDS.contains(&command_id) {
None
} else {
Some(AgentRuntimeToolPolicyBlock::Denied(format!(
"自主构建模式不能等待人工确认:{command_id};请改用 auto-safe 工具或省略该动作"
)))
}
}
blocked => blocked,
}
}
pub(crate) fn fail_closed_agent_runtime_confirmation_for_run(
root: &Path,
agent_id: &str,
run_id: &str,
stored_profile: Option<&str>,
stored_binding_fingerprint: Option<&str>,
blocked: Option<AgentRuntimeToolPolicyBlock>,
) -> Option<AgentRuntimeToolPolicyBlock> {
let Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(reason)) = blocked else {
return blocked;
};
let (run_profile, _) = match agent_runtime_run_profile_identity_at(
root,
agent_id,
run_id,
stored_profile,
stored_binding_fingerprint,
) {
Ok(identity) => identity,
Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)),
};
if run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
Some(AgentRuntimeToolPolicyBlock::Denied(format!(
"自主构建模式不能等待人工确认:{reason};请改用 auto-safe 工具或省略该动作"
)))
} else {
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(reason))
}
}
pub(in crate::agent) fn agent_runtime_effective_tool_policy_at(
root: &Path,
agent_id: &str,
@@ -56,6 +56,10 @@ fn autonomous_game_build_profile_auto_grants_only_scoped_build_actions() {
.confirm_tools
.iter()
.any(|candidate| candidate == tool));
assert!(!policy
.denied_tools
.iter()
.any(|candidate| candidate == tool));
}
assert!(policy
.denied_tools
@@ -69,29 +73,53 @@ fn autonomous_game_build_profile_auto_grants_only_scoped_build_actions() {
"command.terminate",
] {
assert!(policy
.denied_tools
.iter()
.any(|candidate| candidate == tool));
assert!(!policy
.confirm_tools
.iter()
.any(|candidate| candidate == tool));
}
assert!(game_creator_agent_runtime_tool_policy_rule_for_run(
assert!(policy.confirm_tools.is_empty());
let auto_rule = game_creator_agent_runtime_tool_policy_rule_for_run(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
"file.write",
)
.is_none());
);
assert!(auto_rule.is_none());
let denied = game_creator_agent_runtime_tool_policy_rule_for_run(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
"project.git_commit",
);
let Some(AgentRuntimeToolPolicyBlock::Denied(reason)) = denied else {
panic!("autonomous non-auto-safe tool must be denied");
};
assert!(reason.contains("自主构建模式不能等待人工确认"));
assert!(reason.contains("auto-safe"));
assert!(reason.contains("省略"));
let dynamic_confirmation = crate::fail_closed_agent_runtime_confirmation_for_run(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(
"MCP 工具配置要求用户确认:demo/write".to_string(),
)),
);
assert!(matches!(
game_creator_agent_runtime_tool_policy_rule_for_run(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
"project.git_commit",
),
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
dynamic_confirmation,
Some(AgentRuntimeToolPolicyBlock::Denied(reason))
if reason.contains("MCP 工具配置要求用户确认")
&& reason.contains("不能等待人工确认")
));
write_project_permission_policy_at(
@@ -103,6 +131,77 @@ fn autonomous_game_build_profile_auto_grants_only_scoped_build_actions() {
},
)
.expect("deny file write explicitly");
let denied = game_creator_agent_runtime_tool_policy_rule_for_run(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
"file.write",
);
let Some(AgentRuntimeToolPolicyBlock::Denied(reason)) = denied else {
panic!("explicit deny must remain denied");
};
assert!(reason.contains("项目权限策略拒绝执行"));
let policy = agent_runtime_tool_policy_snapshot_for_run_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
)
.expect("autonomous policy snapshot after explicit deny");
assert!(policy
.denied_tools
.iter()
.any(|candidate| candidate == "file.write"));
assert!(!policy
.auto_tools
.iter()
.any(|candidate| candidate == "file.write"));
fs::remove_dir_all(root).ok();
}
#[test]
fn standard_profile_keeps_confirmation_policy_unchanged() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-standard-profile", "标准确认权限项目")
.expect("project init");
let run_id = "standard-profile-root-run";
let binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
AGENT_RUNTIME_SUPERVISOR_GUI_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_STANDARD),
None,
)
.expect("bind standard profile");
let policy = agent_runtime_tool_policy_snapshot_for_run_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
)
.expect("standard policy snapshot");
assert_eq!(policy.run_profile, AGENT_RUNTIME_RUN_PROFILE_STANDARD);
assert!(policy
.auto_tools
.iter()
.any(|tool| tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL));
for tool in ["file.write", "project.git_commit", "command.exec"] {
assert!(policy
.confirm_tools
.iter()
.any(|candidate| candidate == tool));
assert!(!policy
.denied_tools
.iter()
.any(|candidate| candidate == tool));
}
assert!(matches!(
game_creator_agent_runtime_tool_policy_rule_for_run(
&root,
@@ -112,7 +211,20 @@ fn autonomous_game_build_profile_auto_grants_only_scoped_build_actions() {
Some(&binding.binding_fingerprint),
"file.write",
),
Some(AgentRuntimeToolPolicyBlock::Denied(_))
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
));
assert!(matches!(
crate::fail_closed_agent_runtime_confirmation_for_run(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
run_id,
Some(&binding.profile),
Some(&binding.binding_fingerprint),
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(
"标准模式仍需确认".to_string(),
)),
),
Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(_))
));
fs::remove_dir_all(root).ok();
@@ -1,5 +1,228 @@
use super::support::*;
#[tokio::test]
async fn autonomous_game_build_recovery_aborts_legacy_confirmation_batch_and_replans() {
let root = unique_project_path();
init_local_game_project_at(
&root,
"project-autonomous-legacy-confirmation",
"自主构建旧确认恢复测试",
)
.expect("project init");
let parent_run_id = "autonomous-legacy-confirmation-parent";
let parent_binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
parent_run_id,
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
Some(AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD),
None,
)
.expect("bind autonomous parent profile");
let child_run_id = "autonomous-legacy-confirmation-child";
let child_link = AgentRuntimeTaskLink {
parent_agent_id: Some(parent_binding.agent_id.clone()),
parent_run_id: Some(parent_binding.run_id.clone()),
delegation_id: Some("autonomous-legacy-confirmation-delegation".to_string()),
};
let child_binding = bind_game_creator_agent_runtime_run_profile_at(
&root,
"code-prototype",
child_run_id,
"agent-delegate",
None,
Some(&child_link),
)
.expect("inherit autonomous child profile");
let mut runtime = start_game_creator_agent_runtime_task_at(
&root,
"code-prototype",
"在无人确认的情况下继续完成可试玩项目",
child_run_id,
"agent-delegate",
"恢复旧确认批次",
vec!["拒绝不可自动执行的动作并重新规划".to_string()],
)
.expect("start autonomous child runtime");
assert_eq!(runtime.run_profile, child_binding.profile);
assert_eq!(
runtime.run_profile,
AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
);
let blocked_write_path = root.join("game/legacy-confirmation-must-not-run.txt");
let plan = AgentRuntimeToolPlan {
thinking_summary: "旧版本先写文件再等待 git commit 确认".to_string(),
plan_update: None,
plan: vec!["写入文件".to_string(), "提交项目".to_string()],
actions: vec![
AgentRuntimeToolAction {
tool: "file.write".to_string(),
reason: Some("验证整批零执行".to_string()),
input: serde_json::json!({
"path": "game/legacy-confirmation-must-not-run.txt",
"content": "该文件不应出现"
}),
},
AgentRuntimeToolAction {
tool: "project.git_commit".to_string(),
reason: Some("模拟旧版需要确认的动作".to_string()),
input: serde_json::json!({ "message": "旧确认批次" }),
},
],
response: String::new(),
};
let preparation = prepare_game_creator_agent_runtime_provider_action_batch(
&root,
&runtime,
&runtime.current_task,
&plan,
&[],
&read_game_creator_agent_runtime_project_revision(&root).expect("project revision"),
&build_repository_startup_context_at(&root)
.expect("repository context")
.fingerprint,
)
.await
.expect("prepare denied autonomous batch");
let AgentRuntimeProviderActionBatchPreparation::Aborted { mut batch, .. } = preparation else {
panic!("confirmation-only autonomous tool must abort the batch");
};
let rejected_index = batch
.actions
.iter()
.position(|pending| pending.action.tool == "project.git_commit")
.expect("git commit batch member");
let mut legacy_pending = batch.actions[rejected_index].clone();
legacy_pending.execution_mode = AGENT_RUNTIME_ACTION_EXECUTION_MODE_CONFIRMATION.to_string();
legacy_pending.status = crate::AGENT_RUNTIME_PENDING_ACTION_STATUS_PENDING.to_string();
legacy_pending.observation = None;
legacy_pending.updated_at = unix_timestamp();
batch.actions[rejected_index] = legacy_pending.clone();
batch.status = "waiting-confirmation".to_string();
batch.updated_at = unix_timestamp();
crate::write_game_creator_agent_runtime_provider_action_batch(&root, &batch)
.expect("persist legacy waiting batch");
write_game_creator_agent_runtime_pending_tool_action(&root, &legacy_pending)
.expect("persist legacy waiting pending");
runtime.status = "waiting-for-confirmation".to_string();
runtime.phase = "waiting-for-confirmation".to_string();
runtime.current_action = "等待旧版人工确认".to_string();
runtime.waiting_on = "开发者确认".to_string();
runtime.next_step = "确认后继续".to_string();
runtime.pending_tool_action = Some(legacy_pending.summary());
runtime.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(&root, &runtime).expect("append legacy waiting task");
write_game_creator_agent_runtime_state(&root, &runtime).expect("write legacy waiting state");
let (request_sender, request_receiver) = mpsc::channel();
let (release_sender, release_receiver) = mpsc::channel();
let base_url = spawn_releasable_mock_llm_server_responses_with_capture(
vec![serde_json::json!({
"thinkingSummary": "旧确认已拒绝,改用自动安全动作继续",
"planUpdate": {
"explanation": "旧确认动作不再阻塞自主构建",
"steps": [{ "step": "重新规划安全动作", "status": "completed" }]
},
"plan": [],
"actions": [],
"response": "已跳过需要人工确认的动作并继续。"
})
.to_string()],
request_sender,
release_receiver,
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"code-prototype": {{
"apiKey": "autonomous-recovery-key",
"baseUrl": {base_url:?},
"model": "autonomous-recovery-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
let resumed = resume_game_creator_agent_background_tasks_at(&root)
.expect("scan and resume legacy autonomous batch");
assert!(resumed.iter().any(|result| {
result.state.agent_id == "code-prototype" && result.state.run_id == child_run_id
}));
let request = request_receiver
.recv_timeout(Duration::from_secs(4))
.expect("same run must request a replacement plan");
assert!(request.contains("在无人确认的情况下继续完成可试玩项目"));
assert!(
!blocked_write_path.exists(),
"auto prefix must remain unexecuted"
);
assert!(
!crate::game_creator_agent_runtime_provider_action_batch_path(
&root,
"code-prototype",
child_run_id,
)
.exists()
);
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
&root,
"code-prototype",
child_run_id,
));
let migrated = read_game_creator_agent_runtime_at(&root, "code-prototype")
.expect("read replanning runtime")
.state;
assert_eq!(migrated.session_id, runtime.session_id);
assert_eq!(migrated.run_id, runtime.run_id);
assert_eq!(migrated.run_profile, runtime.run_profile);
assert_eq!(
migrated.run_profile_binding_fingerprint,
runtime.run_profile_binding_fingerprint
);
assert_ne!(migrated.phase, "waiting-for-confirmation");
let events = fs::read_to_string(game_creator_agent_runtime_event_path(
&root,
"code-prototype",
))
.expect("read migration events");
assert!(events.contains("tool_confirmation.autonomous_legacy_rejected"));
assert!(events.contains("provider_action_batch.aborted"));
assert!(events.contains("tool_action.rejected_by_policy"));
let records = read_agent_db_records_for_test(&root);
assert!(records.iter().any(|record| {
record["recordType"] == "agent.runtime.tool_observation"
&& record["runId"] == child_run_id
&& record["decision"] == "runtime-policy-rejected"
}));
let cancelling =
cancel_game_creator_agent_runtime_task_at(&root, "code-prototype", child_run_id)
.expect("cancel held replacement planning request");
assert_eq!(cancelling.state.status, "cancelling");
release_sender
.send(())
.expect("release replacement planning");
let mut cancelled = read_game_creator_agent_runtime_at(&root, "code-prototype")
.expect("read cancelling runtime")
.state;
for _ in 0..100 {
if cancelled.status == "cancelled"
&& game_creator_agent_runtime_task_lock_is_available(&root, "code-prototype")
.expect("inspect recovery test lock")
{
break;
}
std::thread::sleep(Duration::from_millis(20));
cancelled = read_game_creator_agent_runtime_at(&root, "code-prototype")
.expect("read cancelled runtime")
.state;
}
assert_eq!(cancelled.status, "cancelled");
fs::remove_dir_all(root).ok();
}
#[test]
fn agent_runtime_run_status_rechecks_revision_inside_project_lock() {
let root = unique_project_path();
@@ -5268,3 +5268,10 @@
- 决策:pending action 在执行前发现 project revision 漂移时,必须持久化为 `blocked` observation,明确 `projectRevisionDrift=true / replanRequired=true / 旧动作未执行`,清理旧 pending,并让同一 Agent、同一 run 基于最新项目状态继续 planning。只有副作用可能已经发生、持久身份损坏或审计无法证明结果时才进入 `needs-reconciliation`
- 锁内边界:`file.write``file.patch` 在取得项目写锁后再次核对 pending 身份、仓库上下文、revision 和 verification gate,避免预检后与另一 Agent 的项目修改交错。revision 漂移只拒绝旧动作,不忽略并发变化,也不直接执行可能覆盖他人结果的旧写入。
- 验收:三个定向回归、全部 `revision` 过滤测试 `32/32``cargo check --tests` 和 Linux 串行全量 `1147 passed / 5 ignored / 0 failed` 通过。确定性正式 E2E 继续以 `17/17` Provider lifecycle、revision `0 -> 2` 和 Chrome `37/37` 通过。新的独立真实 external-provider E2E 只写入一次塔防需求并立即 EOF,人工 approve / answer / steer 均为 `0`;父 turn `settled`,项目 revision `0 -> 8`static smoke 和真实 Chrome `lane-defense-v1 37/37` 通过,唯一 Supervisor assistant 写入,pending、confirmation、user-input、reconciliation、sidecar、重复与敏感信息泄漏均为 `0`
## 2026-07-22 自主构建禁止进入人工确认等待
- 背景:自主构建虽然禁止 `user.input_request`,但项目权限、Agent 权限或 MCP catalog 仍可能把 `blackboard.write``project.git_commit`、完整命令、资产生成等动作判为 `RequiresConfirmation`。Provider 多动作批次会因此整体停在 `waiting-for-confirmation`,无人干预目标无法继续。
- 决策:`autonomous-game-build` 只允许固定 auto-safe 白名单消除默认确认;其余任何本地或 MCP 动态确认结果统一失败关闭为 `Denied`。批次含拒绝成员时先完整预检并持久化 `aborted`,整批工具保持零执行,再把拒绝 observation 交回同一 Session/run 重新规划。标准 profile 和显式 deny 保持原合同。
- 恢复:旧自主 `pending-confirmation` 必须在校验持久 Run Profile、Runtime、Session/run、action identity 和 batch member 后迁移。先原子写 `aborted` batch,再写 `observed-rejected` pending 镜像;两次写入之间退出时由 batch 重建 pending。恢复后的状态和审计使用 `runtime-policy-rejected`,不能误报自动动作已执行或开发者主动拒绝。
- 验收:自主构建测试 `20/20`、确认测试 `18/18`、旧等待批次完整恢复、批次零副作用、`cargo check --tests`、Rust 串行全量 `1149 passed / 5 ignored / 0 failed` 均通过。确定性 Runner + Chrome E2E 为 `17/17` Provider lifecycle、revision `0 -> 2`、试玩 `37/37`;独立外部 Provider E2E 为 `62/62`、revision `0 -> 5``game/index.html=7816 bytes`、试玩 `37/37`,两轮人工输入、等待态、残留、重复与泄漏均为 `0`
@@ -3543,3 +3543,11 @@
- 处理:在 pending 标记为 executing 之前检查 revision 漂移;漂移时写入可恢复的 blocked observation,明确旧动作未执行并要求同 run 重新规划。文件写入和 patch 在项目锁内再做一次同样检查,防止预检后的竞态。只有动作可能已落盘、账本身份冲突、审计失败或持久记录损坏时继续失败关闭到 reconciliation。
- 验证:必须覆盖确认后 stale 动作和锁内 stale 动作两条路径,证明目标文件未改变、旧 pending 被收束、下一次 Provider planning 使用同一 run、最终状态可完成,并用单输入真实 external-provider E2E 验证并行专业 Agent 最终生成可试玩项目。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/project_gates.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/interaction.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/main_loop.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/file_ops.rs`
## 自主模式不能保留任何 RequiresConfirmation 漏口
- 现象:`user.input_request` 已被拒绝,但模型选择项目权限或 MCP catalog 标记为确认的工具后,整个自主批次仍进入 `waiting-for-confirmation`;重启还会忠实恢复这个等待态,形成永久阻塞。
- 原因:只在本地 command policy 中提升少量 auto-safe 工具,不能覆盖 MCP 动态 approval;只修改新请求策略,也不能处理旧版本已经持久化的 waiting batch。拒绝 observation 若仍带 `executionMode=auto`,通用续跑还可能把它错误投影为“已执行自动工具”。
- 处理:最终合并后的本地/MCP policy block 必须再次经过持久 Run Profile gate;自主 profile 的所有剩余确认统一转为 deny,混合批次整体零执行并同 run 重规划。恢复迁移以 aborted batch 为提交点、pending 为镜像,并把自动策略拒绝显式审计为 `runtime-policy-rejected`
- 验证:同时覆盖 auto-safe 白名单、显式 deny、动态确认失败关闭、标准 profile 不变、完整恢复扫描、前缀副作用未发生、Session/run/profile identity 不变和 replacement planning 已发出;最后必须重新运行确定性与外部 Provider 的单输入可玩 E2E。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_tools/policy.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_recovery.rs``apps/ai-game-creator-shell/src-tauri/src/agent/runtime_driver/pending_execution.rs`
@@ -700,3 +700,6 @@ game-project/
- 2026-07-22 的 V1.45 修复专业 Agent 并行修改导致的 stale pending 中断。执行前发现 project revision 漂移不再进入 `needs-reconciliation`,而是以 `blocked + projectRevisionDrift + replanRequired` observation 回到同一 run 继续 planning`file.write / file.patch` 还会在项目写锁内复核 pending 身份、仓库上下文、revision 与 verification gate。该语义不放宽并发写安全:旧动作始终不执行,副作用未知、账本冲突或持久化损坏仍失败关闭。
- V1.45 的独立真实 external-provider 验收只向 `--swarm-chat --init --autonomous-game-build` 写入一次“植物大战僵尸式塔防”需求后立即 EOF。最终 `status=PASS / turn.report=settled`approve / answer / steer 为 `0`;两条初始 delivery 与一条 repair delivery 均被父 run 认领,项目 revision `0 -> 8``game/index.html``7814` bytesstatic smoke、desktop / mobile 浏览器和 `lane-defense-v1 37/37` 全通过。唯一 Supervisor assistant 已提交,pending、confirmation、user-input、provider retry/handoff、tool-plan handoff、finalization、reconciliation、重复和全部隐私泄漏计数均为 `0`
- V1.45 稳定树同时通过三个新增/更新定向回归、`revision 32/32``cargo check --tests`、Linux 串行全量 `1147 passed / 5 ignored / 0 failed``cargo fmt --check`、encoding 与 `git diff --check`。确定性正式 E2E 继续为 **PASS**Provider lifecycle `17/17`、revision `0 -> 2`、Chrome `37/37`,终局残留与泄漏均为 `0`
- 2026-07-22 的 V1.46 消除自主构建的人工确认等待。`autonomous-game-build` 仅把固定 auto-safe 白名单提升为自动执行;项目级、Agent 级和 MCP catalog 动态策略产生的其余 `RequiresConfirmation` 一律转为 `Denied`,向同一 run 返回“改用 auto-safe 工具或省略动作”的 observation。显式 deny 继续优先,标准 profile 的确认语义不变;包含拒绝成员的 Provider action 批次在任何工具执行前整体 `aborted`,不能执行 auto 前缀。
- V1.46 的 Runner 恢复会把旧版本已持久化的自主 `pending-confirmation / waiting-confirmation` 迁移为 `observed-rejected / aborted`。Provider batch 是原子提交点,pending 只是可重建镜像;批次先落盘、pending 后落盘之间强杀时,下次恢复从 aborted batch 补齐拒绝账本并继续原 Session/run。公共状态和审计使用 `runtime-policy-rejected`,不得写成“已执行自动工具”或“开发者拒绝”。
- V1.46 稳定树通过自主构建过滤 `20/20`、确认相关 `18/18`、旧等待态完整恢复、既有批次零副作用、`cargo check --tests`、Rust 串行全量 `1149 passed / 5 ignored / 0 failed`、fmt 与 diff 检查。确定性正式 E2E 为 **PASS**Provider lifecycle `17/17`、revision `0 -> 2`、Chrome `37/37`、人工输入与残留均为 `0`。新的独立外部 Provider 同轮 E2E 也为 **PASS**:单条任务后 EOFapprove / answer / steer 为 `0`Provider lifecycle `62/62`revision `0 -> 5``game/index.html``7816` bytesstatic smoke、desktop / mobile 与 `lane-defense-v1 37/37` 全通过,唯一 Supervisor assistant,全部 sidecar、reconciliation、重复和泄漏计数均为 `0`