完善通用开发 Agent Runtime V1.1

新增仓库上下文指纹复核与 durable action 漂移保护
拆分 App CLI 与独立 Runner 的投递执行边界并补齐恢复诊断
加入桌面和移动端浏览器验证及跨源网络阻断
实现隔离子 Agent 私有记忆与终止状态传播
补齐 Provider 协议确认副作用重放与 join 真实端到端验证
同步共享契约实施计划决策记录与开发流程文档
This commit is contained in:
AIGameCreator App
2026-07-12 20:58:42 +08:00
parent 028e6b872d
commit 5e627a4677
27 changed files with 17694 additions and 531 deletions
+1
View File
@@ -12,6 +12,7 @@
"agent-task": "node scripts/run-cli-with-config.mjs --agent-task",
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.json --noEmit && node scripts/check-config.mjs"
},
"dependencies": {
File diff suppressed because it is too large Load Diff
+227
View File
@@ -196,6 +196,23 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "async-tungstenite"
version = "0.32.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc405d38be14342132609f06f02acaf825ddccfe76c4824a69281e0458ebd4"
dependencies = [
"atomic-waker",
"futures-core",
"futures-io",
"futures-task",
"futures-util",
"log",
"pin-project-lite",
"tokio",
"tungstenite",
]
[[package]]
name = "atk"
version = "0.18.2"
@@ -480,6 +497,71 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chromiumoxide"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26ed067eb6c1f660bdb87c05efb964421d2ca262bae0296cdfe38cf0cd949a3e"
dependencies = [
"async-tungstenite",
"base64 0.22.1",
"bytes",
"chromiumoxide_cdp",
"chromiumoxide_types",
"dunce",
"fnv",
"futures",
"futures-timer",
"pin-project-lite",
"reqwest 0.13.4",
"serde",
"serde_json",
"thiserror 2.0.18",
"tokio",
"tracing",
"url",
"which",
"windows-registry",
]
[[package]]
name = "chromiumoxide_cdp"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68a6a03a7ebac4ea85308f285d6959a3e6b2ce32a0c9465dc7a7b1db0144eec7"
dependencies = [
"chromiumoxide_pdl",
"chromiumoxide_types",
"serde",
"serde_json",
]
[[package]]
name = "chromiumoxide_pdl"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c602dea92337bc4d824668d78c5b79c3b4ddb29b40dd7218282bbe8fd3fc2091"
dependencies = [
"chromiumoxide_types",
"either",
"heck 0.5.0",
"once_cell",
"proc-macro2",
"quote",
"regex",
"serde_json",
]
[[package]]
name = "chromiumoxide_types"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678d5146e74f16fc4a41978b275af572cd913de1f10270d2b93b6c276bc57d80"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "chrono"
version = "0.4.45"
@@ -710,6 +792,12 @@ dependencies = [
"syn 2.0.118",
]
[[package]]
name = "data-encoding"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]]
name = "dbus"
version = "0.9.11"
@@ -905,6 +993,12 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "either"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]]
name = "embed-resource"
version = "3.0.9"
@@ -1122,6 +1216,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
@@ -1129,6 +1238,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -1190,12 +1300,19 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-timer"
version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
@@ -1309,6 +1426,8 @@ dependencies = [
name = "genarrative-ai-game-creator-shell"
version = "0.1.0"
dependencies = [
"chromiumoxide",
"futures",
"libc",
"platform-agent",
"platform-llm",
@@ -1321,7 +1440,9 @@ dependencies = [
"tauri-build",
"tauri-plugin-dialog",
"tauri-plugin-opener",
"tempfile",
"tokio",
"url",
"zip",
]
@@ -2914,6 +3035,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "precomputed-hash"
version = "0.1.1"
@@ -3012,6 +3142,35 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom 0.3.4",
]
[[package]]
name = "rangemap"
version = "1.7.1"
@@ -3588,6 +3747,17 @@ dependencies = [
"stable_deref_trait",
]
[[package]]
name = "sha1"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.9"
@@ -4586,6 +4756,23 @@ version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "tungstenite"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442"
dependencies = [
"bytes",
"data-encoding",
"http 1.4.2",
"httparse",
"log",
"rand",
"sha1",
"thiserror 2.0.18",
"utf-8",
]
[[package]]
name = "type1-encoding-parser"
version = "0.1.1"
@@ -4996,6 +5183,15 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "which"
version = "8.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d7cd18d4acb58fb3cdfe9ea54e6cd96a4e7d4cc45c56338b236e82dad47248"
dependencies = [
"libc",
]
[[package]]
name = "winapi"
version = "0.3.9"
@@ -5145,6 +5341,17 @@ dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-registry"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
dependencies = [
"windows-link 0.2.1",
"windows-result 0.4.1",
"windows-strings 0.5.1",
]
[[package]]
name = "windows-result"
version = "0.3.4"
@@ -5701,6 +5908,26 @@ dependencies = [
"zvariant",
]
[[package]]
name = "zerocopy"
version = "0.8.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]]
name = "zerofrom"
version = "0.1.8"
@@ -8,6 +8,8 @@ publish = false
tauri-build = { version = "2.6.2", features = [] }
[dependencies]
chromiumoxide = "0.9.1"
futures = "0.3"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
@@ -18,7 +20,9 @@ shared-contracts = { path = "../../../server-rs/crates/shared-contracts", defaul
tauri = { version = "2.11.2", features = [] }
tauri-plugin-dialog = "2.7.1"
tauri-plugin-opener = "2.5.4"
tempfile = "3"
tokio = { version = "1", features = ["io-util", "macros", "process", "rt-multi-thread", "time"] }
url = "2"
zip = { version = "2", default-features = false, features = ["deflate"] }
[target.'cfg(unix)'.dependencies]
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -14,6 +14,27 @@ pub(crate) enum CliCommand {
task: String,
initialize: bool,
},
AgentEnqueue {
project_path: PathBuf,
agent_id: String,
run_id: String,
task: String,
initialize: bool,
},
AgentRuntimeStatus {
project_path: PathBuf,
agent_id: String,
},
AgentConfirm {
project_path: PathBuf,
agent_id: String,
run_id: String,
action_id: String,
},
AgentResume {
project_path: PathBuf,
},
RunnerStatus,
AgentRun {
project_path: PathBuf,
prompt: String,
@@ -21,6 +42,126 @@ pub(crate) enum CliCommand {
},
}
impl CliCommand {
pub(crate) fn requires_external_agent_runner(&self) -> bool {
matches!(
self,
Self::AgentTask { .. }
| Self::AgentEnqueue { .. }
| Self::AgentConfirm { .. }
| Self::AgentResume { .. }
)
}
pub(crate) fn is_read_only_status(&self) -> bool {
matches!(self, Self::AgentRuntimeStatus { .. } | Self::RunnerStatus)
}
fn project_path_mut(&mut self) -> Option<(&mut PathBuf, bool)> {
match self {
Self::AgentTask {
project_path,
initialize,
..
}
| Self::AgentEnqueue {
project_path,
initialize,
..
} => Some((project_path, *initialize)),
Self::AgentChat { project_path, .. }
| Self::AgentRuntimeStatus { project_path, .. }
| Self::AgentConfirm { project_path, .. }
| Self::AgentResume { project_path }
| Self::AgentRun { project_path, .. } => Some((project_path, false)),
Self::LlmStatus | Self::RunnerStatus => None,
}
}
}
fn canonicalize_cli_path(
path: &Path,
label: &str,
allow_missing_leaf: bool,
) -> Result<PathBuf, String> {
if !path.is_absolute() {
return Err(format!("{label} 必须是绝对路径"));
}
match fs::canonicalize(path) {
Ok(path) => Ok(path),
Err(error) if allow_missing_leaf && error.kind() == std::io::ErrorKind::NotFound => {
let parent = path
.parent()
.ok_or_else(|| format!("{label} 缺少可解析的父目录"))?;
let file_name = path
.file_name()
.ok_or_else(|| format!("{label} 缺少目录名"))?;
let parent = fs::canonicalize(parent).map_err(|parent_error| {
format!(
"解析 {label} 父目录失败:{}: {parent_error}",
parent.display()
)
})?;
Ok(parent.join(file_name))
}
Err(error) => Err(format!("解析 {label} 失败:{}: {error}", path.display())),
}
}
pub(crate) fn prepare_cli_command_paths(
command: &mut CliCommand,
runtime_config_dir: Option<&Path>,
) -> Result<Option<PathBuf>, String> {
let project_path = if let Some((path, allow_missing_leaf)) = command.project_path_mut() {
let canonical = canonicalize_cli_path(path, "本地项目路径", allow_missing_leaf)?;
*path = canonical.clone();
Some(canonical)
} else {
None
};
let config_dir = runtime_config_dir
.map(|path| canonicalize_cli_path(path, "--config-dir", true))
.transpose()?;
if command.requires_external_agent_runner() && config_dir.is_none() {
return Err(
"Agent Runtime 写命令必须显式传入 --config-dir <项目外 AppData 绝对路径>".to_string(),
);
}
if let (Some(config_dir), Some(project_path)) = (&config_dir, &project_path) {
validate_game_creator_runtime_config_dir_outside_project(config_dir, project_path)?;
}
Ok(config_dir)
}
pub(crate) fn take_cli_runtime_config_dir(
args: &mut Vec<String>,
) -> Result<Option<PathBuf>, String> {
let positions = args
.iter()
.enumerate()
.filter_map(|(index, arg)| (arg == "--config-dir").then_some(index))
.collect::<Vec<_>>();
if positions.len() > 1 {
return Err("--config-dir 只能指定一次".to_string());
}
let Some(index) = positions.first().copied() else {
return Ok(None);
};
let value = args
.get(index + 1)
.map(String::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| "--config-dir 缺少目录路径".to_string())?;
let path = PathBuf::from(value);
if !path.is_absolute() {
return Err("--config-dir 必须是绝对路径".to_string());
}
args.drain(index..=index + 1);
Ok(Some(path))
}
pub(crate) fn game_creator_llm_status_lines(status: &GameCreatorLlmConfigStatus) -> Vec<String> {
let mut lines = vec![
format!("llm.configured={}", status.configured),
@@ -74,6 +215,68 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
if args.first().map(String::as_str) == Some("--llm-status") {
return Ok(Some(CliCommand::LlmStatus));
}
if args.first().map(String::as_str) == Some("--runner-status") {
if args.len() != 1 {
return Err("用法:--runner-status".to_string());
}
return Ok(Some(CliCommand::RunnerStatus));
}
if args.first().map(String::as_str) == Some("--agent-runtime-status") {
if args.len() != 3 {
return Err("用法:--agent-runtime-status <本地项目绝对路径> <agentId>".to_string());
}
return Ok(Some(CliCommand::AgentRuntimeStatus {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-confirm") {
if args.len() != 5 {
return Err(
"用法:--agent-confirm <本地项目绝对路径> <agentId> <runId> <actionId>".to_string(),
);
}
return Ok(Some(CliCommand::AgentConfirm {
project_path: PathBuf::from(&args[1]),
agent_id: args[2].trim().to_string(),
run_id: args[3].trim().to_string(),
action_id: args[4].trim().to_string(),
}));
}
if args.first().map(String::as_str) == Some("--agent-resume") {
if args.len() != 2 {
return Err("用法:--agent-resume <本地项目绝对路径>".to_string());
}
return Ok(Some(CliCommand::AgentResume {
project_path: PathBuf::from(&args[1]),
}));
}
if args.first().map(String::as_str) == Some("--agent-enqueue") {
let mut rest = args[1..].to_vec();
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
rest.remove(index);
true
} else {
false
};
if rest.len() < 4 {
return Err(
"用法:--agent-enqueue [--init] <本地项目绝对路径> <agentId> <runId> <任务>"
.to_string(),
);
}
let task = rest[3..].join(" ");
if task.trim().is_empty() {
return Err("Agent 任务不能为空".to_string());
}
return Ok(Some(CliCommand::AgentEnqueue {
project_path: PathBuf::from(&rest[0]),
agent_id: rest[1].trim().to_string(),
run_id: rest[2].trim().to_string(),
task: task.trim().to_string(),
initialize,
}));
}
if args.first().map(String::as_str) == Some("--agent-chat") {
let project_path = args.get(1).map(String::as_str).ok_or_else(|| {
"用法:--agent-chat <本地项目绝对路径> <agentId> <聊天内容>".to_string()
@@ -194,6 +397,8 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
task,
initialize,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
if initialize && !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
@@ -271,6 +476,87 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
))
}
}
CliCommand::AgentEnqueue {
project_path,
agent_id,
run_id,
task,
initialize,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
initialize_cli_agent_project(&project_path, initialize)?;
let runtime = start_game_creator_agent_background_task_at(
&project_path,
&agent_id,
&task,
&run_id,
)?;
println!("agent.enqueue.accepted");
println!("agentId={agent_id}");
println!("requestedRunId={run_id}");
println!(
"runtimeJson={}",
serde_json::to_string(&runtime)
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?
);
Ok(())
}
CliCommand::AgentRuntimeStatus {
project_path,
agent_id,
} => {
let runtime = read_game_creator_agent_runtime_at(&project_path, &agent_id)?;
println!(
"runtimeJson={}",
serde_json::to_string(&runtime)
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?
);
Ok(())
}
CliCommand::AgentConfirm {
project_path,
agent_id,
run_id,
action_id,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let runtime = confirm_game_creator_agent_runtime_task_at(
&project_path,
&agent_id,
&run_id,
&action_id,
"真实 E2E CLI 精确确认",
)?;
println!("agent.confirm.accepted");
println!(
"runtimeJson={}",
serde_json::to_string(&runtime)
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?
);
Ok(())
}
CliCommand::AgentResume { project_path } => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", false)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
let runtimes = resume_game_creator_agent_background_tasks_at(&project_path)?;
println!("agent.resume.accepted");
println!(
"runtimesJson={}",
serde_json::to_string(&runtimes)
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?
);
Ok(())
}
CliCommand::RunnerStatus => {
println!(
"runnerJson={}",
serde_json::to_string(&read_external_agent_runner_status())
.map_err(|error| format!("序列化 Agent Runner 状态失败:{error}"))?
);
Ok(())
}
CliCommand::AgentRun {
project_path,
prompt,
@@ -318,3 +604,23 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
}
}
}
fn initialize_cli_agent_project(project_path: &Path, initialize: bool) -> Result<(), String> {
if initialize && !project_path.join(".agent/manifest.json").is_file() {
let project_name = project_path
.file_name()
.and_then(|value| value.to_str())
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or("CLI Agent 项目");
init_local_game_project_at(
project_path,
&format!("cli-agent-{}", unix_millis()),
project_name,
)?;
}
if !project_path.join(".agent/manifest.json").is_file() {
return Err("项目尚未初始化;请先在 App 中创建项目,或显式传入 --init".to_string());
}
Ok(())
}
@@ -588,14 +588,9 @@ pub(crate) fn write_game_creator_app_config(
) -> Result<GameCreatorAppConfigView, String> {
let config = normalize_game_creator_app_config(config)?;
let path = writable_game_creator_config_path()?;
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|error| format!("创建客户端配置目录失败:{}: {error}", parent.display()))?;
}
let content = serde_json::to_string_pretty(&config)
.map_err(|error| format!("序列化客户端配置失败:{error}"))?;
fs::write(&path, format!("{content}\n"))
.map_err(|error| format!("保存客户端配置失败:{}: {error}", path.display()))?;
write_game_creator_config_atomically(&path, &format!("{content}\n"))?;
game_creator_app_config_view(load_game_creator_app_config()?)
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -42,22 +42,30 @@ use tauri_plugin_opener::OpenerExt;
// 用 #[cfg] 编译期门控:仅开发(debug)且非测试构建编入;生产 release 与 cargo test 下整体剔除。
mod agent;
mod assets;
mod browser;
mod cli;
mod commands;
mod config;
#[cfg(all(debug_assertions, not(test)))]
mod debug;
mod isolated_agent;
mod preview;
mod project;
mod repository_context;
mod runner;
mod windows;
use agent::*;
use assets::*;
use browser::*;
use cli::*;
use commands::*;
use config::*;
use isolated_agent::*;
use preview::*;
use project::*;
use repository_context::*;
use runner::*;
use windows::*;
#[derive(Debug, Eq, PartialEq, Serialize)]
@@ -1220,9 +1228,58 @@ struct GameCreatorAgentLoopResult {
}
fn main() {
let args = std::env::args().skip(1).collect::<Vec<_>>();
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
let runtime_config_dir = match take_cli_runtime_config_dir(&mut args) {
Ok(config_dir) => config_dir,
Err(error) => {
eprintln!("{error}");
std::process::exit(1);
}
};
if args.first().map(String::as_str) == Some("--agent-runner") {
if args.len() != 1 {
eprintln!("用法:--agent-runner --config-dir <AppData 绝对路径>");
std::process::exit(1);
}
let Some(config_dir) = runtime_config_dir else {
eprintln!("Agent Runner 必须显式传入 --config-dir <AppData 绝对路径>");
std::process::exit(1);
};
set_game_creator_runtime_config_dir(config_dir.clone());
if let Err(error) = run_external_agent_runner_server(config_dir) {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
return;
}
match parse_cli_command(&args) {
Ok(Some(command)) => {
Ok(Some(mut command)) => {
let config_dir =
match prepare_cli_command_paths(&mut command, runtime_config_dir.as_deref()) {
Ok(config_dir) => config_dir,
Err(error) => {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
};
if let Some(config_dir) = config_dir {
let configured = if command.is_read_only_status() {
configure_external_agent_runner_read_only(&config_dir)
} else {
configure_external_agent_runner(&config_dir)
};
if let Err(error) = configured {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
set_game_creator_runtime_config_dir(config_dir);
}
if command.requires_external_agent_runner() {
if let Err(error) = ensure_external_agent_runner_started() {
eprintln!("agent.runner.failed: {error}");
std::process::exit(1);
}
}
if let Err(error) = run_cli_command(command) {
eprintln!("agent.run.failed: {error}");
std::process::exit(1);
@@ -1242,6 +1299,24 @@ fn main() {
.manage(game_creator_preview_registry())
.setup(|app| {
configure_game_creator_runtime_config_dir(app.handle())?;
let config_dir = game_creator_runtime_config_dir().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
"客户端 AppData 配置目录未初始化",
)
})?;
configure_external_agent_runner(config_dir).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("配置 Agent Runner 失败:{error}"),
)
})?;
ensure_external_agent_runner_started().map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("启动 Agent Runner 失败:{error}"),
)
})?;
set_game_creator_agent_runtime_update_app_handle(app.handle().clone());
#[cfg(all(debug_assertions, not(test)))]
open_developer_window(app.handle())?;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+99 -2
View File
@@ -3294,6 +3294,101 @@ export function WorkspaceLauncher({
};
}, []);
useEffect(() => {
const invoke = resolveTauriInvoke();
const pendingRun = agentChatPendingRuntimeRun;
if (!invoke || !pendingRun) {
return;
}
let disposed = false;
let inFlight = false;
const pollRuntime = async () => {
if (disposed || inFlight) {
return;
}
inFlight = true;
try {
const runtime = await invoke<AgentRuntimeResult>(
'read_game_creator_agent_runtime',
{
projectPath: pendingRun.projectPath,
agentId: pendingRun.agentId,
...(pendingRun.sessionId
? { sessionId: pendingRun.sessionId }
: {}),
},
);
if (
disposed ||
agentChatPendingRuntimeRunRef.current?.runId !== pendingRun.runId ||
agentChatProjectPathRef.current.trim() !== pendingRun.projectPath ||
agentChatSelectedAgentIdRef.current !== pendingRun.agentId
) {
return;
}
const runtimeState = agentRuntimeStateFromResult(runtime);
const tracksPendingRun =
runtimeState.runId === pendingRun.runId ||
(runtimeState.recentTasks ?? []).some(
(task) => task.runId === pendingRun.runId,
);
if (!tracksPendingRun) {
return;
}
if (
pendingRun.sessionId !== null &&
runtimeState.sessionId !== pendingRun.sessionId
) {
return;
}
setAgentChatRuntime((current) =>
normalizeAgentRuntimeState(runtimeState, current),
);
if (
agentChatActiveSessionIdRef.current === null ||
runtimeState.sessionId === agentChatActiveSessionIdRef.current
) {
setAgentChatActiveRuntime((current) =>
normalizeAgentRuntimeState(runtimeState, current),
);
}
setAgentChatRuntimeError('');
setAgentChatStatus(agentRuntimeConversationStatus(runtimeState));
if (
isAgentRuntimeTerminalState(runtimeState) &&
!agentChatRuntimeSyncingRunIdsRef.current.has(pendingRun.runId)
) {
const syncConversation = agentChatRuntimeSyncConversationRef.current;
if (!syncConversation) {
return;
}
agentChatRuntimeSyncingRunIdsRef.current.add(pendingRun.runId);
void syncConversation(invoke, pendingRun).finally(() => {
agentChatRuntimeSyncingRunIdsRef.current.delete(pendingRun.runId);
});
}
} catch (error) {
if (!disposed) {
setAgentChatRuntimeError(
`Runtime 状态刷新失败:${
error instanceof Error ? error.message : String(error)
}`,
);
}
} finally {
inFlight = false;
}
};
void pollRuntime();
const timer = window.setInterval(() => {
void pollRuntime();
}, 750);
return () => {
disposed = true;
window.clearInterval(timer);
};
}, [agentChatPendingRuntimeRun]);
useEffect(() => {
let disposed = false;
void getClientProfileDashboard()
@@ -12608,11 +12703,13 @@ function isProjectPolicyConfirmableCommandId(value: string) {
'agent.retry',
'agent.resume',
'agent.delegate',
'agent.spawn_isolated',
'agent.schedule_ready',
'agent.audit',
'agent.trace_read',
'preview.status',
'preview.start',
'preview.validate',
'preview.open',
'preview.stop',
'canvas.project_sync',
@@ -16990,7 +17087,7 @@ export function App() {
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
@@ -17053,7 +17150,7 @@ export function App() {
...current,
{
role: 'assistant',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
},
]);
return;
@@ -20465,7 +20465,7 @@ describe('AI 游戏创作 App 界面边界', () => {
expect(
await screen.findByText(
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.spawn_isolated、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.validate、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
),
).not.toBeNull();
expect(screen.queryByText('project.policy_write')).toBeNull();
@@ -4167,3 +4167,24 @@
- 决策:`agent.delegate` 子任务必须 durable 保存 `parentAgentId / parentRunId / delegationId`,其中 `delegationId` 从已持久化工具动作的 `actionId` 派生,不能使用执行时随机值;终态任务记录必须保存经过统一凭据清洗和安全截断的 `terminalDetail`,不能依赖可能被后续 run 覆盖的 Agent 全局 state。子任务进入 `completed / failed / cancelled / budget-exhausted` 任一终态后,Runtime 必须在 delegation 级 OS 文件锁内按固定 receipt runId 幂等生成且至多生成一次 `agent.delegate.result` 回执;不同委派并发写同一目标 Agent 时,runId 分配与 pending 追加还必须在目标 Agent 任务账本 OS 锁内原子完成。失败、排队或活跃取消、预算耗尽与成功同等需要回执,`needs-reconciliation` 只有最终取消后才回执。父 Agent 通过既有队列接收 `source=agent-delegate-receipt` 的续跑任务,回执 prompt 禁止重复同一委派,并携带完整的已清洗 `terminalDetail`,不能只保留 UI 摘要;排队期间不提前写入父会话,真正执行时才幂等落盘,用户消息或回执消息落盘失败时不得进入 LLM。回执任务必须保留父 run 关联,真正开始或恢复前再次核验父 run,关联缺失或父 run 不存在时失败关闭;父 run 已取消或普通失败时只保留 suppressed receipt 审计,不自动复活。父 Session 存在未结束委派时禁止切换或归档,极端竞态下回执回落到父 Agent 当前可写 Session。续跑继续遵守同 Agent FIFO、per-Agent OS 锁、权限确认、取消、恢复和 `needs-reconciliation` 屏障,不允许直接重入、插队或重复投递;恢复必须先恢复 pending action / reconciliation 屏障,再补齐“子终态已落盘、回执未入队”的崩溃窗口。
- 决策:Agent loop 的语义事件类型固定为 `thinking_summary / plan / action / observation / response / error`。普通失败和预算耗尽必须先追加统一 `error` 事件,同时保留 `turn.failed / turn.budget_exhausted` 生命周期事件供旧读取方兼容;状态、phase 和清洗后的错误详情必须在两类事件中一致。Runtime 状态面板默认展示最新 4 条事件,但在当前后端最近事件窗口大于 4 条时必须允许展开全部返回记录,不能让 `plan`、早期 observation 或 thinking summary 永久不可见。
- 验证:Rust 覆盖首轮上下文不泄露、工具后 observation 可见、六类语义事件、确认前后内容边界、前后台同 Agent 串行、前台结束后队列 drain、恢复确认 gate、预算耗尽失败、默认工具白名单一致性,以及 delegate 成功 / 失败 / 取消 / 预算耗尽终态回执、`delegationId` 幂等去重和父 Agent receipt 续跑仍受 FIFO / 锁 / 确认 / 恢复门禁;revision / verification gate 还要覆盖修改前推进、失败不回退、`requiresVerification` 单向持久化、验证只绑定当前 revision、v1 context bundle / pending action 恢复失败关闭、修改 run 与只读 run 的 stale 回复不落盘、跨 Agent 漂移在同 run 自愈、stale context 重启恢复、动态验证输出不能绕过 stall、stall 跨 revision / restart 保持,以及最终 assistant / completed 在项目写锁内复核后才落盘;finalization 还要覆盖三个阶段边界的崩溃窗口恢复、`prepared` 后取消或 revision / gate 漂移丢弃、journal 损坏或身份冲突阻断并显示 `finalizing`、conversation `messageId` / audit 自愈与并发不重复,以及终态投影 / receipt 不重放;前端覆盖统一事件展示、主工作区和独立开发 Agent 聊天窗口的默认恢复确认条与显式恢复 command。
## 2026-07-12 AI 游戏创作 Agent Runtime V1.1 通用开发能力
- 决策:Runtime 首轮从“完全不预加载项目内容”调整为注入固定预算的 repository startup context。自动内容仅限有界目录/manifest/验证脚本摘要、适用 `AGENTS.md`、根 `CONTEXT.md`/README 来源和安全 Git 摘要;任意源码、对话、资产和私有记忆正文仍经工具读取。仓库文本是不可信输入,不能提升权限或覆盖 Runtime 安全规则。
- 决策:后台执行所有权迁入同一发布二进制的 `--agent-runner` 模式。Runner 从显式 AppData 配置目录读取密钥,通过带协议版本、requestId 和私有 token 的 loopback 本地协议接收唤醒;`.agent/runtime/**` 继续是事实源。App 退出后 Runner 可继续任务,整机重启后仍需按 `agent.resume` 策略显式恢复。
- 决策:跨进程运行前先把 Agent DB、conversation、events/tasks、activity/output 和 Session catalog 的临界区升级为进程内锁加 OS 文件锁;配置写入使用同目录原子替换。事件只作为刷新提示,断线后重读 snapshot。
- 决策:新增 `preview.validate`,只验证当前授权项目的精确 loopback 预览,不接受任意 URL/JavaScript/Profile。工具用隔离 Chrome/Edge CDP 采集桌面/移动截图、DOM/console/network 和 canvas 非空证据,证据只落 `.agent/runtime/browser-validations`;它不替代 `project.verify``game.static_smoke`
- 决策:保留静态 `agent.delegate`,新增批量 `agent.spawn_isolated`。角色/Provider/策略按 `templateAgentId`,队列/锁/session/run/private memory 按 Runtime 生成的 `instanceId`;单次最多 3 个、深度 1、writeScopes 不得重叠,全部 child 终态后只产生一个幂等 join continuation。
- 决策:新增仓库外配置的真实 Provider opt-in 验收。通过依据固定为 task/event/agent.db、文件、revision、verification、finalization、conversation、结构化 join 和浏览器证据;模型最终文本不作为通过证据,缺关键外部配置时报告 `BLOCKED`,不得 skip 后记为通过。
- 2026-07-12 安全修正:所有 Runtime 写 CLI 必须显式使用项目外 AppData 并交给独立 Runner`--runner-status` 保持只读。AppData、endpoint 和锁文件必须校验 owner/权限;Runner、Agent lane 和项目 execution-owner 安全打开时拒绝符号链接、硬链接和 Windows reparse point。同一项目的 execution-owner 跨 AppData 唯一并绑定 `bootId / protocolVersion`,不同 Runner 不能同时接管同一项目。
- 2026-07-12 安全修正:项目索引和 checkpoint 统一排除敏感配置、`.agent`、VCS、依赖及构建目录;checkpoint manifest 路径必须是唯一规范相对路径,create/diff/restore 均复用项目安全路径解析。通用文件工具不得访问 `.agent/checkpoints/**`,restore 不得覆盖或删除本地敏感配置。
- 2026-07-12 修正:动态隔离 all-join 使用独立持久交付记录。固定 `joinRunId` 至多入队一次;原父 run 通过 `agent.run_status` 认领 ready join 时,必须持久标记并取消未执行 continuation;只有父 run 未认领时才在 lane 释放后执行唯一 continuation,恢复不得生成 `-dup-*` join run 或重复调用父 LLM。
- 2026-07-12 安全修正:Windows AppData、Runner endpoint、AppData lock 和 execution-owner 诊断文件使用禁止继承且只允许当前用户 SID 的 protected DACL;只读状态入口只校验,不创建目录、不收紧权限。项目 execution-owner 的 OS 锁文件与 JSON 诊断投影分离,Runtime 通过稳定目录句柄先取得系统锁,再原子修复缺失、截断或损坏的诊断,诊断内容不得作为接管依据。
- 2026-07-12 安全修正:通用文件、项目索引和 checkpoint 使用同一可移植相对路径语法,拒绝 Windows 盘符 / UNC / ADS、尾随点或空格、保留设备名和大小写碰撞;快照额外排除私钥、数据库和 dump。进入 prompt 的绝对路径脱敏同时覆盖 Unix、Windows 盘符和 UNC 路径。
- 2026-07-12 安全修正:`preview.validate` 只使用系统固定安装位置的 Chrome / Chromium / Edge,并在导航前通过 CDP Fetch request-stage 拦截覆盖全部资源。除精确预览 HTTP origin 及同 host / port WebSocket 外,跨 origin HTTP、redirect target、WebSocket 和其他端口必须在连接前阻断。
- 2026-07-12 修正:父 run 认领 ready all-join 必须绑定当前持久化 `agent.run_status``actionId`;同一 action 重试幂等返回,其他 action 不得重复消费,已开始的 continuation 不得被迟到认领覆盖。
- 2026-07-12 修正:所有 durable 工具动作执行前复核 repository fingerprint;规范漂移会作废旧动作并回到同 run planning,整个 `.agent` 控制面不参与 fingerprint,避免 checkpoint、日志和 Runtime 状态制造伪漂移。
- 2026-07-12 修正:动态隔离 instance 使用自己的 Runtime 私有临时 memory lane,只允许 `scope=agent` 读写本人,拒绝 sibling 和项目 / Session / 黑板共享写入;父任务进入 failed、budget-exhausted 或 cancelled 时必须幂等取消全部非终态 children。
- 2026-07-12 修正:`preview.validate` 必须固定同时生成 desktop / mobile 证据,每个视口都要有可见且至少两种 RGBA 状态的 canvas;单视口、无 canvas、透明或均匀纯色画布不能通过。
- 验证:真实 `gpt-5.5``llm-runtime` 套件已通过 Runner 强杀恢复、11 条合法工具协议、4 套完整确认生命周期、5 个零重放副作用动作、9 次结构化成功工具执行、checkpoint/修改、项目验证、Chrome 桌面与移动取证、3 个隔离实例和唯一 join;终态投影与 assistant audit 唯一,action/message/receipt 重复为 0,密钥与诱饵泄露为 0。未配置 External Editor API 时 `full` 套件按契约返回 `BLOCKED(editorApi)`
- 详细契约与验收矩阵见 `docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`
@@ -60,10 +60,10 @@ npm run agc
开发侧需要无 UI 验收某个单 Agent 的完整 Runtime 时使用:
```bash
npm run ai-game-creator-shell:agent-task -- --init /absolute/project code-prototype "修复失败测试并完成验证"
npm run ai-game-creator-shell:agent-task -- --config-dir /absolute/app-data --init /absolute/project code-prototype "修复失败测试并完成验证"
```
省略 `--init` 时项目必须已经由客户端初始化遇到权限确认会返回非零并保留待确认动作,继续操作应回到开发窗口,不能用 CLI 静默绕过。
省略 `--init` 时项目必须已经由客户端初始化。所有 Runtime 写命令都必须显式传入项目外 `--config-dir` 并投递给独立 Runner`--runner-status` 和 Agent 状态查询只读取已有配置与 endpoint,不得创建 AppData、修改权限或为了查询启动 Runner。遇到权限确认会返回非零并保留待确认动作,继续操作应回到开发窗口,不能用 CLI 静默绕过。
`npm run agc` 会启动 Tauri 开发客户端;其 `beforeDevCommand` 通过 `npm run agc:serve` 先完成壳 typecheck,再启动或复用配套 SpacetimeDB、`api-server` 和固定 `127.0.0.1:3080` Vite。只需要浏览器预览同一客户端时可用 `npm run agc:serve`;只启动配套后端和数据库时可用 `npm run agc:backend -- --database <name>`
@@ -14,6 +14,7 @@
| 创作入口、草稿架和玩法链路 | `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` |
| 创作流程统一阶段计划 | `docs/planning/【玩法创作】创作流程统一总计划-2026-05-30.md` |
| 宿主壳、移动 App、桌面 App 与 AI H5 沙箱边界 | `docs/【前端架构】宿主壳能力统一协议-2026-06-17.md``docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md` |
| AI 游戏创作独立 App、Agent Runtime、Runner、浏览器验证与动态隔离子 Agent | `docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md``docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md` |
| 本地启动、验证、部署、埋点和运营查询 | `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` |
| 微信小程序虚拟支付 | `docs/【技术方案】微信虚拟支付接入-2026-05-26.md` |
| UI 像素资产与 9-slice 规范 | `UI_CODING_STANDARD.md` |
@@ -0,0 +1,250 @@
# AI 游戏创作 Agent Runtime V1.1 技术方案
更新时间:`2026-07-12`
## 目标
在不引入第二套任务系统、不开放任意 shell、不复制现有 Runtime 的前提下,优先补齐五项通用开发能力:
1. 仓库启动上下文与代码理解。
2. 脱离 WebView / Tauri UI 生命周期的独立持久 Runner。
3. 面向当前本地项目预览的真实浏览器验证。
4. 动态创建、隔离运行、并行执行并统一回收结果的子 Agent。
5. 使用真实 Provider 覆盖计划、工具、修改、确认、验证、持久化和重启恢复的自动验收。
本方案是现有 [`【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`](./【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md) 的 Runtime V1.1 扩展,不建立平行 App、平行队列或平行持久化真相。
## 不在本轮
- 任意 shell、PTY、远程命令执行和通用进程管理。
- 完整 Git 分支、提交、合并和 worktree 工具。
- 云端 Runner、跨机器任务迁移或无人值守开机自启。
- RAG、CodeGraph、tree-sitter 或语言服务器作为发布依赖。
- 允许 Agent 访问任意网址、执行任意 JavaScript 或复用用户浏览器 Profile。
- 用动态子 Agent 绕过项目权限、项目写锁、revision 或验证门禁。
## 总体结构
```text
WebView / CLI
|
| Tauri command + RunnerClient
v
AppData runner endpoint (protocol + port + private token)
|
| 127.0.0.1 length-prefixed JSON
v
同一发布二进制 --agent-runner
|
+-- .agent/runtime 任务、恢复、确认、终态账本
+-- repository startup context
+-- preview.validate / Chrome CDP
+-- isolated child-agent lanes and join receipts
```
`.agent/runtime/**` 继续是任务执行事实源。Tauri command 负责用户授权、精确确认和 UI 桥接;Runner 是 LLM、工具循环、队列 drain、恢复和浏览器验证的唯一执行者。事件只作为刷新提示,UI 断线重连后必须重新读取完整 Runtime snapshot。
## 1. 仓库启动上下文
### 契约
新增 `RepositoryStartupContext`,每个 run 在首次 planning 前确定性装配,恢复时按相同规则重建。正文不混入普通 observation,也不被 6 轮压缩窗口删除;context bundle 只保存 schema、来源清单和 fingerprint。
启动上下文最多包含:
- 有界顶层目录摘要、文件/目录数量和是否截断。
- `package.json``Cargo.toml``pyproject.toml``go.mod` 等 manifest 摘要。
- 可验证脚本名和原始命令摘要;执行仍必须走 `project.verify`
- 根目录及嵌套目录中的 `AGENTS.md` 路径和有界正文,按根到具体目录叠加。
-`CONTEXT.md`、README 的有界背景摘要和项目记忆来源列表。
- 固定安全 Git 摘要:是否为仓库、当前分支、tracked dirty 数;不开放任意 Git 参数。
- 常见入口文件候选和按扩展名聚合的语言分布。
任意源码正文、对话、资产、私有记忆和黑板正文仍必须通过已有工具按需读取。仓库文本一律标记为不可信项目输入,只能约束项目做法,不能提升工具权限、覆盖 Runtime 系统规则或自动批准动作。
### 预算与安全
- 最多扫描 10,000 个目录项、2,000 个候选文件、深度 12。
- 跳过符号链接、`.git`、整个 `.agent` 控制面、依赖、构建、缓存和敏感配置目录;任务图、会话、记忆、checkpoint 和 Runtime 证据只能通过专用工具读取,不能参与仓库指纹。
- 单规范文件最多 24 KiB,全部规范正文最多 64 KiBprompt 渲染最多 20 KiB。
- 不读取 `.env*`、运行时配置、认证文件、Cookie、Token 或数据库 dump。
- 每个 durable 工具动作在真正执行前都要重建并复核 repository fingerprint;发现适用规范或启动上下文漂移后,旧动作必须落为 `blocked` observation,跳过本轮剩余动作并在同一 run 下一轮 planning 重新确认。创建 checkpoint、写 Runtime 日志等 `.agent` 控制面变化不得制造伪漂移。
- 项目索引、checkpoint、diff 和 restore 使用同一敏感路径排除规则,额外排除私钥、数据库及 dump 文件;checkpoint manifest 中每个文件路径必须是可移植的规范相对路径,拒绝绝对路径、`..`、反斜杠、盘符 / UNC / ADS 别名、控制字符、Windows 非法字符、尾随点或空格、保留设备名和符号链接边界。manifest 路径按 Windows 大小写不敏感键去重,禁止 `State.txt``state.txt` 同时出现。通用文件工具不能读写 `.agent/checkpoints/**`restore 不覆盖或删除 `.env*`、运行配置、认证文件、私钥、数据库、dump、`.agent`、VCS、依赖和构建目录。
`project.index` 升级为显式刷新和查看完整启动摘要的工具;自动启动上下文使用同一构建核心,不建立第二份索引。
## 2. 独立持久 Runner
### 进程模式
同一 Tauri 发布二进制新增:
```text
--agent-runner --config-dir <AppData 目录>
```
Runner 从显式 AppData 目录读取 `game-creator.config.json`API Key 不进入 IPC payload、项目目录、状态文件、日志或验收报告。Unix 使用新 session,Windows 使用新进程组和无窗口标志,使 Runner 不依附 WebView 生命周期。
### 本地协议
- Runner 只监听随机 `127.0.0.1` 端口。
- AppData endpoint 文件权限收紧为当前用户,保存 `protocolVersion / pid / bootId / port / token / heartbeatAt`
- 请求使用 `u32` 长度前缀加 UTF-8 JSON,单帧最多 1 MiB。
- 每个请求必须携带私有 token、`requestId` 和协议版本。
- 首版方法:`runner.ping``runner.status``runtime.wake_pending``runtime.resume``runtime.continue_action``runner.shutdown_if_idle`
- 只读失败可重试;写请求先按 requestId/runId 重读事实源,禁止网络重试制造重复任务。
### 执行所有权
- App 写入精确用户输入、任务或确认账本后只通知 Runner;不再在 App 进程中 claim 后台任务。
- Runner 复用现有恢复顺序:finalization -> pending action -> recoverable task -> delegate/join receipt reconciliation。
- 同 Agent 继续持有 per-Agent OS 锁,不同 Agent/子 Agent lane 可并行。
- Runner 崩溃后,现有 `executing -> needs-reconciliation`、finalization 幂等和 cancel tombstone 语义保持不变。
- 整机重启后不自动执行;App/CLI 下次启动并经过 `agent.resume` 策略后恢复。
### 跨进程前置修复
- `.agent/agent.db`、conversation、events、tasks、activity、output 和 Session catalog 的读改写临界区升级为进程内 Mutex + OS 文件锁。
- App 配置写入改为同目录临时文件 + 原子替换,Runner 只读取完整版本。
- Runner endpoint、项目 execution-owner 和协议主版本共同阻止 split-brain。
- 所有会写 Runtime 的 CLI 命令必须显式传入项目外 AppData,不能回退到 CLI 进程内执行;`--runner-status` 只读已有 endpoint,不得为了查询状态启动 Runner。
- AppData 在 Unix 上必须由当前用户持有且权限为 `0700`endpoint/lock 为 `0600`。Windows 下 AppData 目录、Runner endpoint、AppData Runner lock 和 execution-owner 诊断文件使用禁止继承的 protected DACL,只允许当前用户 SID;目录 ACE 带对象 / 容器继承,文件 ACE 不带继承。写入口可以收紧后复核,只读状态入口只能校验,不能创建、收紧或修复。
- Runner lock、Agent lane lock 和项目 execution-owner 均拒绝符号链接、硬链接或 Windows reparse pointUnix 通过 `openat` 逐级无跟随打开,Windows 持有逐级目录句柄,取得稳定句柄和 OS 锁后才允许写诊断信息。
- 项目 execution-owner 是项目级跨进程系统锁,不按 AppData 分叉;不同 AppData 启动的 Runner 不能同时接管同一项目。`.agent/runtime/execution-owner.lock` 只承载句柄级排他所有权,`execution-owner.json` 是可恢复诊断投影:诊断缺失、截断或损坏不能阻止合法锁持有者恢复,也不能作为接管依据;新诊断经临时文件写入、同步、原子替换和回读全等校验,仅在旧完整记录可信时写入 `recoveredFromBootId`
## 3. 浏览器验证
### 工具
新增 `preview.validate`,不新增通用 `browser.open`
输入只允许:
```json
{
"viewports": ["desktop", "mobile"],
"expectedText": ["可选可见文本"],
"settleMs": 800,
"failOnConsoleError": true
}
```
工具不接受 URL、脚本、请求头、Cookie 或浏览器 Profile。Runtime 只验证当前项目 `PreviewRegistry` 中的精确 loopback URL;独立 Runner 未持有持久预览时,可启动同一项目的临时预览并在验证后关闭。
### 证据
通过系统 Chrome/Chromium/Edge 的 CDP 采集:
- `document.readyState`、title、可见文本摘要和 DOM 字符数。
- console error/warn、未捕获异常和失败网络请求。
- canvas 尺寸、可见面积和非空像素抽样;每个视口至少存在一个可见 canvas,且抽样必须同时包含非透明像素和至少两种 RGBA 状态,全透明或均匀纯色画布不能通过。
- `desktop 1280x720``mobile 390x844` PNG 截图。
`viewports` 必须且只能同时包含 `desktop``mobile`;单视口、重复视口、空 body、无 canvas 或空白 canvas 都是失败,不允许用成功摘要掩盖缺失证据。
证据写入 `.agent/runtime/browser-validations/<agentId>/<runId>/<revision>/`。observation 和 LLM 只接收脱敏计数、诊断摘要和项目内相对路径,不接收截图二进制或完整 DOM。
### 安全
- 只从操作系统固定安装位置发现 Chrome / Chromium / Edge,不搜索 `PATH`。仅允许精确 `http://127.0.0.1:<port>` 预览 origin;导航前开启覆盖全部资源的 CDP Fetch request-stage 拦截,网络只放行同 origin HTTP 和同 host / port WebSocket。其他端口、HTTP(S)、WSS、跨 origin redirect target 和 WebSocket 必须在连接或握手前阻断并记为致命策略失败;浏览器默认走不可达代理,只为精确 HTTP / WS origin 配置 bypass。
- 使用全新临时 Profile;不添加 `--no-sandbox`
- 验证前后重读 project revision 和预览身份,任一漂移都使结果失效。
- `preview.validate` 是独立试玩证据,不替代修改后的 `project.verify``game.static_smoke` 完成门禁。
## 4. 动态隔离子 Agent
### 工具契约
保留 `agent.delegate` 的静态规范 Agent 语义,新增 `agent.spawn_isolated`
```json
{
"children": [
{
"templateAgentId": "code-prototype",
"task": "完成一个边界清晰的子任务",
"acceptanceCriteria": ["可验证条件"],
"expectedArtifacts": ["game/feature-a/**"],
"writeScopes": ["game/feature-a/**"]
}
],
"joinMode": "all"
}
```
模型不能指定执行实例 ID。Runtime 从父 actionId 稳定派生:
- `delegationGroupId = sha256(parentActionId)`
- `delegationId = sha256(groupId + childIndex)`
- `instanceId = child-<delegationId 前缀>`
- 每个实例使用独立 session、run、queue、state、event、lock 和私有临时记忆。
### 隔离与限制
- 角色 prompt、LLM 配置和 Agent 策略继承 `templateAgentId`;执行和持久化 lane 使用 `instanceId`
- 单次最多 3 个并行实例,最大深度 1。
- sibling `writeScopes` 不得重叠;所有真实写入仍通过项目级写锁、revision 和 verification gate。
- 子实例默认拒绝 `agent.spawn_isolated``project.restore``agent.schedule_ready`
- 子实例的 `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 状态页可读取其状态。
### Join 与结构化结果
每个 child 终态生成结构化结果:
```text
delegationId, instanceId, templateAgentId, runId, status,
summary, artifacts[path, sha256], evidence[], verifiedRevision, error
```
同一 group 全部终态后,Runtime 使用 `.agent/runtime/isolated-agents/join-deliveries/<groupId>.json` 持久记录交付状态,并且只允许固定 `joinRunId` 入队一次。父 run 仍持有自身 lane 时,只能通过当前已持久化 `agent.run_status` 工具动作的 `actionId` 认领 ready all-join;交付记录保存 `claimedByActionId`,同一 action 崩溃重试可幂等重读,同一 run 的其他 action 不再看到该 join。认领后取消尚未执行的固定 continuationcontinuation 已开始时拒绝认领。父 run 未认领时,唯一 continuation 才在 lane 释放后执行,`dispatched -> claimed-by-parent / suppressed` 单向不可逆。恢复、并发 child 终态和重复状态查询都不能重新开放交付、生成 `-dup-*` join run 或重复调用父 LLM。
## 5. 真实 Provider 验收
新增 opt-in 命令:
```bash
npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite full
```
验收在系统临时目录创建带 disposable sentinel 的项目,配置和凭据始终留在仓库外。通过条件只采信持久化事实,不采信模型自述。
完整套件至少证明:
1. 首轮拿到 repository startup context,且没有泄露敏感文件。
2. 真实 Provider 返回合法工具计划,协议记录为 `native_function` 或受支持的 `text_json` 回退。
3. 执行 checkpoint、读取、精确 patch/write,并在确认策略下停到 `waiting-for-confirmation`
4. 精确确认后由独立 Runner 继续,App/发起 CLI 退出不影响任务。
5. 修改后通过 `project.verify`,再通过 `preview.validate` 生成桌面和移动证据。
6. 两个相同模板实例和一个不同模板实例真实并行,最后只产生一个 join continuation。
7. Runner 强制终止后按原 run/session 恢复,不重复工具、消息、receipt 或最终 assistant。
8. task/event/agent.db、revision、verification、finalization、conversation 和浏览器报告一致。
9. 对项目、stdout、报告和 Runtime 元数据执行已加载密钥扫描,`secretLeakCount=0`
`full` 套件缺少 LLM、受支持浏览器或 External Editor API 配置时必须报告 `BLOCKED`,不能以 skip 计为通过。默认 CI 继续运行本地假 Provider smoke;真实套件不进入无凭据 CI。
### 2026-07-12 真实验收结果
使用发布 AppData 中已配置的真实 `gpt-5.5` 运行 `llm-runtime` 套件,结果为 `PASS`:Runner 强制终止后恢复同一 run/session;共形成 85 条 task、143 条 event、127 条 Agent DB 审计、11 条合法工具协议记录和 9 次结构化成功工具执行;4 个确认动作的 waiting / required / approved / ok 生命周期各自唯一,5 个副作用动作的新 actionId 重放为 0。项目 revision 为 1,项目验证与 1 组桌面/移动浏览器验证通过;3 个隔离实例来自 2 个模板且只形成 1 个 joincompleted 投影、最终 assistant 及其 conversation audit 均仅 1 条,重复 action/message/receipt 均为 0;已加载密钥和项目诱饵泄露均为 0;保留项目核对完成后按 disposable sentinel 清理。
同一 AppData 的 `full` 套件返回 `BLOCKED(editorApi)`,原因是未配置 External Editor API。该结果属于正确的外部前置条件阻断,不记为通过,也不复用其他环境凭据。
### 安全与负向验收
- repository fingerprint 测试覆盖自动 `file.write / file.patch / file.delete / project.restore` 在 planning 后规范漂移时全部阻断,并覆盖 `.agent` checkpoint / 日志 / Runtime 控制面变化不产生伪漂移。
- 隔离测试覆盖两个同模板 instance 的私有记忆互不可见、共享记忆写入拒绝、父任务三类终态取消 children、重复 reconcile 不重开 join,以及已开始 continuation 不可被父 run 迟到认领。
- 浏览器纯逻辑测试覆盖单视口、重复视口、无 canvas、透明 canvas 和均匀纯色 canvas 失败;显式真实 Chrome 测试同时证明桌面/移动截图有效,跨 origin HTTP redirect 与 WebSocket 目标在发送前零连接。
- Runner 测试覆盖只读状态无副作用、锁链接 / 硬链接拒绝、跨 AppData 唯一 owner,以及缺失、截断或损坏 owner 诊断在 OS 锁后原子恢复。
## 验收命令
- `npm run ai-game-creator-shell:typecheck`
- `npm run test -- apps/ai-game-creator-shell/tests`
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`
- `cargo test -p platform-agent --manifest-path server-rs/Cargo.toml game_creation`
- `cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app`
- `npm run ai-game-creator-shell:agent-run:smoke`
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite full`
- `npm run check:encoding`
- `git diff --check`
@@ -16,6 +16,12 @@
## Runtime 边界
2026-07-12 起,通用开发能力的 Runtime V1.1 增量以 [`【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`](./【技术方案】AI游戏创作Agent%20Runtime%20V1.1-2026-07-12.md) 为编码级事实源。它补充仓库启动上下文、同一发布二进制独立 Runner、受限本地预览浏览器验证、动态隔离子 Agent 和真实 Provider 全链路验收;本文件中“进程内 tokio task”“首轮不预加载项目内容”和“不创建动态执行实例”的旧口径由 V1.1 明确替代,未涉及能力继续沿用本文件。
2026-07-12 真实验收:发布 AppData 中的真实 Provider 已通过强化后的 `llm-runtime` 套件,覆盖 Runner 强杀恢复、仓库上下文、checkpoint/精确修改/四段确认生命周期、项目验证、桌面与移动非空画布证据、3 个隔离实例并行和唯一 all-join;工具协议、副作用判重、终态投影、assistant audit、消息、回执和密钥泄露均以结构化落盘事实验收。`full` 套件因当前 AppData 未配置 External Editor API 正确返回 `BLOCKED(editorApi)`,不得记为通过。
以下能力清单保留 Runtime V1 的演进记录;其中“App 进程内 tokio task”“跨进程同项目写入不作为支持目标”和“恢复到当前 App 进程”的旧描述均已由 V1.1 替代。当前边界是 App / CLI 只落账并唤醒同一发布二进制的独立 Runnerappend-only JSONL 使用进程内锁加 OS 文件锁,恢复继续由 Runner 接管同一 run / session。
Agent Runtime 负责:
- 2026-07-12 安全边界补充:`project.verify` 的 script 最多 160 个字符,固定使用系统 script shell,并在解析和执行前拒绝项目级 `.npmrc` 改写 npm 语义。Runtime context bundle 绑定 `projectId / agentId / taskId / sessionId / runId / source / task`,结尾换行计入 64 KiB 上限;恢复时同时校验 `nextLoopIndex`、context window、当前窗口已完成轮数、观察指纹、计划、observation 数量,以及 `contextStalled` 只能位于非零上下文窗口边界。bundle 通过项目内无符号链接路径原子写入,并从同一文件句柄最多读取 64 KiB;项目路径和常见 `sk- / GitHub / npm / AWS / JWT` 凭据统一脱敏。工具 observation 进入 context checkpoint 后才删除 `observed-*` 动作账本,避免重启时旧 ledger 抢占有效 bundle。
+1
View File
@@ -136,6 +136,7 @@
"ai-game-creator-shell:agent-task": "npm --prefix apps/ai-game-creator-shell run agent-task --",
"ai-game-creator-shell:agent-run": "npm --prefix apps/ai-game-creator-shell run agent-run --",
"ai-game-creator-shell:agent-run:smoke": "npm --prefix apps/ai-game-creator-shell run agent-run:smoke",
"ai-game-creator-shell:agent-runtime:real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:real-e2e --",
"ai-game-creator-shell:typecheck": "npm --prefix apps/ai-game-creator-shell run typecheck",
"ai-game-creator-shell:check": "npm run ai-game-creator-shell:typecheck && npm run test -- apps/ai-game-creator-shell/tests && cargo test -p platform-llm --manifest-path server-rs/Cargo.toml && cargo test -p platform-agent --manifest-path server-rs/Cargo.toml game_creation && cargo test -p shared-contracts --manifest-path server-rs/Cargo.toml game_creation_app && cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml && npm run ai-game-creator-shell:agent-run:smoke",
"check:native-shells": "node scripts/check-native-shells.mjs"
@@ -70,6 +70,11 @@ describe('AI 游戏创作 App 共享契约', () => {
(command) => command.id === 'agent.delegate',
)?.permission,
).toBe('confirm');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'agent.spawn_isolated',
)?.permission,
).toBe('confirm');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'agent.capabilities',
@@ -103,6 +108,11 @@ describe('AI 游戏创作 App 共享契约', () => {
(command) => command.id === 'preview.status',
)?.permission,
).toBe('auto');
expect(
GAME_CREATION_APP_COMMANDS.find(
(command) => command.id === 'preview.validate',
)?.permission,
).toBe('auto');
expect(
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'file.read')
?.permission,
@@ -163,6 +173,10 @@ describe('AI 游戏创作 App 共享契约', () => {
'task-graph-agenda',
'evaluator-repair-routing',
'tool-call-budget',
'isolated-subagents',
'repository-startup-context',
'browser-validation',
'persistent-runner',
'multi-agent-collaboration',
'role-level-collaboration',
'repair-loop-carryover',
@@ -34,6 +34,7 @@ export const GAME_CREATION_APP_COMMANDS = [
{ id: 'agent.retry', permission: 'confirm' },
{ id: 'agent.resume', permission: 'confirm' },
{ id: 'agent.delegate', permission: 'confirm' },
{ id: 'agent.spawn_isolated', permission: 'confirm' },
{ id: 'agent.schedule_ready', permission: 'confirm' },
{ id: 'agent.capabilities', permission: 'auto' },
{ id: 'agent.audit', permission: 'auto' },
@@ -48,6 +49,7 @@ export const GAME_CREATION_APP_COMMANDS = [
{ id: 'asset.upload', permission: 'confirm' },
{ id: 'asset.register', permission: 'confirm' },
{ id: 'preview.start', permission: 'confirm' },
{ id: 'preview.validate', permission: 'auto' },
{ id: 'preview.open', permission: 'confirm' },
{ id: 'preview.stop', permission: 'auto' },
{ id: 'preview.status', permission: 'auto' },
@@ -107,6 +109,11 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
area: 'agent-runtime',
title: '组内角色协作',
},
{
id: 'isolated-subagents',
area: 'agent-runtime',
title: '动态隔离子 Agent',
},
{ id: 'quality-review', area: 'agent-runtime', title: '质量评审' },
{ id: 'run-lifecycle', area: 'agent-runtime', title: 'Run 生命周期控制' },
{
@@ -124,13 +131,24 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
{ id: 'local-artifacts', area: 'local-runtime', title: '本地产物保存' },
{ id: 'project-checkpoints', area: 'local-runtime', title: '项目快照与恢复' },
{ id: 'project-index', area: 'local-runtime', title: '本地项目索引' },
{
id: 'repository-startup-context',
area: 'local-runtime',
title: '仓库启动上下文',
},
{ id: 'local-preview', area: 'local-runtime', title: '本地 HTTP 预览' },
{
id: 'browser-validation',
area: 'local-runtime',
title: '浏览器试玩验证',
},
{
id: 'canvas-project-sync',
area: 'local-runtime',
title: '画板项目资源同步',
},
{ id: 'developer-window', area: 'dev-runtime', title: '开发窗口' },
{ id: 'persistent-runner', area: 'dev-runtime', title: '独立持久 Runner' },
{ id: 'guardrails', area: 'dev-runtime', title: '权限 Gate' },
{ id: 'project-policy', area: 'dev-runtime', title: '项目级权限策略' },
{ id: 'trace-log', area: 'dev-runtime', title: '执行日志' },

Some files were not shown because too many files have changed in this diff Show More