修复自主构建等待确认导致的循环中断
将自主模式剩余确认策略统一失败关闭并保持标准模式语义 迁移旧自主确认批次并在同一 Session 和 run 重新规划 补齐零执行审计、恢复回归和真实可玩验收记录
This commit is contained in:
@@ -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 {
|
||||
|
||||
+9
-1
@@ -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,
|
||||
|
||||
+125
-13
@@ -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` bytes,static 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**:单条任务后 EOF,approve / answer / steer 为 `0`,Provider lifecycle `62/62`,revision `0 -> 5`,`game/index.html` 为 `7816` bytes,static smoke、desktop / mobile 与 `lane-defense-v1 37/37` 全通过,唯一 Supervisor assistant,全部 sidecar、reconciliation、重复和泄漏计数均为 `0`。
|
||||
|
||||
Reference in New Issue
Block a user