From edfeeff7011b08226c6fe48ab65ee9a3146ce921 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Sat, 18 Jul 2026 03:16:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=81=E5=A0=B5=E9=9A=94=E7=A6=BB=E5=AD=90?= =?UTF-8?q?=20Agent=20=E5=91=BD=E4=BB=A4=E8=B6=8A=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 禁止动态隔离子 Agent 通过项目级命令绕过 writeScopes 在工具策略和 Provider 批次预检中统一执行隔离门禁 补充恶意 sibling 写入、旧 pending 与混合协作回归 同步 Runtime V1.34 技术方案和共享决策 --- .../src-tauri/src/agent.rs | 61 +++-- .../src-tauri/src/isolated_agent.rs | 79 +++++- .../src-tauri/src/tests.rs | 250 ++++++++++++++++++ .../shared-memory/decision-log.md | 9 + ...案】AI游戏创作Agent Runtime V1.1-2026-07-12.md | 29 +- ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 2 + 6 files changed, 398 insertions(+), 32 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 8b6d41c1f..27933372c 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -13972,15 +13972,29 @@ pub(crate) async fn prepare_game_creator_agent_runtime_provider_action_batch( None, )?; let command_id = game_creator_agent_runtime_tool_command_id(action.tool.trim()); - let local_policy_block = command_id - .map(|command_id| { - game_creator_agent_runtime_tool_policy_rule(root, &runtime.agent_id, command_id) - }) - .unwrap_or_else(|| { - Some(AgentRuntimeToolPolicyBlock::Denied( - "工具不在 Agent Runtime 白名单中".to_string(), - )) - }); + let isolated_scope_block = if runtime.agent_id.starts_with("child-") { + validate_isolated_agent_tool_scope_at( + root, + &runtime.agent_id, + action.tool.trim(), + &action.input, + ) + .err() + .map(AgentRuntimeToolPolicyBlock::Denied) + } else { + None + }; + let local_policy_block = isolated_scope_block.or_else(|| { + command_id + .map(|command_id| { + game_creator_agent_runtime_tool_policy_rule(root, &runtime.agent_id, command_id) + }) + .unwrap_or_else(|| { + Some(AgentRuntimeToolPolicyBlock::Denied( + "工具不在 Agent Runtime 白名单中".to_string(), + )) + }) + }); let mcp_policy_block = if matches!( local_policy_block, Some(AgentRuntimeToolPolicyBlock::Denied(_)) @@ -22438,10 +22452,15 @@ fn agent_runtime_tool_policy_snapshot_at( agent_id: &str, ) -> Result { let policy = agent_runtime_effective_tool_policy_at(root, agent_id)?; + let isolated = agent_id.trim().starts_with("child-"); let mut auto_tools = Vec::new(); let mut confirm_tools = Vec::new(); let mut denied_tools = Vec::new(); for tool in agent_runtime_executable_tools() { + if isolated && ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) { + denied_tools.push(tool.to_string()); + continue; + } if tool == GAME_CREATOR_USER_INPUT_REQUEST_TOOL { auto_tools.push(tool.to_string()); continue; @@ -22511,17 +22530,12 @@ fn agent_runtime_effective_tool_policy_at( } } if isolated { - for command_id in [ - "agent.spawn_isolated", - "project.restore", - "agent.schedule_ready", - "canvas.asset_generate", - "task.create", - "task.update", - GAME_CREATOR_MCP_CALL_TOOL, - ] { - if !denied_commands.iter().any(|command| command == command_id) { - denied_commands.push(command_id.to_string()); + for command_id in ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS { + if !denied_commands + .iter() + .any(|command| command.as_str() == *command_id) + { + denied_commands.push((*command_id).to_string()); } } } @@ -22545,6 +22559,13 @@ fn game_creator_agent_runtime_tool_policy_rule( Ok(agent_id) => agent_id, Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), }; + if agent_id.starts_with("child-") + && ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS.contains(&command_id) + { + return Some(AgentRuntimeToolPolicyBlock::Denied(format!( + "动态隔离子 Agent 默认拒绝无 writeScope 落点的命令:{command_id}" + ))); + } let policy_agent_id = match game_creator_runtime_template_agent_id_at(root, &agent_id) { Ok(policy_agent_id) => policy_agent_id, Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)), diff --git a/apps/ai-game-creator-shell/src-tauri/src/isolated_agent.rs b/apps/ai-game-creator-shell/src-tauri/src/isolated_agent.rs index ad8ac01fa..c3a6222ab 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/isolated_agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/isolated_agent.rs @@ -33,6 +33,41 @@ pub(crate) const ISOLATED_AGENT_JOIN_PROMPT_SCHEMA_VERSION: &str = pub(crate) const ISOLATED_AGENT_PRIVATE_MEMORY_SCHEMA_VERSION: &str = "game-creator-isolated-agent-private-memory.v1"; +pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_COMMAND_IDS: &[&str] = &[ + "project.verify", + "project.git_commit", + "command.exec", + "command.start", + "command.stdin", + "preview.start", + "agent.delegate", + "agent.spawn_isolated", + "project.restore", + "agent.schedule_ready", + "canvas.asset_generate", + "task.create", + "task.update", + GAME_CREATOR_MCP_CALL_TOOL, +]; + +pub(crate) const ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS: &[&str] = &[ + "project.verify", + "project.git_commit", + "command.exec", + "command.start", + "command.stdin", + "preview.start", + "agent.delegate", + "agent.spawn_isolated", + "project.restore", + "agent.schedule_ready", + "canvas.asset_generate", + "task.create", + "task.update", + "blackboard.write", + GAME_CREATOR_MCP_CALL_TOOL, +]; + const ISOLATED_AGENT_INSTANCE_DIR: &str = ".agent/runtime/isolated-agents/instances"; const ISOLATED_AGENT_GROUP_DIR: &str = ".agent/runtime/isolated-agents/groups"; const ISOLATED_AGENT_RESULT_DIR: &str = ".agent/runtime/isolated-agents/results"; @@ -469,17 +504,7 @@ pub(crate) fn validate_isolated_agent_tool_scope_at( return Err("动态隔离子 Agent 只能写入自己的 instance 私有记忆".to_string()); } } - if matches!( - tool, - "agent.spawn_isolated" - | "project.restore" - | "agent.schedule_ready" - | "canvas.asset_generate" - | "task.create" - | "task.update" - | "blackboard.write" - | GAME_CREATOR_MCP_CALL_TOOL - ) { + if ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS.contains(&tool) { return Err(format!( "动态隔离子 Agent 默认拒绝无 writeScope 落点的工具:{tool}" )); @@ -1957,6 +1982,38 @@ mod tests { &serde_json::json!({}), ) .is_err()); + for tool in ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS { + let error = validate_isolated_agent_tool_scope_at( + temp.path(), + instance_id, + tool, + &serde_json::json!({}), + ) + .expect_err("unscoped isolated child tool must be denied"); + assert!( + error.contains("动态隔离子 Agent"), + "unexpected {tool} error: {error}" + ); + } + for tool in [ + "command.output_read", + "command.poll", + "command.terminate", + "command.run_limited", + "preview.validate", + "project.checkpoint", + ] { + assert!( + validate_isolated_agent_tool_scope_at( + temp.path(), + instance_id, + tool, + &serde_json::json!({}), + ) + .is_ok(), + "{tool} should remain available to isolated children" + ); + } } #[test] diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index eda24df11..33d57e9d1 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -17018,6 +17018,256 @@ async fn runtime_v11_closure_isolated_child_memory_is_instance_private() { fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn runtime_v134_isolated_child_unscoped_commands_cannot_bypass_write_scopes() { + use platform_agent::game_creation::{ + GameCreationIsolatedAgentChildSpec, GameCreationIsolatedAgentJoinMode, + GameCreationIsolatedAgentSpawnRequest, + }; + + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "隔离命令作用域测试").expect("project init"); + write_project_permission_policy_at( + &root, + ProjectPermissionPolicy { + denied_commands: Vec::new(), + confirm_commands: Vec::new(), + agent_policies: BTreeMap::new(), + }, + ) + .expect("write permissive project policy"); + let request = GameCreationIsolatedAgentSpawnRequest { + children: vec![GameCreationIsolatedAgentChildSpec { + template_agent_id: "code-prototype".to_string(), + task: "只修改 feature-a".to_string(), + acceptance_criteria: vec!["feature-a 产物存在".to_string()], + expected_artifacts: vec!["game/feature-a/output.txt".to_string()], + write_scopes: vec!["game/feature-a/**".to_string()], + }], + join_mode: GameCreationIsolatedAgentJoinMode::All, + }; + let group = create_or_read_isolated_group_at( + &root, + "design-director", + "runtime-v134-parent-run", + "runtime-v134-parent-session", + "runtime-v134-parent-action", + &request, + ) + .expect("create isolated group"); + let instance = resolve_isolated_agent_instance_at(&root, &group.instance_ids[0]) + .expect("resolve isolated child"); + ensure_agent_conversation_session_at( + &root, + &instance.instance_id, + &instance.session_id, + "隔离命令作用域", + ) + .expect("ensure isolated child session"); + let runtime = start_game_creator_agent_runtime_task_for_session_at( + &root, + &instance.instance_id, + Some(&instance.session_id), + &instance.task, + &instance.run_id, + AGENT_RUNTIME_ISOLATED_CHILD_SOURCE, + "验证 isolated command scope", + vec!["尝试越界命令".to_string()], + ) + .expect("start isolated child runtime"); + let sibling_marker = root.join("game/feature-b/bypass.txt"); + + let blocked_actions = [ + AgentRuntimeToolAction { + tool: "project.verify".to_string(), + reason: Some("验证脚本可能写 sibling 目录".to_string()), + input: serde_json::json!({ + "script": "test", + "expectedCommand": "bash -lc 'mkdir -p game/feature-b && printf bypass > game/feature-b/bypass.txt'", + "timeoutSeconds": 120 + }), + }, + AgentRuntimeToolAction { + tool: "command.exec".to_string(), + reason: Some("尝试一次性命令越界写入".to_string()), + input: serde_json::json!({ + "program": "bash", + "args": ["-lc", "mkdir -p game/feature-b && printf bypass > game/feature-b/bypass.txt"], + "cwd": ".", + "timeoutSeconds": 120 + }), + }, + AgentRuntimeToolAction { + tool: "command.start".to_string(), + reason: Some("尝试持久进程越界写入".to_string()), + input: serde_json::json!({ + "program": "bash", + "args": ["-lc", "mkdir -p game/feature-b && printf bypass > game/feature-b/bypass.txt"], + "cwd": ".", + "timeoutSeconds": 120 + }), + }, + AgentRuntimeToolAction { + tool: "command.stdin".to_string(), + reason: Some("尝试向旧进程注入写入命令".to_string()), + input: serde_json::json!({ + "processId": "proc-legacy-isolated", + "data": "mkdir -p game/feature-b && printf bypass > game/feature-b/bypass.txt", + "appendNewline": true, + "eof": false + }), + }, + AgentRuntimeToolAction { + tool: "project.git_commit".to_string(), + reason: Some("尝试由 isolated child 提交项目".to_string()), + input: serde_json::json!({ + "message": "越界提交", + "paths": ["game/feature-a/output.txt"], + "expectedHead": "0".repeat(40), + "expectedSnapshotFingerprint": "0".repeat(64) + }), + }, + AgentRuntimeToolAction { + tool: "preview.start".to_string(), + reason: Some("尝试启动共享预览副作用".to_string()), + input: serde_json::json!({}), + }, + AgentRuntimeToolAction { + tool: "agent.delegate".to_string(), + reason: Some("尝试绕过 isolated all-join 嵌套委派".to_string()), + input: serde_json::json!({ + "agentId": "art-director", + "task": "绕过父级 all-join" + }), + }, + ]; + for action in &blocked_actions { + let observation = execute_game_creator_agent_runtime_tool_action( + &root, + &instance.instance_id, + &instance.run_id, + &instance.task, + action, + ) + .await; + assert_eq!( + observation.status, "rejected", + "{}: {}", + action.tool, observation.summary + ); + assert!(observation.summary.contains("动态隔离子 Agent")); + } + + let policy = runtime.tool_policy.clone(); + for tool in ISOLATED_AGENT_UNSCOPED_DENIED_TOOLS { + assert!( + policy.denied_tools.iter().any(|value| value == tool), + "{tool} must be visible as denied" + ); + assert!(!policy.auto_tools.iter().any(|value| value == tool)); + assert!(!policy.confirm_tools.iter().any(|value| value == tool)); + } + for tool in ["command.run_limited", "command.terminate"] { + assert!(policy.allowed_tools.iter().any(|value| value == tool)); + assert!(!policy.denied_tools.iter().any(|value| value == tool)); + } + + let revision_before = read_game_creator_agent_runtime_project_revision(&root) + .expect("read revision before isolated batch"); + let plan = AgentRuntimeToolPlan { + thinking_summary: "尝试在同一批次越界写入并读取".to_string(), + plan_update: None, + plan: vec!["执行越界命令".to_string(), "读取项目".to_string()], + actions: vec![ + blocked_actions[1].clone(), + AgentRuntimeToolAction { + tool: "file.read".to_string(), + reason: Some("读取允许的项目上下文".to_string()), + input: serde_json::json!({ + "path": "game/index.html", + "startLine": 1, + "maxLines": 20 + }), + }, + ], + response: String::new(), + }; + let preparation = prepare_game_creator_agent_runtime_provider_action_batch( + &root, + &runtime, + &instance.task, + &plan, + &[], + &revision_before, + &"a".repeat(64), + ) + .await + .expect("prepare isolated child provider action batch"); + let AgentRuntimeProviderActionBatchPreparation::Aborted { + batch, + pending, + observation, + } = preparation + else { + panic!("isolated command batch must abort before confirmation"); + }; + assert_eq!(batch.status, "aborted"); + assert_eq!(pending.action.tool, "command.exec"); + assert_eq!( + pending.status, + AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_REJECTED + ); + assert_eq!(observation.status, "blocked"); + assert!(observation.summary.contains("动态隔离子 Agent")); + assert!(!game_creator_agent_runtime_pending_tool_action_path( + &root, + &instance.instance_id, + &instance.run_id, + ) + .exists()); + assert_eq!( + read_game_creator_agent_runtime_project_revision(&root) + .expect("read unchanged revision after isolated batch"), + revision_before + ); + assert!(!sibling_marker.exists()); + assert!( + static_delegate_target_agent_ids_at(&root, &instance.instance_id, &instance.run_id,) + .expect("read isolated nested delegate targets") + .is_empty() + ); + + let recovered_pending = pending_tool_action_for_test( + &root, + &runtime, + blocked_actions[1].clone(), + AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING, + None, + ); + write_game_creator_agent_runtime_pending_tool_action(&root, &recovered_pending) + .expect("write legacy isolated command pending action"); + let recovered = execute_game_creator_agent_runtime_tool_action_with_pending_action( + &root, + &instance.instance_id, + &instance.run_id, + &instance.task, + &recovered_pending.action, + Some(&recovered_pending.action_id), + Some(&recovered_pending), + ) + .await; + assert_eq!(recovered.status, "rejected"); + assert!(recovered.summary.contains("动态隔离子 Agent")); + assert!(!sibling_marker.exists()); + assert_eq!( + read_game_creator_agent_runtime_project_revision(&root) + .expect("read unchanged revision after recovered pending"), + revision_before + ); + + fs::remove_dir_all(root).ok(); +} + #[test] fn runtime_v11_closure_parent_terminals_cancel_children_and_suppress_join_once() { use platform_agent::game_creation::{ diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 40938a15d..74391b8ca 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4833,3 +4833,12 @@ - Prompt:原生 function arguments 的统一外壳明确为 `reason + input`;legacy text JSON schema 只属于没有 function tools 的 Provider,避免工具自己的 input schema 与旧 actions JSON 示例互相竞争。 - 诊断过程:首轮旧保守正文规则得到 35 个成功计划、23 次 `response-shape` repair,且因 E2E 白名单遗漏 Agent DB envelope 误报 58 条泄漏而 FAIL;第二次新规则尝试在 2 个计划、0 repair 时因 Provider isolated write scope 不满足 fixture 提前停止。两轮都不作为完成证据。 - 真实验收:最终代码对应的正式 `gpt-5.5 / openai_chat / high` 同 suite 独立 PASS。46/46 个成功计划全部使用 `native_runtime_tools`,格式 repair 和八类直方图均为 0;Provider lifecycle 为 54/54 started/terminal,其中 53 completed、1 次瞬态失败通过新 request identity 显式重试恢复,相比 V1.32 的 86 减少 32,总耗时 `631.6s`。static + isolated 混合协作、业务 delivery repair、Provider 真并行、pidfd Runner 强杀恢复、宿主验证和唯一 Supervisor assistant 全部成立;重复、残留 sidecar、正文、Key、项目 / 正式配置路径与报告泄漏均为 0,隔离现场完整清理。 + +## 2026-07-18 AI 游戏创作 Agent Runtime V1.34 动态隔离子 Agent writeScopes 命令绕过封堵 + +- 背景:动态 isolated child 的 `writeScopes` 只约束结构化 file/patchset 路径;现有 V1.11 OS sandbox 仍把项目根整体挂为可写。若 child 继承 `project.verify`、通用命令、持久进程或预览启动,shell、构建 hook 和后代进程可以绕过路径校验写到 scope 外。approval 不能替代 OS 级作用域隔离。 +- 决策:在 scope-aware OS sandbox 完成前,动态 child 无条件禁用 `project.verify / project.git_commit / command.exec / command.start / command.stdin / preview.start / agent.delegate / agent.spawn_isolated / project.restore / agent.schedule_ready / canvas.asset_generate / task.create / task.update / blackboard.write` 和全部 MCP 动态函数/兼容调用。有效策略快照把对应内置工具和 `mcp.call` 显示为 `denied`;动态 MCP function 归一后执行同一拒绝。模板 Agent policy、项目 policy、legacy 快照和用户 approval 均不能放宽。 +- 保留边界:继续允许固定只读且不接受任意 program/argv/shell 的 `command.run_limited`,同 child/run 身份的 `command.output_read / command.poll / command.terminate`,只验证既有精确 loopback 预览的 `preview.validate`,以及目标完整位于有效 `writeScopes` 内的 `file.write / file.patch / file.delete / project.patchset`。多文件变更含一个越界目标即在 checkpoint、revision 和真实写入前整组拒绝;通用验证交由父 Agent 或静态专业 Agent 完成。 +- 原子与恢复:新单动作在 confirmation 与 OS launcher 前拒绝,不产生 spawn、revision 或项目副作用。新多 action batch 在选择 confirmation 模式前逐项校验,任一 denied member 使整批 abort,允许成员也不执行;只保留 `aborted / nextActionIndex=0` batch 事实,不发布独立 pending sidecar。旧 pending、approval 与旧 batch 真正进入执行器时仍重验当前边界;旧 executing 未知结果继续按既有 reconciliation 规则处理,绝不 replay。 +- 验证方式:新增恶意 `bash -lc` sibling 写入回归,覆盖单动作、两动作 batch、策略快照和旧 executing pending 的执行器重验;断言 sibling 文件、nested delivery、独立 pending sidecar 和 revision 变化均为 0。工具作用域单测逐项覆盖拒绝集合与保留工具;同时运行 isolated 30 项、mixed 3 项、Supervisor collaboration 27 项、Provider batch 12 项和 Tauri 全量回归。 +- 真实验收边界:V1.31/V1.32 已以 isolated mutation 为 0 的真实 Provider suite 证明 mixed 协作、all-join、Runner 恢复和唯一回复;V1.34 只做安全收紧,本切片不为此重跑两套 Provider,也不能把旧 PASS 当作未来新 child 写入语义的证据。只有后续 scope-aware OS sandbox 能把有效 `writeScopes` 变成项目根其余部分只读、链接/挂载不可逃逸且所有后代继承的强制边界,并通过独立跨平台门禁后,才可在新决策中重新评估命令工具;其余拒绝能力仍需各自单独评审。 diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md index d703c6995..481e927f6 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md @@ -184,7 +184,7 @@ Runner 从显式 AppData 目录读取 `game-creator.config.json`,API Key 不 - 角色 prompt、LLM 配置和 Agent 策略继承 `templateAgentId`;执行和持久化 lane 使用 `instanceId`。 - 单次最多 3 个并行实例,最大深度 1。 - sibling `writeScopes` 不得重叠;所有真实写入仍通过项目级写锁、revision 和 verification gate。 -- 子实例默认拒绝 `agent.spawn_isolated`、`project.restore` 和 `agent.schedule_ready`。 +- 子实例初版默认拒绝 `agent.spawn_isolated`、`project.restore` 和 `agent.schedule_ready`;现行无条件拒绝集合以 V1.34 为准,模板、项目策略和用户确认都不得放宽。 - 子实例的 `memory.read/write(scope=agent)` 只访问 `.agent/runtime/isolated-agents/memory/.json` 私有临时 lane;不能指定 sibling 或静态模板 Agent,也不能写 `project / session / blackboard` 共享记忆。普通静态 Agent 的私有记忆语义保持不变。 - 父任务进入 `failed / budget-exhausted / cancelled` 时,向所有非终态子实例写取消 tombstone;重复收束和恢复必须幂等,已开始或完成的 join continuation 不得被重新认领。 - 动态实例不写 manifest,不出现在普通用户 Agent 列表;开发 Runtime 状态页可读取其状态。 @@ -1194,6 +1194,31 @@ V1.32 的真实基线是 `49` 次成功 native tool plan 对应 `30` 次格式 最终代码对应的正式 `gpt-5.5 / openai_chat / high` 独立轮 **PASS**,总耗时 `631.6s`(约 10 分 32 秒)。46/46 次成功计划全部使用 `native_runtime_tools`,格式 repair 为 `0`,八类 repair 直方图全为 `0`;Provider lifecycle 从 V1.32 基线的 86 降为 54,started / terminal 均为 54,其中 53 completed、1 次瞬态失败通过新的 request identity 显式重试恢复,wrapper/text fallback 和协议审计 payload leak 均为 0。相同父 Session/run 完成 2 个 static delegate、1 个三 child isolated all-join、1 次业务 delivery repair、两类 Provider 真并行、pidfd Runner 强杀恢复、宿主验证和唯一 Supervisor assistant;重复 delivery/group/instance/result/join/claim/message/action/receipt/lifecycle、残留 sidecar、私密正文、API Key、项目 / 正式配置路径及报告泄漏均为 0,隔离 AppData 与 disposable 项目完整清理。 +## V1.34 动态隔离子 Agent writeScopes 命令绕过封堵 + +V1.34 修补动态 isolated child 的作用域绕过面。`writeScopes` 当前只对结构化文件工具和 `project.patchset` 的目标路径做确定性校验,而 V1.11 的 workspace-write OS sandbox 会把项目根整体挂为可写;因此 `project.verify`、通用命令、持久进程或预览启动一旦交给 child,shell、构建脚本、生命周期 hook 和后代进程仍可能写到自身 `writeScopes` 之外。用户确认只能批准动作,不能把项目根全局可写变成 scope-aware 隔离。在 scope-aware OS sandbox 完成并通过独立门禁前,本节覆盖 V1.11、V1.12 和第 4 节中较宽的 child 工具继承口径;静态专业 Agent 与父 Agent 的既有权限不因本节改变。 + +### 有效工具边界 + +- Runtime 识别出动态 isolated child 后,有效策略快照把 `project.verify`、`project.git_commit`、`command.exec`、`command.start`、`command.stdin`、`preview.start`、`agent.delegate`、`agent.spawn_isolated`、`project.restore`、`agent.schedule_ready`、`canvas.asset_generate`、`task.create`、`task.update`、`blackboard.write` 和 `mcp.call` 显示为 `denied`。动态 MCP function 最终归一为 `mcp.call` 后同样拒绝。模板 Agent、项目 policy、legacy 空策略和用户 approval 都不能放宽这组边界。 +- child 继续可使用固定受限且不接受任意 program、argv 或 shell 的 `command.run_limited`,当前只允许只读验证 `game.static_smoke`;`command.output_read` 只回读本 child 有权访问的既有命令输出,`command.poll / command.terminate` 只观察或收束已绑定同一 child/run 的既有进程,不得启动、接管、重连或向进程写 stdin。`preview.validate` 继续只验证当前授权项目的精确既有 loopback 预览,不负责启动预览服务。 +- 项目内容写入只保留 `file.write / file.patch / file.delete / project.patchset`。每一个 create/update/delete 目标都必须经过现有私有路径、链接与规范化校验,并完整落在该 child 的有效 `writeScopes` 内;patchset 中任一目标越界时整组修改在 checkpoint、revision 和真实文件写入前失败。其它未在本节列出的工具继续遵守既有 isolated child 限制和有效 policy,本节不新增能力。 +- child 修改后可用符合固定合同的 `command.run_limited` 形成当前静态试玩凭证;需要通用构建、测试或项目级验证时,由父 Agent 或静态专业 Agent 在认领 child 交付后执行。`preview.validate` 仍只提供浏览器证据,不单独签发项目 revision 验证门禁。child 不得伪造 `verifiedRevision`,也不得借 `project.verify / command.exec` 绕过作用域。 + +### 拒绝、批次与恢复顺序 + +- 单个新 action 在 durable child 身份核对后、项目 policy 确认和 OS launcher 之前执行 scope 校验;拒绝结果不进入 `waiting-for-confirmation`,不创建进程、revision 或项目副作用。 +- 新的 2-3 action Provider batch 在确定 confirmation 模式前逐项执行同一 scope 校验。任一成员被拒绝时,batch 直接写成 `aborted / nextActionIndex=0`,不发布独立 pending-action sidecar,不创建 confirmation,也不执行同批其它成员;安全 batch 事实仍保留用于恢复和审计。 +- 旧 `pending / approved` 动作即使已由旧版本显示或确认,真正进入执行器时仍会重新调用当前 scope 校验,approval 不能穿透;旧 Provider batch 到达对应成员时也应用同一边界。旧 `executing` 且没有可信终态的通用工具继续沿用既有 `needs-reconciliation` 规则,Runtime 不会自动 replay。已有 child-owned process 只允许通过保留的 `command.poll / command.terminate` 观察和清理。 + +### 确定性验收 + +新增 `runtime_v134_isolated_child_unscoped_commands_cannot_bypass_write_scopes`,用真实恶意 `bash -lc` 参数尝试写入 sibling scope,覆盖单动作、两动作 batch、策略快照和旧 executing pending 的执行器重验;断言 sibling 文件不存在、nested delivery 为 0、独立 pending sidecar 不存在且 project revision 保持不变。`isolated_agent::tests::write_tools_stay_inside_instance_scopes_and_dangerous_tools_are_denied` 逐项覆盖完整拒绝集合和保留工具。回归范围同时运行 `isolated` 30 项、`project_supervisor_mixed_` 3 项、`supervisor_collaboration_` 27 项与 `provider_action_batch_` 12 项。 + +V1.31 与 V1.32 的真实 Provider suite 已分别证明 mixed static/isolated 协作、all-join、Runner 恢复和唯一收束,且验收中的 isolated child 项目 mutation 为 `0`。V1.34 只收紧 child 的本地工具能力,不改变 Provider 请求、委派合同或回复协议,因此本切片不重跑两套外部 Provider;后续只要改变 child prompt、任务、协作拓扑或写入语义,就必须重新建立真实 Provider 证据。 + +后续只有 scope-aware OS sandbox 能把 child 的有效 `writeScopes` 转换为 OS 强制边界,保证项目根其余部分只读、链接和挂载不能逃逸、所有 shell/构建器/hook/后代进程继承同一限制,并通过跨平台越界写与恢复测试后,才可在新的版本决策中重新评估 `project.verify / command.exec / command.start / command.stdin / preview.start`。scope-aware sandbox 是重新开放命令的必要条件而非自动授权;`project.git_commit`、委派、共享控制面写入、素材生成和 MCP 仍需各自的独立安全决策,模板或项目 policy 不得提前开放。 + ## 验收命令 - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture` @@ -1203,6 +1228,8 @@ V1.32 的真实基线是 `49` 次成功 native tool plan 对应 `30` 次格式 - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml mcp_ -- --nocapture` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml provider_action_batch_ -- --nocapture` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml parallel_read_batch_ -- --nocapture` +- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml runtime_v134_ -- --nocapture` +- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml isolated -- --nocapture --test-threads=1` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_mixed_ -- --nocapture --test-threads=1` - `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml supervisor_collaboration_ -- --nocapture --test-threads=1` - `npm run agc:collaboration-policy-e2e -- --config-dir ` diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 880ad3ed1..df86f2684 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -594,3 +594,5 @@ game-project/ - 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。 - 2026-07-17 起,同一 Runtime 文档的“V1.32 Runtime 强制 Supervisor 协作合同”作为 mixed swarm 可靠性事实源。项目可用 `.agent/collaboration-policy.json` 约束首波 static/isolated 模式、数量和 required static Agent;Runtime 在任何 child 副作用前整批校验并把合同指纹固化进 Provider batch v2。当前父 run 一旦形成 delivery/group,正式 `project-supervisor` 默认只负责编排、状态认领和验证,不再直接执行项目 mutation;专业 Agent/isolated child 权限与唯一 Supervisor 最终回复边界保持不变。 - 2026-07-17 V1.32 最终代码已完成独立真实 Provider PASS:首批 mixed batch、三 isolated child、Runner 强杀恢复、专业返工、宿主验证、唯一最终回复与零重复/残留/泄漏同时成立。真实报告计数、隔离重试配置和仍待收敛的 tool-plan repair 成本统一以 Runtime 文档 V1.32 章节与共享决策记录为准。 +- 2026-07-18 起,同一 Runtime 文档的“V1.34 动态隔离子 Agent writeScopes 命令绕过封堵”作为 isolated child 的现行能力事实源。在 scope-aware OS sandbox 完成前,动态 child 无条件禁用 `project.verify / project.git_commit / command.exec / command.start / command.stdin / preview.start / agent.delegate / agent.spawn_isolated / project.restore / agent.schedule_ready / canvas.asset_generate / task.create / task.update / blackboard.write` 和全部 MCP;原生工具策略统一显示 `denied`,模板、项目 policy 与用户确认均不能放宽。保留固定只读 `command.run_limited`、同身份 `command.output_read / command.poll / command.terminate`、既有预览的 `preview.validate`,以及严格位于 `writeScopes` 内的 `file.write / file.patch / file.delete / project.patchset`。 +- V1.34 的新单动作在 confirmation 和 OS launcher 前拒绝;新多 action 原生 batch 只要含一个 denied member 就在独立 pending-action sidecar、confirmation、OS spawn、revision 和任何成员项目副作用前整批 abort,只保留 `aborted / nextActionIndex=0` batch 事实。旧 pending / approval / batch 真正进入执行器时仍重新应用当前 child 边界,旧 executing 未知结果继续进入既有 reconciliation。该安全收紧由恶意 sibling 写入、策略快照、batch、旧 pending 执行器重验和 isolated/mixed/collaboration/provider-batch 回归证明;不因本切片重跑已通过且 isolated mutation 为 0 的 V1.31/V1.32 外部 Provider suite。通用命令只有在后续 scope-aware OS sandbox 对所有后代强制同一 `writeScopes` 并通过独立决策与测试后才可重新评估开放。