封堵隔离子 Agent 命令越界

禁止动态隔离子 Agent 通过项目级命令绕过 writeScopes
在工具策略和 Provider 批次预检中统一执行隔离门禁
补充恶意 sibling 写入、旧 pending 与混合协作回归
同步 Runtime V1.34 技术方案和共享决策
This commit is contained in:
AIGameCreator App
2026-07-18 03:16:36 +08:00
parent 1b045a5f46
commit edfeeff701
6 changed files with 398 additions and 32 deletions
@@ -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<AgentRuntimeToolPolicySnapshot, String> {
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)),
@@ -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]
@@ -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::{
@@ -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 和八类直方图均为 0Provider 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` 变成项目根其余部分只读、链接/挂载不可逃逸且所有后代继承的强制边界,并通过独立跨平台门禁后,才可在新决策中重新评估命令工具;其余拒绝能力仍需各自单独评审。
@@ -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/<instanceId>.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 降为 54started / 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 <AppData>`
@@ -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 AgentRuntime 在任何 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` 并通过独立决策与测试后才可重新评估开放。