封堵隔离子 Agent 命令越界
禁止动态隔离子 Agent 通过项目级命令绕过 writeScopes 在工具策略和 Provider 批次预检中统一执行隔离门禁 补充恶意 sibling 写入、旧 pending 与混合协作回归 同步 Runtime V1.34 技术方案和共享决策
This commit is contained in:
@@ -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 和八类直方图均为 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` 变成项目根其余部分只读、链接/挂载不可逃逸且所有后代继承的强制边界,并通过独立跨平台门禁后,才可在新决策中重新评估命令工具;其余拒绝能力仍需各自单独评审。
|
||||
|
||||
@@ -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 降为 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 <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 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` 并通过独立决策与测试后才可重新评估开放。
|
||||
|
||||
Reference in New Issue
Block a user