完善Agent持久进程会话能力
新增Runner托管的PTY持久进程会话与安全输出游标 接入start、poll、stdin、terminate的权限、审计和完成门禁 强化Linux进程组看护、Windows Job回收与崩溃核对语义 补齐Runtime确定性测试和真实Provider双套件验收 同步共享契约、实施方案与长期项目记忆
This commit is contained in:
File diff suppressed because it is too large
Load Diff
+100
@@ -497,6 +497,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
|
||||
|
||||
[[package]]
|
||||
name = "chromiumoxide"
|
||||
version = "0.9.1"
|
||||
@@ -942,6 +948,12 @@ dependencies = [
|
||||
"tendril",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
|
||||
|
||||
[[package]]
|
||||
name = "dpi"
|
||||
version = "0.1.2"
|
||||
@@ -1137,6 +1149,17 @@ dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filedescriptor"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"thiserror 1.0.69",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@@ -1432,6 +1455,7 @@ dependencies = [
|
||||
"libc",
|
||||
"platform-agent",
|
||||
"platform-llm",
|
||||
"portable-pty",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1446,6 +1470,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"unicode-normalization",
|
||||
"url",
|
||||
"windows-sys 0.61.2",
|
||||
"zip",
|
||||
]
|
||||
|
||||
@@ -2208,6 +2233,12 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libappindicator"
|
||||
version = "0.9.0"
|
||||
@@ -2458,6 +2489,18 @@ version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -3017,6 +3060,27 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60f6ce597ecdcc9a098e7fddacb1065093a3d66446fa16c675e7e71d1b5c28e6"
|
||||
|
||||
[[package]]
|
||||
name = "portable-pty"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 1.3.2",
|
||||
"downcast-rs",
|
||||
"filedescriptor",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"nix",
|
||||
"serial2",
|
||||
"shared_library",
|
||||
"shell-words",
|
||||
"winapi",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "postscript"
|
||||
version = "0.14.1"
|
||||
@@ -3719,6 +3783,17 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serial2"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9eb6ea5562eeaed6936b8b54e086aa0f88b9e5b1bef45beb038e2519fa1185b1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serialize-to-javascript"
|
||||
version = "0.1.2"
|
||||
@@ -3780,6 +3855,22 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
@@ -5736,6 +5827,15 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
||||
@@ -17,6 +17,7 @@ sha2 = "0.10"
|
||||
similar = "2.7"
|
||||
platform-llm = { path = "../../../server-rs/crates/platform-llm" }
|
||||
platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
||||
portable-pty = "0.9"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
|
||||
shared-contracts = { path = "../../../server-rs/crates/shared-contracts", default-features = false }
|
||||
tauri = { version = "2.11.2", features = [] }
|
||||
@@ -30,3 +31,6 @@ zip = { version = "2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
libc = "0.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_JobObjects"] }
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,14 @@ pub(crate) struct ProjectCommandSpec {
|
||||
pub(crate) verification_eligible: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ProjectCommandLaunchSpec {
|
||||
pub(crate) executable: PathBuf,
|
||||
pub(crate) arguments: Vec<String>,
|
||||
pub(crate) cwd: PathBuf,
|
||||
pub(crate) environment: Vec<(OsString, OsString)>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct ProjectCommandResult {
|
||||
pub(crate) command_id: String,
|
||||
@@ -943,7 +951,7 @@ fn project_command_git_path_is_sensitive(value: &str) -> bool {
|
||||
|| file_name.ends_with(".key")
|
||||
}
|
||||
|
||||
fn project_command_actual_arguments(spec: &ProjectCommandSpec) -> Vec<String> {
|
||||
pub(crate) fn project_command_actual_arguments(spec: &ProjectCommandSpec) -> Vec<String> {
|
||||
match spec.program.as_str() {
|
||||
"npm" => std::iter::once("--ignore-scripts".to_string())
|
||||
.chain(spec.arguments.iter().cloned())
|
||||
@@ -1013,6 +1021,113 @@ fn project_command_actual_arguments(spec: &ProjectCommandSpec) -> Vec<String> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_project_command_launch_spec(
|
||||
root: &Path,
|
||||
spec: &ProjectCommandSpec,
|
||||
) -> Result<ProjectCommandLaunchSpec, ProjectCommandError> {
|
||||
let isolated_home = resolve_local_project_path(root, ".agent/runtime/command-env/home")
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
|
||||
let isolated_tmp = resolve_local_project_path(root, ".agent/runtime/command-env/tmp")
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
|
||||
let isolated_cache = resolve_local_project_path(root, ".agent/runtime/command-env/cache")
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
|
||||
fs::create_dir_all(&isolated_home)
|
||||
.and_then(|()| fs::create_dir_all(&isolated_tmp))
|
||||
.and_then(|()| fs::create_dir_all(&isolated_cache))
|
||||
.map_err(|error| {
|
||||
ProjectCommandError::new(
|
||||
ProjectCommandErrorStage::Preflight,
|
||||
format!("创建 command.exec 隔离目录失败:{error}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut environment = vec![
|
||||
(OsString::from("CI"), OsString::from("1")),
|
||||
(OsString::from("NO_COLOR"), OsString::from("1")),
|
||||
(OsString::from("FORCE_COLOR"), OsString::from("0")),
|
||||
(OsString::from("TERM"), OsString::from("dumb")),
|
||||
(OsString::from("HOME"), isolated_home.as_os_str().to_owned()),
|
||||
(
|
||||
OsString::from("USERPROFILE"),
|
||||
isolated_home.as_os_str().to_owned(),
|
||||
),
|
||||
(
|
||||
OsString::from("TMPDIR"),
|
||||
isolated_tmp.as_os_str().to_owned(),
|
||||
),
|
||||
(OsString::from("TEMP"), isolated_tmp.as_os_str().to_owned()),
|
||||
(OsString::from("TMP"), isolated_tmp.as_os_str().to_owned()),
|
||||
(
|
||||
OsString::from("CARGO_HOME"),
|
||||
isolated_cache.join("cargo").into_os_string(),
|
||||
),
|
||||
(OsString::from("CARGO_NET_OFFLINE"), OsString::from("true")),
|
||||
(OsString::from("CARGO_TERM_COLOR"), OsString::from("never")),
|
||||
(OsString::from("npm_config_audit"), OsString::from("false")),
|
||||
(OsString::from("npm_config_fund"), OsString::from("false")),
|
||||
(
|
||||
OsString::from("npm_config_ignore_scripts"),
|
||||
OsString::from("true"),
|
||||
),
|
||||
(OsString::from("npm_config_offline"), OsString::from("true")),
|
||||
(
|
||||
OsString::from("npm_config_update_notifier"),
|
||||
OsString::from("false"),
|
||||
),
|
||||
(
|
||||
OsString::from("npm_config_cache"),
|
||||
isolated_cache.join("npm").into_os_string(),
|
||||
),
|
||||
(
|
||||
OsString::from("npm_config_userconfig"),
|
||||
isolated_home.join("empty-user.npmrc").into_os_string(),
|
||||
),
|
||||
(OsString::from("GIT_CONFIG_NOSYSTEM"), OsString::from("1")),
|
||||
(
|
||||
OsString::from("GIT_CONFIG_GLOBAL"),
|
||||
isolated_home.join("empty-gitconfig").into_os_string(),
|
||||
),
|
||||
(OsString::from("GIT_PAGER"), OsString::from("cat")),
|
||||
(OsString::from("GIT_EXTERNAL_DIFF"), OsString::new()),
|
||||
(OsString::from("PAGER"), OsString::from("cat")),
|
||||
(
|
||||
OsString::from("HTTP_PROXY"),
|
||||
OsString::from("http://127.0.0.1:9"),
|
||||
),
|
||||
(
|
||||
OsString::from("HTTPS_PROXY"),
|
||||
OsString::from("http://127.0.0.1:9"),
|
||||
),
|
||||
(
|
||||
OsString::from("ALL_PROXY"),
|
||||
OsString::from("http://127.0.0.1:9"),
|
||||
),
|
||||
(OsString::from("NO_PROXY"), OsString::new()),
|
||||
(OsString::from("PATH"), spec.safe_path.clone()),
|
||||
];
|
||||
for name in ["SystemRoot", "PATHEXT", "RUSTUP_HOME"] {
|
||||
if let Some(value) = std::env::var_os(name) {
|
||||
environment.push((OsString::from(name), value));
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
if let Some(system_root) = std::env::var_os("SystemRoot") {
|
||||
environment.push((
|
||||
OsString::from("ComSpec"),
|
||||
PathBuf::from(system_root)
|
||||
.join("System32/cmd.exe")
|
||||
.into_os_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ProjectCommandLaunchSpec {
|
||||
executable: spec.executable.clone(),
|
||||
arguments: project_command_actual_arguments(spec),
|
||||
cwd: spec.cwd.clone(),
|
||||
environment,
|
||||
})
|
||||
}
|
||||
|
||||
fn configure_project_command_process_group(command: &mut tokio::process::Command) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -1145,7 +1260,7 @@ async fn collect_project_command_output_task(
|
||||
}
|
||||
}
|
||||
|
||||
fn project_command_source_fingerprint(root: &Path) -> Result<String, String> {
|
||||
pub(crate) fn project_command_source_fingerprint(root: &Path) -> Result<String, String> {
|
||||
validate_project_root(root)?;
|
||||
let mut entries_seen = 0usize;
|
||||
let mut files_seen = 0usize;
|
||||
@@ -1247,66 +1362,18 @@ async fn run_project_command_process(
|
||||
root: &Path,
|
||||
spec: &ProjectCommandSpec,
|
||||
) -> Result<ProjectCommandProcessResult, ProjectCommandError> {
|
||||
let isolated_home = resolve_local_project_path(root, ".agent/runtime/command-env/home")
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
|
||||
let isolated_tmp = resolve_local_project_path(root, ".agent/runtime/command-env/tmp")
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
|
||||
let isolated_cache = resolve_local_project_path(root, ".agent/runtime/command-env/cache")
|
||||
.map_err(|error| ProjectCommandError::new(ProjectCommandErrorStage::Preflight, error))?;
|
||||
fs::create_dir_all(&isolated_home)
|
||||
.and_then(|()| fs::create_dir_all(&isolated_tmp))
|
||||
.and_then(|()| fs::create_dir_all(&isolated_cache))
|
||||
.map_err(|error| {
|
||||
ProjectCommandError::new(
|
||||
ProjectCommandErrorStage::Preflight,
|
||||
format!("创建 command.exec 隔离目录失败:{error}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut command = tokio::process::Command::new(&spec.executable);
|
||||
let launch = prepare_project_command_launch_spec(root, spec)?;
|
||||
let mut command = tokio::process::Command::new(&launch.executable);
|
||||
command
|
||||
.args(project_command_actual_arguments(spec))
|
||||
.current_dir(&spec.cwd)
|
||||
.args(&launch.arguments)
|
||||
.current_dir(&launch.cwd)
|
||||
.env_clear()
|
||||
.env("CI", "1")
|
||||
.env("NO_COLOR", "1")
|
||||
.env("FORCE_COLOR", "0")
|
||||
.env("HOME", &isolated_home)
|
||||
.env("USERPROFILE", &isolated_home)
|
||||
.env("TMPDIR", &isolated_tmp)
|
||||
.env("TEMP", &isolated_tmp)
|
||||
.env("TMP", &isolated_tmp)
|
||||
.env("CARGO_HOME", isolated_cache.join("cargo"))
|
||||
.env("CARGO_NET_OFFLINE", "true")
|
||||
.env("CARGO_TERM_COLOR", "never")
|
||||
.env("npm_config_audit", "false")
|
||||
.env("npm_config_fund", "false")
|
||||
.env("npm_config_ignore_scripts", "true")
|
||||
.env("npm_config_offline", "true")
|
||||
.env("npm_config_update_notifier", "false")
|
||||
.env("npm_config_cache", isolated_cache.join("npm"))
|
||||
.env(
|
||||
"npm_config_userconfig",
|
||||
isolated_home.join("empty-user.npmrc"),
|
||||
)
|
||||
.env("GIT_CONFIG_NOSYSTEM", "1")
|
||||
.env("GIT_CONFIG_GLOBAL", isolated_home.join("empty-gitconfig"))
|
||||
.env("GIT_PAGER", "cat")
|
||||
.env("GIT_EXTERNAL_DIFF", "")
|
||||
.env("PAGER", "cat")
|
||||
.env("HTTP_PROXY", "http://127.0.0.1:9")
|
||||
.env("HTTPS_PROXY", "http://127.0.0.1:9")
|
||||
.env("ALL_PROXY", "http://127.0.0.1:9")
|
||||
.env("NO_PROXY", "")
|
||||
.env("PATH", &spec.safe_path)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
for name in ["SystemRoot", "ComSpec", "PATHEXT", "RUSTUP_HOME"] {
|
||||
if let Some(value) = std::env::var_os(name) {
|
||||
command.env(name, value);
|
||||
}
|
||||
for (name, value) in &launch.environment {
|
||||
command.env(name, value);
|
||||
}
|
||||
configure_project_command_process_group(&mut command);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ mod image_inspect;
|
||||
mod isolated_agent;
|
||||
mod patchset;
|
||||
mod preview;
|
||||
mod process_session;
|
||||
mod project;
|
||||
mod repository_context;
|
||||
mod runner;
|
||||
@@ -73,6 +74,7 @@ use image_inspect::*;
|
||||
use isolated_agent::*;
|
||||
use patchset::*;
|
||||
use preview::*;
|
||||
use process_session::*;
|
||||
use project::*;
|
||||
use repository_context::*;
|
||||
use runner::*;
|
||||
@@ -1250,6 +1252,16 @@ struct GameCreatorAgentLoopResult {
|
||||
|
||||
fn main() {
|
||||
let mut args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
#[cfg(target_os = "linux")]
|
||||
if is_process_session_child_mode(&args) {
|
||||
match run_process_session_child(&args) {
|
||||
Ok(exit_code) => std::process::exit(exit_code),
|
||||
Err(error) => {
|
||||
eprintln!("process.session.child.failed: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
let runtime_config_dir = match take_cli_runtime_config_dir(&mut args) {
|
||||
Ok(config_dir) => config_dir,
|
||||
Err(error) => {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3837,6 +3837,9 @@ impl Default for ProjectPermissionPolicy {
|
||||
}
|
||||
}
|
||||
|
||||
const PROJECT_PERMISSION_MANDATORY_CONFIRM_COMMANDS: &[&str] =
|
||||
&["command.start", "command.stdin", "command.terminate"];
|
||||
|
||||
pub(crate) fn read_project_permission_policy_at(
|
||||
root: &Path,
|
||||
) -> Result<ProjectPermissionPolicyView, String> {
|
||||
@@ -3890,6 +3893,19 @@ pub(crate) fn normalize_project_permission_policy(
|
||||
) -> Result<ProjectPermissionPolicy, String> {
|
||||
policy.denied_commands = normalize_policy_command_ids(policy.denied_commands)?;
|
||||
policy.confirm_commands = normalize_policy_command_ids(policy.confirm_commands)?;
|
||||
for command_id in PROJECT_PERMISSION_MANDATORY_CONFIRM_COMMANDS {
|
||||
if !policy
|
||||
.denied_commands
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
&& !policy
|
||||
.confirm_commands
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
{
|
||||
policy.confirm_commands.push((*command_id).to_string());
|
||||
}
|
||||
}
|
||||
policy
|
||||
.confirm_commands
|
||||
.retain(|command| !policy.denied_commands.contains(command));
|
||||
|
||||
@@ -2310,6 +2310,9 @@ fn external_agent_runner_directory_has_durable_files(path: &Path) -> Result<bool
|
||||
}
|
||||
|
||||
fn external_agent_runner_root_is_idle(root: &Path) -> Result<bool, String> {
|
||||
if crate::has_active_process_sessions_at(root)? {
|
||||
return Ok(false);
|
||||
}
|
||||
for durable_dir in [
|
||||
root.join(".agent/runtime/pending-actions"),
|
||||
root.join(".agent/runtime/finalizations"),
|
||||
@@ -2475,6 +2478,7 @@ pub(crate) fn run_external_agent_runner_server(config_dir: impl AsRef<Path>) ->
|
||||
set_external_agent_runner_config_dir(config_dir.clone());
|
||||
|
||||
let boot_id = random_identifier(b"genarrative-agent-runner-boot-id")?;
|
||||
crate::initialize_process_session_boot_id(&boot_id)?;
|
||||
let token = random_identifier(b"genarrative-agent-runner-token")?;
|
||||
let _instance_lock = acquire_external_agent_runner_instance_lock(
|
||||
&external_agent_runner_lock_path(&config_dir),
|
||||
@@ -2557,10 +2561,14 @@ pub(crate) fn run_external_agent_runner_server(config_dir: impl AsRef<Path>) ->
|
||||
while state.active_connections.load(Ordering::Acquire) > 0 && Instant::now() < worker_deadline {
|
||||
thread::sleep(EXTERNAL_AGENT_RUNNER_LOOP_INTERVAL);
|
||||
}
|
||||
let process_shutdown = crate::shutdown_all_process_sessions_and_wait(Duration::from_secs(3));
|
||||
if let Some(error) = server_error {
|
||||
Err(error)
|
||||
Err(match process_shutdown {
|
||||
Ok(()) => error,
|
||||
Err(process_error) => format!("{error};{process_error}"),
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
process_shutdown
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4268,3 +4268,15 @@
|
||||
- 修正:`agent.run_status` 的 ready all-join 结果作为安全 milestone 跨窗口保留;认领后的后续状态查询显式返回 `claimedIsolatedJoins` 和“不要为同一组重复查询”。这避免 `scope=all` 的 900 字符静态 Agent 状态截断、上下文压缩后丢失 reviewer 结果并持续轮询。
|
||||
- 修正:context compaction 把最新成功 `agent.action_history` 作为受保护观察保留,避免后续只读噪声把唯一动作回查证据挤出最终 bundle;terminal receipt 仍是长期事实源。
|
||||
- 验证:Tauri 定向用例覆盖精确 `sourceActionId`、ready/claimed all-join、跨两窗口 milestone 和动作历史保护。真实 `gpt-5.5` `llm-runtime` 最终 PASS:146 条 task、248 条 event、255 条 Agent DB、16 条工具协议、15 次代表性成功工具执行、7 套确认、8 个实际副作用 action、43 条 receipt;2 次 `command.output_read` 均早于唯一动作历史查询,Runner 强杀恢复身份稳定,唯一 patchset、失败/成功命令、项目/浏览器/视觉验证和 3 个隔离 reviewer 均完成。副作用重放、重复 action/message/receipt、正文/图片/密钥/诱饵泄漏均为 0。
|
||||
|
||||
## 2026-07-14 AI 游戏创作 Agent Runtime V1.10 Runner-owned 持久进程会话
|
||||
|
||||
- 决策:新增 `command.start / command.poll / command.stdin / command.terminate` 四个模型工具,专门承载受控前台持久进程。start / stdin / terminate 默认 `confirm`,poll 默认 `auto`。`command.start` 复用 `command.exec` 的固定 program、逐项 argv、项目 cwd、白名单解析、安全 PATH、隔离环境和参数拒绝,不接受 shell、环境注入、用户 executable、管道、重定向或 daemonize / detach;Runner 直接持有固定 `120x30` PTY、child handle、stdin writer 和输出泵,同项目最多 4 个、同 Agent instance 最多 2 个 running session。V1.2 对 PTY / 后台进程的排除只适用于一次性 `command.exec`。
|
||||
- 决策:`processId` 是 start action create-once 的 opaque Runtime 身份,完整绑定 project、Agent instance、task、session、run、start action、action / command fingerprint 和 Runner boot;它不是 OS PID。poll / stdin / terminate 每次都从活 registry 和 durable record 交叉复核 owning 身份,跨 Agent、动态 sibling、run 或项目一律失败关闭,不能把知道 ID 当成授权。
|
||||
- 决策:App / WebView / 发起 CLI 退出不影响会话,独立 Runner 继续持有 PTY。Runner 重启只做 reconciliation:旧 boot 已进入 prepared / launching / running / terminating 且没有可信 terminal record 的会话进入 `needs-reconciliation`,不得重放 start 或 stdin,不得重发 terminate,也不得按持久化 PID 重连或接管 PTY;可信终态只补 observation / audit / receipt。首版连旧 boot 的 prepared 也保守核对,不自动推断为安全重试。
|
||||
- 决策:`command.poll` 使用绑定 processId 的 opaque cursor,并以 `maxChars / waitMs` 分页读取保留逻辑行边界的清洗后私有 PTY transcript;默认 / 最大返回 8,000 / 16,000 字符,最长等待 30 秒,同一 action/cursor 恢复必须稳定。后台输出泵独立等待 child 并排空尾部,单会话清洗后输出上限为 256 KiB,超限终止并落 `output-limit-exceeded`。输出正文只进入 owning Agent 的私有 transcript、observation 和 context bundle,task/event/Agent DB/receipt/action history/activity/output/UI snapshot/report 只保存 cursor、字节数、SHA-256、截断和退出元数据。`command.stdin` 单次最终 UTF-8 bytes 上限 8 KiB,支持 `appendNewline / eof`,是不可重放副作用;公共确认与审计只留 `processId / bytesWritten / contentSha256 / stdinOpen / eof`,不得保存 data、摘要、前后缀或可逆编码。
|
||||
- 决策:owning run 存在 launching / running / terminating 或未解决 reconciliation 会话时,final reply、finalization journal 和 completed 投影全部阻断。`runner.shutdown_if_idle` 同时检查活 registry、输出泵、终止任务和 durable unresolved record;取消 run 也必须先完成进程收束,不能留下会话后把 Runner 判 idle。
|
||||
- 决策:terminate 必须携带最后一次 poll cursor,并返回同一 cursor 的零消费状态元数据;后续 poll 不得从 0 重读或跳过尾部。Unix 固定为 graceful request + 完整固定宽限等待、随后只 force kill 同组残留、再 wait / reap / drain PTY;Windows 首版使用 Job force terminate + wait / reap,不宣称已有等价 graceful console event。只发送信号不算完成;signal / Job / wait / reap 或终态审计无法确认都进入 reconciliation。重复 terminate 只幂等返回已知终态,不能按 PID 再杀一次。
|
||||
- 决策:容量预检同时扫描 registry 与 durable active / reconciliation record;未解决旧 boot 会话禁止新 start,同项目 4 / 同 Agent 2 的拒绝发生在 revision 推进和 OS spawn 前。终态 record 的 `needsReconciliation=true` 即使 status 为 failed / terminated 也继续阻止 final 和 idle,可信终态落盘后从 registry 清理。
|
||||
- 安全边界:Linux child wrapper 监测 owning Runner parent PID,Runner 强杀后 fail-closed 杀死同一前台进程组;Windows 使用 kill-on-close Job Object。它们只提供默认同组 / 同 Job 生命周期,不是 OS sandbox,也不能阻止主动 `setsid`、外部 service、读取当前用户可读宿主文件或绕过代理联网。当前仍没有容器、namespace、seccomp、macOS sandbox profile 或 Windows restricted token / AppContainer;实现、UI 和报告不得宣称达到 Codex CLI 级沙箱或主动逃逸下的完整进程树隔离。
|
||||
- 验收:真实 `gpt-5.5` `process-session` 在无工具配方任务中完成 1 次 start、3 次连续 cursor poll、1 次 stdin 和 1 次 terminate;41 条 task、75 条 event、63 条 Agent DB、8 条 receipt、4 套确认生命周期、唯一 completed / assistant,fixture launch=1,终态 PID / 端口、重放、重复、公共正文 / 密钥 / 诱饵泄漏均为 0。独立 `process-session-runner-kill` 在 readiness 后强杀 owning Runner;21 条 task、34 条 event、36 条 Agent DB,新 boot 保持原 run / session,只产生 1 条 reconciliation,launch=1、PID reconnect / completed / assistant / 重放 / 泄漏均为 0。两个 disposable 项目均按 sentinel 清理。
|
||||
|
||||
@@ -92,6 +92,36 @@ npm run ai-game-creator-shell:typecheck
|
||||
|
||||
第一条覆盖固定程序 / argv 拒绝规则、输出清洗、超时和源码改写检测;第二条覆盖全局 / per-Agent 配置继承,第三条覆盖 `default / low / medium / high` 到 Provider 请求的映射;后两条覆盖共享 `confirm` 契约、配置结构和发布默认 `high`。模块级定向验证通过后,再按改动范围运行 `npm run ai-game-creator-shell:check`、`npm run check:encoding` 和 `git diff --check`。
|
||||
|
||||
### AI 游戏创作 Runtime V1.10 持久进程定向复验
|
||||
|
||||
V1.10 的 PTY 只通过四个 Runner-owned 工具开放;不要把 V1.2 `command.exec` 改成长驻入口。最小工具输入保持结构化:
|
||||
|
||||
```json
|
||||
{"tool":"command.start","input":{"program":"npm","args":["run","dev"],"cwd":".","timeoutSeconds":300}}
|
||||
{"tool":"command.poll","input":{"processId":"proc-...","cursor":"v1:proc-...:0","maxChars":8000,"waitMs":1000}}
|
||||
{"tool":"command.stdin","input":{"processId":"proc-...","data":"q","appendNewline":true,"eof":false}}
|
||||
{"tool":"command.terminate","input":{"processId":"proc-...","cursor":"v1:proc-...:254"}}
|
||||
```
|
||||
|
||||
定向复验按以下顺序取证:
|
||||
|
||||
1. 用 Runner-owned PTY fixture 覆盖 start、增量 poll、stdin、自然退出和 terminate;确认 `processId` 绑定完整 owning project / Agent / task / session / run / start action / Runner boot,而不是 OS PID。
|
||||
2. 让发起 App / CLI 在 start 后退出,确认 Runner 仍持有会话;随后分别在 launch、running、stdin 和 graceful / force 终止窗口强杀 Runner,确认恢复只进入 reconciliation,不增加 fixture launch / stdin 次数,也不按 PID 重连。
|
||||
3. terminate 必须携带最后一次 poll 的 `nextCursor`,返回同一 cursor 且不消费输出;活会话和 unresolved reconciliation 期间尝试 finalization 与 `runner.shutdown_if_idle`,必须分别被完成门禁和 busy 状态阻断;终止成功必须同时满足 child terminal、同组残留清理、wait / reap 和 PTY drain。
|
||||
4. 使用不同静态 Agent、动态 sibling、run 和项目重放同一 `processId`,全部必须失败关闭。扫描 task、event、Agent DB、receipt、action history、activity/output、UI snapshot 和报告,PTY 输出正文命中数必须为 0,stdin 只能出现 `bytesWritten / contentSha256 / stdinOpen / eof`。
|
||||
5. Linux 用忽略 SIGHUP 的 npm / Node fixture 验证 Runner 强杀后 owner watchdog 回收同一前台进程组;Windows 验证 kill-on-close Job Object。仍要明确 PTY、固定 argv、隔离环境和两阶段终止不是容器或 OS sandbox,主动 `setsid` / 外部 service 仍不在完整隔离承诺内。
|
||||
|
||||
实现用例统一使用可检索的 `process_session_` 前缀。先运行定向 Rust 用例,再跑 Tauri 全量和真实 Provider:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml process_session_ -- --nocapture
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml
|
||||
npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite process-session
|
||||
npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite process-session-runner-kill
|
||||
```
|
||||
|
||||
定向命令必须实际匹配到 V1.10 用例,`0 tests` 不算通过。真实 Provider fixture 不得把工具顺序、processId、readiness 文本所在 chunk 或 OS PID 写进任务提示;验收器只按持久 action identity、fixture 计数、私有输出和公共泄漏扫描判定。三项门禁实际通过后才能把日期、Provider、数量和 PASS 结果写入技术方案或 decision log;未运行或被外部配置阻断时只记录 `BLOCKED` / 未验收事实。
|
||||
|
||||
`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>`。
|
||||
|
||||
Linux 多用户共享同一台机器开发时,本地 dev 脚本会为当前 Linux 用户分配一个固定端口段并写入系统级注册表 `/var/tmp/genarrative-dev-port-ranges/registry.json`,自动分配从 `10000-10099` 开始,每段 100 个端口,四个 dev 服务依次使用 `start` 到 `start + 3`。可用 `GENARRATIVE_DEV_PORT_RANGE` 或 `npm run dev -- --port-range` 手动指定端口段用于特殊场景;注册表会阻止不同用户使用相同或重叠段,并让同一用户后续启动继续复用自己已占用的固定段。该机制只在 Linux 生效,Windows 仍沿用原有端口探测与漂移逻辑。
|
||||
|
||||
@@ -2853,3 +2853,27 @@
|
||||
- 处理:租约持有 `Arc<SpacetimeConnectionPool>` 并实现 `Drop` 统一复位槽位/归还连接;槽位改 `AtomicBool` CAS 抢占,删除自旋循环(持有 permit 必然命中空闲槽位)。任何新的"显式归还"资源在 async 取消语义下都要先想 Drop 兜底。
|
||||
- 验证:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`(`dropped_lease_releases_slot_and_permit`、`acquire_times_out_at_pool_acquire_when_pool_is_busy`)。
|
||||
- 关联:`server-rs/crates/spacetime-client/src/lib.rs`、`docs/【后端架构】SpacetimeDB连接池租约Drop兜底与取消安全-2026-06-11.md`。
|
||||
|
||||
## 持久进程恢复不能重放 start 或按 PID 重连
|
||||
|
||||
- 现象:Runner 强杀或重启后,同一个 run 又启动了一份开发服务器,或者新 Runner 根据旧 PID 把宿主上的同号进程误认成原 PTY 会话。
|
||||
- 原因:把 durable process record 当成活 OS handle,或在 spawn 前没有同步写入 `launching`,导致恢复逻辑无法区分“尚未启动”和“已经尝试启动”;OS PID 会复用,也不包含项目、Agent、run、action 和 Runner boot 身份。
|
||||
- 处理:`processId` create-once 绑定完整 Runtime 身份,`prepared / launching` 必须先于 OS spawn 持久化。旧 boot 下 prepared / launching / running / terminating 且缺少可信 terminal record 时只进入 `needs-reconciliation`;不重放 start / stdin / terminate,不探测或接管旧 PID / PTY。首版不自动推断旧 prepared 为安全重试。
|
||||
- 验证:在 launch 前后、running、stdin 写入后和两阶段 terminate 中分别强杀 Runner;fixture 的 launch / stdin 计数保持 1,恢复后没有 PID reconnect、没有 final,`runner.shutdown_if_idle` 仍报告 busy / reconciliation。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/runner.rs`、`apps/ai-game-creator-shell/src-tauri/src/command_exec.rs`。
|
||||
|
||||
## PTY 输出与 stdin 正文不能进入公共 Runtime 持久面
|
||||
|
||||
- 现象:模型能正常 poll 进程输出,但 task/event、Agent DB、receipt、动作历史、activity/output、UI snapshot 或验收报告里也出现了终端正文;或者 stdin challenge 被确认摘要、inputSummary、错误日志保存为明文。
|
||||
- 原因:直接复用普通 tool observation / pending action 的通用序列化,或为了排障把 PTY chunk 和 stdin data 整段复制进审计。持久进程正文可能包含密钥、绝对路径、交互输入和第三方进程回显,不能只依赖事后清洗。
|
||||
- 处理:PTY 原始字节经控制序列、UTF-8、凭据和绝对路径清洗后,必须显式恢复清洗器裁掉的逻辑换行,再只进入 owning Agent 的私有 transcript、observation 和 context。公共持久面只保留 cursor、字节数、SHA-256、截断、状态和退出元数据;stdin 正文只允许存在于执行所需的私有 pending action,终态后删除,审计仅保留 `bytesWritten / contentSha256 / stdinOpen / eof`,禁止前后缀、摘要和可逆编码。
|
||||
- 验证:fixture 同时输出唯一 sentinel、绝对路径和诱饵密钥,并发送唯一 stdin challenge;私有 poll 能读取清洗后结果,所有公共文件和报告的正文命中数为 0,stdin 只命中字节数和 SHA-256。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/command_output.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。
|
||||
|
||||
## PTY 会话未终态时不能 final 或关闭 Runner
|
||||
|
||||
- 现象:Agent 回复“服务已在后台运行”后 run 被记成 completed,随后 `runner.shutdown_if_idle` 关闭 Runner;或 terminate 只发出一次信号就宣告成功,留下仍存活的 child / grandchild。
|
||||
- 原因:完成门禁只检查 pending tool action,没有把 live process registry、输出泵、终止任务和 unresolved reconciliation 纳入 active work;或者 terminate 为取状态从 offset 0 偷读一个字符并返回新 cursor,诱导后续 poll 重读 / 跳过;同时把 PTY / process group 错当成完整 OS sandbox 和可靠进程树隔离。
|
||||
- 处理:launching / running / terminating 与任意 status 上的 `needsReconciliation=true` 全部阻止 final reply、finalization journal、completed 和 idle shutdown。terminate 携带最后一次 poll cursor 并返回同一 cursor 的零消费元数据;Unix 完成完整 graceful wait 后只 force kill 同组残留,再 wait / reap / drain PTY,Windows 首版使用 Job force terminate + wait / reap;任何 signal / Job / wait 阶段无法确认都保持 reconciliation。Linux wrapper 监测 owner PID 并在 Runner 强杀后 kill 当前前台进程组,Windows 使用 kill-on-close Job Object;取消 run 也走同一收束路径。主动 `setsid` / 外部 service 和 OS sandbox 仍不在承诺内。
|
||||
- 验证:活会话下 finalization 和 `runner.shutdown_if_idle` 必须失败关闭;分别验证 graceful handler 尾部输出、宽限超时后的 force、忽略 SIGHUP 的 npm 孙进程和 Windows Job 路径,只有 child 已终态、同组残留已处理且 PTY 尾部排空才出现唯一 terminal record。另用允许程序证明代理和固定 cwd 不是文件系统 / 网络沙箱,不得把该现象误写成测试失败或安全能力。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/runner.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# AI 游戏创作 Agent Runtime V1.1 技术方案
|
||||
|
||||
更新时间:`2026-07-13`
|
||||
更新时间:`2026-07-14`
|
||||
|
||||
## 目标
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
## 不在本轮
|
||||
|
||||
- 任意 shell、PTY、远程命令执行和通用进程管理。
|
||||
- 任意 shell、远程命令执行和通用进程管理。V1.1-V1.9 的 `command.exec` 不提供 PTY 或后台进程;V1.10 仅按本文件对应章节开放 Runner-owned 受控前台 PTY 会话,不开放 shell、用户指定可执行文件或 detached 子进程。
|
||||
- 完整 Git 分支、提交、合并和 worktree 工具。
|
||||
- 云端 Runner、跨机器任务迁移或无人值守开机自启。
|
||||
- RAG、CodeGraph、tree-sitter 或语言服务器作为发布依赖。
|
||||
@@ -241,7 +241,7 @@ npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> -
|
||||
|
||||
### 受控命令反馈循环
|
||||
|
||||
单 Agent 新增 `command.exec`,用于补齐“复现问题 -> 读取真实 stdout / stderr -> 修改 -> 再验证”的开发闭环。该工具不是任意 shell,也不提供 PTY、后台服务或通用进程管理:
|
||||
单 Agent 新增 `command.exec`,用于补齐“复现问题 -> 读取真实 stdout / stderr -> 修改 -> 再验证”的开发闭环。该工具不是任意 shell,也不提供 PTY、后台服务或通用进程管理。这里对 PTY 和后台进程的排除只约束一次性 `command.exec`;V1.10 的持久进程必须使用独立的 `command.start / command.poll / command.stdin / command.terminate`,不能把 `command.exec` 偷换成长驻入口:
|
||||
|
||||
- 输入固定为 `program / args / cwd / timeoutSeconds`;`program` 只接受 Runtime 内置白名单,`args` 是逐项 argv,不接受 shell 字符串、重定向、管道、命令替换、环境变量或用户指定 executable 路径。
|
||||
- 首批只允许 `cargo` 的 `check / test / clippy / fmt / build / metadata`、`npm` 的 `test / run`、精确 `node --test <项目内普通测试文件...>`、受限只读 Git 子命令和受限 `rg`。npm 转发 argv 拒绝 shell 元字符和空白;Node 拒绝额外选项、glob、符号链接和 reparse point;Git 拒绝 pager、外部 diff/textconv、`grep -O`、pathspec 文件和 `.git / .env / key / config` 等敏感对象路径;`rg` 拒绝 follow、hidden/no-ignore、zip、preprocessor、类型覆盖和用户 glob,并在用户选项之后注入不可覆盖的敏感路径排除。每种程序继续执行参数级拒绝规则,禁止安装、发布、联网、修改 Git、切换工作区、读取项目外路径或覆盖执行环境。
|
||||
@@ -451,6 +451,99 @@ V1.8 已能按 `actionId` 分页读取完整命令输出,但模型可见的 `c
|
||||
|
||||
真实验收先后暴露并修正了两个既有收束缺口:项目验证与浏览器验证被错误要求固定先后;`scope=all` 的短状态截断和 context compaction 会丢失已认领 reviewer 结果并诱发重复轮询。最终 PASS 形成 146 条 task、248 条 event、255 条 Agent DB、16 条工具协议、15 次代表性成功工具执行、7 套确认生命周期、8 个实际副作用 action 和 43 条 terminal receipt(主 run 28 条),project revision 为 3。Runner 强杀后恢复原 run/session 且身份稳定;副作用重放、重复 action/message/receipt、命令正文跨边界泄漏、图片载荷、密钥和诱饵泄漏均为 0,最终 completed、assistant audit 和 assistant 消息各 1 条。
|
||||
|
||||
## V1.10 Runner-owned 持久进程会话
|
||||
|
||||
V1.10 为需要持续交互的前台开发进程增加 Runner-owned PTY 会话,例如受控开发服务器或交互式测试进程。它不把 `command.exec` 改成长驻命令,不开放 shell,也不允许进程自行 daemonize / detach。App、WebView 和发起 CLI 都只是持久动作入口;独立 Runner 持有 PTY master、child handle、输出泵和进程终态。
|
||||
|
||||
### 身份与状态机
|
||||
|
||||
- `command.start` 在真正 launch 前为原 durable action create-once 一个 opaque `processId`。该 ID 必须绑定 `projectId / agentId / taskId / sessionId / runId / startActionId / actionFingerprint / commandFingerprint / runnerBootId`;模型不能指定,也不能用 OS PID、端口或 cwd 反推。相同 start action 的恢复只返回同一个 `processId`,不同 action 不得复用。
|
||||
- OS PID / process-group id 只允许作为 Runner 私有诊断字段,不能充当 API 身份、权限凭证或恢复索引。`command.poll / command.stdin / command.terminate` 虽然只接收 `processId`,执行前仍必须从 Runner registry 和 durable record 交叉复核完整身份;知道或猜中 ID 不等于取得权限。
|
||||
- 对外状态固定为 `prepared / launching / running / terminating / exited / terminated / timed-out / output-limit-exceeded / needs-reconciliation / failed`,并使用独立 `needsReconciliation` 标志表达终态证据不完整。durable start action 必须在调用 OS spawn 前同步进入不可重放的 launching / executing 阶段;一旦已经尝试 launch,后续无法证明“尚未启动”时就只能 reconciliation,不能再次 launch。
|
||||
- Runner 内存控制面是活进程的唯一事实:registry 保存 PTY master、唯一 stdin writer 和控制通道,supervisor thread 独占 child handle、输出泵和等待任务;`.agent/runtime/**` 保存可恢复身份、状态和私有 transcript 元数据。durable record 不能伪装成仍持有 OS handle。
|
||||
|
||||
### 四个模型工具
|
||||
|
||||
`command.start` 输入固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"program": "npm",
|
||||
"args": ["run", "dev"],
|
||||
"cwd": ".",
|
||||
"timeoutSeconds": 300
|
||||
}
|
||||
```
|
||||
|
||||
- `program / args / cwd` 复用 `command.exec` 的固定程序、逐项 argv、项目内 cwd、可执行文件解析、安全 PATH、隔离 HOME / TMP / cache、离线配置和参数级拒绝规则。不得新增 shell 字符串、环境变量、用户 executable 路径、管道、重定向或后台符号;已知 daemonize / detach / background 参数必须在 spawn 前拒绝。
|
||||
- Runner 直接把已解析 executable 和 argv 接到固定 `120 cols x 30 rows` 的 PTY,子进程 stdout / stderr 合并为 PTY 输出。模型不能指定 shell、terminal device、PTY 尺寸、环境、process group 或宿主句柄。同一项目最多 4 个、同一 Agent instance 最多 2 个 unresolved process session;容量预检同时扫描当前 registry 与 durable active / reconciliation record,存在未解决旧 boot 会话时禁止新 start。容量、参数和 detach 拒绝必须发生在 revision 推进与 OS spawn 前,不能把普通拒绝误记为 reconciliation。
|
||||
- `command.start` 默认 `confirm`,确认继续绑定 actionId、完整动作指纹、repository fingerprint、project revision、execution owner 和精确 command fingerprint;`timeoutSeconds` 继续使用 Runtime 硬边界,超时必须终止并落 `timed-out`。成功 observation 只返回 `processId / status / cursor` 等安全元数据;进程输出必须通过 `command.poll` 读取。持久进程动作永远不能签发 verification passed gate。
|
||||
|
||||
`command.poll` 输入固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"processId": "proc-...",
|
||||
"cursor": "v1:proc-...:0",
|
||||
"maxChars": 8000,
|
||||
"waitMs": 1000
|
||||
}
|
||||
```
|
||||
|
||||
- `cursor` 是包含版本、processId 和 UTF-8 字节偏移的 opaque token;首次 poll 可省略,后续必须原样传回上一页 `nextCursor`,模型不得自行拼接。`maxChars` 默认 8,000、最大 16,000,`waitMs` 最大 30,000;相同 action 和相同 cursor 的恢复必须返回相同 chunk 或明确 reconciliation,不能移动一个隐式全局游标。返回至少包含 `status / output / cursor / nextCursor / hasMore / stdinOpen / exitCode / signal / outputBytes / outputSha256 / sourceChanged / needsReconciliation`;未终态时 `exitCode / signal` 为空。
|
||||
- PTY 原始字节先按稳定规则去除危险控制序列、做 UTF-8 有损解码、凭据和绝对路径清洗,并保留清洗后逻辑行边界,再进入当前 Agent 的私有 observation / context bundle。单会话清洗后输出上限固定为 256 KiB,达到上限时必须结束进程并落 `output-limit-exceeded`,不能静默覆盖未读正文。
|
||||
- `command.poll` 是同一 owning run 内的只读 durable action,默认 `auto`,不推进 project revision、不改变 verification gate、不认领 join。后台输出泵必须独立观察 child 终态并排空 PTY,不能要求模型轮询才把进程记为退出。
|
||||
|
||||
`command.stdin` 输入固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"processId": "proc-...",
|
||||
"data": "q",
|
||||
"appendNewline": true,
|
||||
"eof": false
|
||||
}
|
||||
```
|
||||
|
||||
- `data` 只接受 UTF-8 文本,`appendNewline=true` 时在哈希和写入前追加一个换行,`eof=true` 时写入后关闭唯一 writer;NUL / 二进制正文拒绝,单次最终 bytes 最大 8 KiB。stdin action 默认 `confirm` 且是不可重放副作用;写入前必须落 durable executing 标记,写入后若 observation / audit / receipt 未能完成则进入 `needs-reconciliation`,不得因为重试再次发送。
|
||||
- stdin 正文只允许短暂存在于执行所需的私有 pending action 和当前私有 context,终态补齐后删除 pending 正文。task、event、Agent DB、terminal receipt、activity、output、普通日志、验收报告和确认摘要只能记录 `processId / bytesWritten / contentSha256 / stdinOpen / eof`,禁止保存 data、前后缀、首尾字符或可逆编码。
|
||||
|
||||
`command.terminate` 输入固定为:
|
||||
|
||||
```json
|
||||
{
|
||||
"processId": "proc-...",
|
||||
"cursor": "v1:proc-...:254"
|
||||
}
|
||||
```
|
||||
|
||||
- `cursor` 必须传最后一次 `command.poll.nextCursor`。terminate 只返回同一 cursor 的零消费状态元数据,不读取、不跳过 PTY 字符;终止后继续从该 cursor poll 尾部与可信终态。
|
||||
- `command.terminate` 默认 `confirm`。Unix 终止固定为两阶段:先向 Runner-owned child / process group 发出 graceful request 并完整等待 800 ms;随后只对组内残留执行 force kill,并等待、reap、排空 PTY。直接 child 在宽限期内退出也不能立刻跳过剩余宽限并杀掉仍在执行 graceful handler 的同组子进程。Windows 首版没有等价的可靠 graceful console event,使用 Job Object force terminate 后 wait / reap;不能把它描述成已经覆盖 Windows graceful 阶段。
|
||||
- 只有 child 已确认终态、等待任务已回收且 PTY 尾部已排空,才能写 `exited / terminated` terminal record。graceful、force、wait、reap 或终态审计任一步无法确认时必须进入 `needs-reconciliation`,不能用“已发送信号”冒充“已终止”。重复 terminate 只能幂等返回同一终态,不得按 record 中的 PID 重新发送信号。
|
||||
|
||||
### 生命周期、恢复与收束
|
||||
|
||||
- App / WebView / 发起 CLI 退出、断开 endpoint 或重开窗口都不关闭 PTY;只要 owning Runner 仍存活,进程、输出泵和终态观察继续运行。App 重连后只按 `processId` 和 durable snapshot 查询,不接管 child handle。
|
||||
- Linux 的 PTY child wrapper 保持为同一前台进程组 leader,并监测 owning Runner parent PID;Runner 被 `SIGKILL` 后 wrapper 对当前进程组执行 fail-closed `SIGKILL`。Windows 会话在 spawn 后立即纳入 kill-on-close Job Object。Unix 正常 terminate 由 Runner 两阶段回收,Windows 由 Job force terminate;两者都必须确认 direct child reap,任何 signal / Job / wait 结果不确定都落 reconciliation,不能写可信 terminal。
|
||||
- Runner 重启只做 reconciliation。旧 `runnerBootId` 下已经进入 `prepared / launching / running / terminating` 且没有可信 terminal record 的会话统一转为 `needs-reconciliation`:不得重放 `command.start`,不得重发 stdin / terminate,也不得根据持久化 PID 重新打开、探测或接管 PTY。当前首版对旧 boot 的 `prepared` 也采取保守核对,不用“理论上尚未 spawn”作为自动重试依据。
|
||||
- 已有可信 terminal record 时,恢复只补齐 observation、专用审计和 receipt;不能再调用 OS 进程 API。`needs-reconciliation` 会话保留原 run / session / process identity 和私有证据,等待显式人工核对,不自动改成 exited、failed 或 completed。
|
||||
- owning run 存在 `launching / running / terminating-*` 或未解决 `needs-reconciliation` 会话时,最终回复、finalization journal 和 completed 投影全部失败关闭。Runtime prompt 必须要求 Agent 在收束前 poll 到终态或调用 terminate,不能用一段“服务仍在后台运行”的文字绕过门禁。
|
||||
- `runner.shutdown_if_idle` 必须同时检查 Runner registry 与 durable process records。存在活 handle、等待终态的输出泵、两阶段终止任务或未解决 reconciliation 时返回 busy,不得关闭 Runner;取消 run 也必须先驱动同一两阶段终止,无法确认终态时保持 reconciliation。
|
||||
|
||||
### 隔离、隐私与真实安全边界
|
||||
|
||||
- `processId` 只能由精确 owning `project + agentId + taskId + sessionId + runId` 使用;同模板不同动态 instance、不同静态 Agent、不同 run 或不同项目的 poll / stdin / terminate 全部失败关闭。动态 `child-*` 继续按模板 Agent 解析 policy,但进程身份仍绑定 instanceId,不能访问 sibling 或模板本体的会话。
|
||||
- 运行输出正文只存在于受保护的私有 transcript sidecar、当前 owning Agent 的私有 observation 和受限 context bundle。task/event、Agent DB 普通审计、terminal receipt、`agent.action_history`、activity/output JSONL、UI 公共 snapshot 和验收报告只保存 cursor、字节数、SHA-256、截断、状态和退出元数据;禁止保存正文。通用文件工具、仓库索引、checkpoint、diff 和 startup context 继续排除整个 `.agent/runtime/**`。
|
||||
- PTY 只改变输入输出和生命周期所有权,不构成 OS sandbox。V1.10 仍没有容器、mount / user / network namespace、seccomp、macOS sandbox profile 或等价 Windows restricted token / AppContainer;Linux owner watchdog 与 Windows Job Object 增强的是默认同组 / 同 Job 生命周期,不能阻止被允许程序主动 `setsid`、创建外部 service、直接读取当前 OS 用户可读文件或绕过代理联网。实现、UI 和验收报告不得宣称具备 Codex CLI 级沙箱或对主动逃逸的完整进程树隔离。
|
||||
|
||||
### 验收门禁
|
||||
|
||||
确定性测试必须覆盖:固定 program / argv 和 daemonize 参数拒绝;真实 PTY 启动、增量 poll、UTF-8 / 控制序列清洗、stdin 和自然退出;同 action 恢复不重复 launch / stdin / terminate;App 客户端退出后 Runner 继续;跨 Agent / run / project 的 processId 拒绝;活会话阻断 finalization 与 `shutdown_if_idle`;graceful 成功和超时后 force 两条终止路径;输出上限;task/event/Agent DB/receipt/report 输出正文零泄漏和 stdin 仅 `bytesWritten / contentSha256`;以及 Runner 在 launch、running、stdin、两阶段 terminate 各崩溃窗口重启后只进入 reconciliation、不重放、不按 PID 重连。
|
||||
|
||||
真实 Provider `llm-runtime` 必须使用无固定工具顺序、无预置 processId 的 disposable PTY fixture,证明模型能自行 `command.start`、poll 到 readiness、发送唯一 stdin challenge、poll 到对应输出、terminate 并在进程终态后唯一收束;发起 App / CLI 在 start 后退出,Runner 仍完成后续会话。另设 Runner 强杀场景,以 fixture launch count 和 durable action 身份证明启动次数仍为 1,重启后没有 PID reconnect、没有最终回复且 `shutdown_if_idle` 保持 busy / reconciliation。验收器还必须扫描 challenge、PTY 输出 sentinel 和 stdin 正文,证明公共持久面泄漏为 0。上述确定性与真实 Provider 门禁实际通过前,不得新增 V1.10 PASS、条数统计或“已验收”结论。
|
||||
|
||||
2026-07-14 真实 `gpt-5.5` V1.10 验收已通过。`process-session` 在无工具配方任务中自行完成 1 次 start、3 次连续 cursor poll、1 次 stdin 和 1 次 terminate,形成 41 条 task、75 条 event、63 条 Agent DB、8 条 action receipt、4 套确认生命周期、唯一 completed / assistant audit / assistant;fixture launch 为 1,终态后 PID 与端口均消失,副作用重放、重复 action / message / receipt、PTY / stdin 公共正文、密钥和诱饵泄漏均为 0。独立 `process-session-runner-kill` 在 readiness 后真实 `SIGKILL` owning Runner,形成 21 条 task、34 条 event、36 条 Agent DB;新 boot 保持原 run / session 身份,只把旧会话转为 1 条 reconciliation,launch 仍为 1、PID reconnect 为 0、PID / 端口消失,completed / assistant / 副作用重放和公共正文泄漏均为 0。两套 disposable 项目均按 sentinel 清理。
|
||||
|
||||
## 验收命令
|
||||
|
||||
- `npm run ai-game-creator-shell:typecheck`
|
||||
@@ -459,6 +552,7 @@ V1.8 已能按 `actionId` 分页读取完整命令输出,但模型可见的 `c
|
||||
- `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 llm-runtime`
|
||||
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite full`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
@@ -18,12 +18,16 @@
|
||||
|
||||
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 明确替代,未涉及能力继续沿用本文件。
|
||||
|
||||
同一文档的“V1.2 对标 Codex CLI 增量”继续作为受控命令与推理档位的事实源。`command.exec` 只接受 Runtime 白名单内的固定 `program` 和逐项 `args` argv,默认 `confirm`,可执行文件解析为项目外绝对路径且子进程只使用安全 PATH;不解析 shell 字符串,不提供管道、重定向、PTY 或后台进程。它的 action、stdout / stderr、退出码、超时与源码指纹结果统一进入现有 `action / observation`、project revision、verification gate 和 `needs-reconciliation` 链路;只有明确验证型命令且退出码、源码指纹、命令日志、manifest 与 Agent DB 审计全通过才签发 passed gate,Git / rg / cargo metadata / 普通 npm run 只作诊断。首版只请求终止受控进程组,安全等级与 `project.verify` 相同,不宣称已具备完整 OS sandbox 或 detached-process 隔离。
|
||||
同一文档的“V1.2 对标 Codex CLI 增量”继续作为受控命令与推理档位的事实源。对一次性 `command.exec` 而言,只接受 Runtime 白名单内的固定 `program` 和逐项 `args` argv,默认 `confirm`,可执行文件解析为项目外绝对路径且子进程只使用安全 PATH;不解析 shell 字符串,不提供管道、重定向、PTY 或后台进程。这里对 PTY 和后台进程的排除仅适用于 `command.exec`,不能用来否定 V1.10 的独立持久进程工具,也不能把 `command.exec` 自身改成长驻入口。`command.exec` 的 action、stdout / stderr、退出码、超时与源码指纹结果统一进入现有 `action / observation`、project revision、verification gate 和 `needs-reconciliation` 链路;只有明确验证型命令且退出码、源码指纹、命令日志、manifest 与 Agent DB 审计全通过才签发 passed gate,Git / rg / cargo metadata / 普通 npm run 只作诊断。首版只请求终止受控进程组,安全等级与 `project.verify` 相同,不宣称已具备完整 OS sandbox 或 detached-process 隔离。
|
||||
|
||||
同一文档的“V1.3 多文件变更集与内容审查”作为复杂代码修改的新事实源。`project.patchset` 在一个确认动作和一把项目锁内预检最多 12 个 create / update / delete,自动 checkpoint、只推进一次 revision,并以 SHA-256 乐观并发条件和回滚语义避免半完成修改;`project.diff(includeContent=true)` 返回有界统一 diff hunks。它不开放任意 `git apply` 文本,也不替代修改后的可执行验证。
|
||||
|
||||
同一文档的 V1.4-V1.9 继续作为当前事实源:V1.4 用只读 `git.inspect` 提供有界工作树状态和安全 hunks;V1.5 用跨 context window 的 milestones 保留已完成副作用与验证证据;V1.6 用 terminal receipt 和 `agent.action_history` 提供可恢复动作回查,并对未认领 all-join 的最终回复与动作历史设置双重完成门禁;V1.7 用 `image.inspect` 把 desktop / mobile 截图作为受控多模态输入交给当前 Agent 自己的 Provider,并严格禁止图片载荷持久化;V1.8 用 `command.output_read` 按 actionId 分页读取同一 Agent 当前或历史 run 的安全命令 transcript,正文只进入私有 context observation,不进入 task/event/Agent DB/receipt;V1.9 让 durable `command.exec` observation 直接返回安全的 `sourceActionId`,当前命令分页不再依赖先查动作历史。历史能力清单与这些版本冲突时,以 Runtime V1.1 技术方案和当前代码为准。
|
||||
|
||||
2026-07-14 起,同一文档的“V1.10 Runner-owned 持久进程会话”作为持续交互进程的编码级事实源。新增且只新增 `command.start / command.poll / command.stdin / command.terminate`:start 复用固定 program、逐项 argv、项目 cwd、安全环境和精确确认,在独立 Runner 内直接创建受控前台 PTY;`processId` 绑定项目、Agent、task、session、run、start action、命令指纹和 Runner boot 身份,不等于 OS PID。App / WebView / 发起 CLI 退出不终止会话;Runner 重启对已尝试 launch 的非终态会话只进入 `needs-reconciliation`,不得重放 start / stdin / terminate,也不得按 PID 重连。运行输出正文只进入 owning Agent 私有 transcript / observation / context,stdin 公共审计只留字节数和 SHA-256;跨 Agent、run、项目访问失败关闭。活会话或未解决 reconciliation 同时阻止最终回复、completed 和 `runner.shutdown_if_idle`;terminate 携带最后一次 poll cursor 且零消费输出,Unix 按 graceful wait -> residual group force kill -> reap / drain 两阶段完成,Windows 首版使用 Job force terminate + wait / reap。Linux 使用 owner-PID watchdog 回收同一前台进程组,Windows 使用 kill-on-close Job Object;这仍不是任意 shell、主动 detached 进程管理或 OS sandbox。
|
||||
|
||||
2026-07-14 V1.10 真实 `gpt-5.5` 验收:`process-session` 在无工具配方任务中完成 start / 3 次连续 cursor poll / stdin / terminate,41 条 task、75 条 event、63 条 Agent DB、8 条 receipt、4 套确认生命周期和唯一 completed / assistant,fixture launch 为 1,终态 PID / 端口、重放、重复与公共正文 / 密钥 / 诱饵泄漏均为 0。独立 Runner 强杀套件形成 21 条 task、34 条 event、36 条 Agent DB,新 boot 保持原 run / session,只产生 1 条 reconciliation,launch 仍为 1、PID reconnect / final / assistant / 重放 / 泄漏均为 0;两个 disposable 项目均已清理。
|
||||
|
||||
2026-07-12 真实验收:发布 AppData 中的真实 `gpt-5.5` 已通过最终安全收紧后的 `llm-runtime` 套件,覆盖 Runner 强杀恢复且 run/session 身份稳定、仓库上下文、checkpoint/精确修改、失败命令诊断与修复复验、6 套确认生命周期、项目验证、桌面与移动非空画布证据、3 个隔离实例并行和唯一 all-join;95 条 task、161 条 event、137 条 Agent DB、13 条合法工具协议、副作用判重、终态投影、assistant audit、消息、回执和密钥泄露均以结构化落盘事实验收。`full` 套件仍要求 External Editor API 配置,缺失时必须返回 `BLOCKED(editorApi)`,不得记为通过。
|
||||
|
||||
2026-07-13 V1.3 真实验收:同一真实 Provider 套件已改为先读取 SHA-256,再用唯一一次 `project.patchset` 同时更新和创建文件,并使用自动 checkpointId 读取 2 项内容 hunks;prepared / completed 审计各 1 条、patchset revision 增量为 1,Runner 强杀恢复、命令和项目验证、双视口浏览器验证、隔离 Agent join、重复副作用与密钥扫描继续全部通过。
|
||||
|
||||
@@ -19,17 +19,33 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
it('keeps command permissions explicit', () => {
|
||||
const commandIds = GAME_CREATION_APP_COMMANDS.map((command) => command.id);
|
||||
|
||||
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(56);
|
||||
expect(GAME_CREATION_APP_COMMANDS).toHaveLength(60);
|
||||
expect(commandIds).toContain('project.git_inspect');
|
||||
expect(commandIds).toContain('project.patchset');
|
||||
expect(commandIds).toContain('command.exec');
|
||||
expect(commandIds).toContain('command.output_read');
|
||||
expect(commandIds).toContain('command.start');
|
||||
expect(commandIds).toContain('command.poll');
|
||||
expect(commandIds).toContain('command.stdin');
|
||||
expect(commandIds).toContain('command.terminate');
|
||||
expect(commandIds.indexOf('command.exec')).toBe(
|
||||
commandIds.indexOf('command.run_limited') + 1,
|
||||
);
|
||||
expect(commandIds.indexOf('command.output_read')).toBe(
|
||||
commandIds.indexOf('command.exec') + 1,
|
||||
);
|
||||
expect(commandIds.indexOf('command.start')).toBe(
|
||||
commandIds.indexOf('command.output_read') + 1,
|
||||
);
|
||||
expect(commandIds.indexOf('command.poll')).toBe(
|
||||
commandIds.indexOf('command.start') + 1,
|
||||
);
|
||||
expect(commandIds.indexOf('command.stdin')).toBe(
|
||||
commandIds.indexOf('command.poll') + 1,
|
||||
);
|
||||
expect(commandIds.indexOf('command.terminate')).toBe(
|
||||
commandIds.indexOf('command.stdin') + 1,
|
||||
);
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find((command) => command.id === 'help.show')
|
||||
?.permission,
|
||||
@@ -49,6 +65,26 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
(command) => command.id === 'command.output_read',
|
||||
)?.permission,
|
||||
).toBe('auto');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'command.start',
|
||||
)?.permission,
|
||||
).toBe('confirm');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'command.poll',
|
||||
)?.permission,
|
||||
).toBe('auto');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'command.stdin',
|
||||
)?.permission,
|
||||
).toBe('confirm');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'command.terminate',
|
||||
)?.permission,
|
||||
).toBe('confirm');
|
||||
expect(
|
||||
GAME_CREATION_APP_COMMANDS.find(
|
||||
(command) => command.id === 'project.git_inspect',
|
||||
@@ -200,7 +236,7 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
(capability) => capability.id,
|
||||
);
|
||||
|
||||
expect(GAME_CREATION_AGENT_CAPABILITIES).toHaveLength(34);
|
||||
expect(GAME_CREATION_AGENT_CAPABILITIES).toHaveLength(35);
|
||||
expect(capabilityIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
'chat',
|
||||
@@ -214,6 +250,7 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
'tool-call-budget',
|
||||
'isolated-subagents',
|
||||
'repository-startup-context',
|
||||
'persistent-process-sessions',
|
||||
'browser-validation',
|
||||
'visual-inspection',
|
||||
'persistent-runner',
|
||||
@@ -258,6 +295,15 @@ describe('AI 游戏创作 App 共享契约', () => {
|
||||
area: 'local-runtime',
|
||||
title: '模型视觉检查',
|
||||
});
|
||||
expect(
|
||||
GAME_CREATION_AGENT_CAPABILITIES.find(
|
||||
(capability) => capability.id === 'persistent-process-sessions',
|
||||
),
|
||||
).toEqual({
|
||||
id: 'persistent-process-sessions',
|
||||
area: 'local-runtime',
|
||||
title: 'Runner 托管的持久进程会话',
|
||||
});
|
||||
expect(
|
||||
GAME_CREATION_AGENT_CAPABILITIES.find(
|
||||
(capability) => capability.id === 'conversation-history',
|
||||
|
||||
@@ -59,6 +59,10 @@ export const GAME_CREATION_APP_COMMANDS = [
|
||||
{ id: 'command.run_limited', permission: 'confirm' },
|
||||
{ id: 'command.exec', permission: 'confirm' },
|
||||
{ id: 'command.output_read', permission: 'auto' },
|
||||
{ id: 'command.start', permission: 'confirm' },
|
||||
{ id: 'command.poll', permission: 'auto' },
|
||||
{ id: 'command.stdin', permission: 'confirm' },
|
||||
{ id: 'command.terminate', permission: 'confirm' },
|
||||
{ id: 'canvas.project_open', permission: 'confirm' },
|
||||
{ id: 'canvas.project_sync', permission: 'confirm' },
|
||||
{ id: 'canvas.asset_import', permission: 'confirm' },
|
||||
@@ -141,6 +145,11 @@ export const GAME_CREATION_AGENT_CAPABILITIES = [
|
||||
area: 'local-runtime',
|
||||
title: '仓库启动上下文',
|
||||
},
|
||||
{
|
||||
id: 'persistent-process-sessions',
|
||||
area: 'local-runtime',
|
||||
title: 'Runner 托管的持久进程会话',
|
||||
},
|
||||
{ id: 'local-preview', area: 'local-runtime', title: '本地 HTTP 预览' },
|
||||
{
|
||||
id: 'browser-validation',
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct GameCreationAppCommandDescriptor {
|
||||
pub permission: GameCreationAppPermission,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 56] = [
|
||||
pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 60] = [
|
||||
command("help.show", GameCreationAppPermission::Auto),
|
||||
command("project.create", GameCreationAppPermission::Confirm),
|
||||
command("project.status", GameCreationAppPermission::Auto),
|
||||
@@ -68,6 +68,10 @@ pub const GAME_CREATION_APP_COMMANDS: [GameCreationAppCommandDescriptor; 56] = [
|
||||
command("command.run_limited", GameCreationAppPermission::Confirm),
|
||||
command("command.exec", GameCreationAppPermission::Confirm),
|
||||
command("command.output_read", GameCreationAppPermission::Auto),
|
||||
command("command.start", GameCreationAppPermission::Confirm),
|
||||
command("command.poll", GameCreationAppPermission::Auto),
|
||||
command("command.stdin", GameCreationAppPermission::Confirm),
|
||||
command("command.terminate", GameCreationAppPermission::Confirm),
|
||||
command("canvas.project_open", GameCreationAppPermission::Confirm),
|
||||
command("canvas.project_sync", GameCreationAppPermission::Confirm),
|
||||
command("canvas.asset_import", GameCreationAppPermission::Confirm),
|
||||
@@ -95,7 +99,7 @@ pub struct GameCreationAgentCapabilityDescriptor {
|
||||
pub title: &'static str,
|
||||
}
|
||||
|
||||
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 34] = [
|
||||
pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescriptor; 35] = [
|
||||
capability("chat", "user", "聊天入口"),
|
||||
capability("file-upload", "user", "上传文件"),
|
||||
capability("built-in-commands", "agent-runtime", "内置命令调用"),
|
||||
@@ -135,6 +139,11 @@ pub const GAME_CREATION_AGENT_CAPABILITIES: [GameCreationAgentCapabilityDescript
|
||||
"local-runtime",
|
||||
"仓库启动上下文",
|
||||
),
|
||||
capability(
|
||||
"persistent-process-sessions",
|
||||
"local-runtime",
|
||||
"Runner 托管的持久进程会话",
|
||||
),
|
||||
capability("local-preview", "local-runtime", "本地 HTTP 预览"),
|
||||
capability("browser-validation", "local-runtime", "浏览器试玩验证"),
|
||||
capability("visual-inspection", "local-runtime", "模型视觉检查"),
|
||||
@@ -647,7 +656,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn command_contract_keeps_expected_permissions() {
|
||||
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 56);
|
||||
assert_eq!(GAME_CREATION_APP_COMMANDS.len(), 60);
|
||||
|
||||
let command_ids = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
@@ -665,8 +674,28 @@ mod tests {
|
||||
.iter()
|
||||
.position(|command_id| *command_id == "command.output_read")
|
||||
.expect("command.output_read should exist");
|
||||
let start_index = command_ids
|
||||
.iter()
|
||||
.position(|command_id| *command_id == "command.start")
|
||||
.expect("command.start should exist");
|
||||
let poll_index = command_ids
|
||||
.iter()
|
||||
.position(|command_id| *command_id == "command.poll")
|
||||
.expect("command.poll should exist");
|
||||
let stdin_index = command_ids
|
||||
.iter()
|
||||
.position(|command_id| *command_id == "command.stdin")
|
||||
.expect("command.stdin should exist");
|
||||
let terminate_index = command_ids
|
||||
.iter()
|
||||
.position(|command_id| *command_id == "command.terminate")
|
||||
.expect("command.terminate should exist");
|
||||
assert_eq!(exec_index, limited_index + 1);
|
||||
assert_eq!(output_read_index, exec_index + 1);
|
||||
assert_eq!(start_index, output_read_index + 1);
|
||||
assert_eq!(poll_index, start_index + 1);
|
||||
assert_eq!(stdin_index, poll_index + 1);
|
||||
assert_eq!(terminate_index, stdin_index + 1);
|
||||
|
||||
let help = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
@@ -695,6 +724,33 @@ mod tests {
|
||||
GameCreationAppPermission::Auto
|
||||
);
|
||||
|
||||
let command_start = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "command.start")
|
||||
.expect("command.start should exist");
|
||||
assert_eq!(command_start.permission, GameCreationAppPermission::Confirm);
|
||||
|
||||
let command_poll = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "command.poll")
|
||||
.expect("command.poll should exist");
|
||||
assert_eq!(command_poll.permission, GameCreationAppPermission::Auto);
|
||||
|
||||
let command_stdin = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "command.stdin")
|
||||
.expect("command.stdin should exist");
|
||||
assert_eq!(command_stdin.permission, GameCreationAppPermission::Confirm);
|
||||
|
||||
let command_terminate = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "command.terminate")
|
||||
.expect("command.terminate should exist");
|
||||
assert_eq!(
|
||||
command_terminate.permission,
|
||||
GameCreationAppPermission::Confirm
|
||||
);
|
||||
|
||||
let project_patchset = GAME_CREATION_APP_COMMANDS
|
||||
.iter()
|
||||
.find(|command| command.id == "project.patchset")
|
||||
@@ -925,7 +981,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn capabilities_cover_standard_agent_runtime_needs() {
|
||||
assert_eq!(GAME_CREATION_AGENT_CAPABILITIES.len(), 34);
|
||||
assert_eq!(GAME_CREATION_AGENT_CAPABILITIES.len(), 35);
|
||||
|
||||
let ids = GAME_CREATION_AGENT_CAPABILITIES
|
||||
.iter()
|
||||
@@ -948,6 +1004,7 @@ mod tests {
|
||||
"short-term-memory",
|
||||
"long-term-memory",
|
||||
"conversation-history",
|
||||
"persistent-process-sessions",
|
||||
"canvas-project-sync",
|
||||
"local-preview",
|
||||
"visual-inspection",
|
||||
@@ -978,6 +1035,15 @@ mod tests {
|
||||
.expect("visual-inspection capability should exist");
|
||||
assert_eq!(visual_inspection.area, "local-runtime");
|
||||
assert_eq!(visual_inspection.title, "模型视觉检查");
|
||||
let persistent_process_sessions = GAME_CREATION_AGENT_CAPABILITIES
|
||||
.iter()
|
||||
.find(|capability| capability.id == "persistent-process-sessions")
|
||||
.expect("persistent-process-sessions capability should exist");
|
||||
assert_eq!(persistent_process_sessions.area, "local-runtime");
|
||||
assert_eq!(
|
||||
persistent_process_sessions.title,
|
||||
"Runner 托管的持久进程会话"
|
||||
);
|
||||
assert_eq!(
|
||||
GAME_CREATION_AGENT_CAPABILITIES
|
||||
.iter()
|
||||
|
||||
Reference in New Issue
Block a user