为持久进程增加可信启动握手
新增 PTY 外私有 bridge 与 sandbox ready/commit/exec 协议 升级 process record v3 并收紧恢复、幂等与 final/idle 门禁 完善 target 前台进程组、graceful 后代收束和审计失败处理 补齐跨平台回归测试及 Runtime 文档
This commit is contained in:
@@ -12584,6 +12584,9 @@ fn agent_runtime_process_poll_detail(
|
||||
"sandboxMode": result.sandbox_mode,
|
||||
"networkAccess": result.network_access,
|
||||
"sandboxProfileVersion": result.sandbox_profile_version,
|
||||
"sandboxEstablishment": result.sandbox_establishment,
|
||||
"targetExec": result.target_exec,
|
||||
"launchFailureKind": result.launch_failure_kind,
|
||||
});
|
||||
if include_output {
|
||||
detail
|
||||
@@ -12629,6 +12632,9 @@ fn append_agent_runtime_process_poll_audit(
|
||||
"sandboxMode": result.sandbox_mode,
|
||||
"networkAccess": result.network_access,
|
||||
"sandboxProfileVersion": result.sandbox_profile_version,
|
||||
"sandboxEstablishment": result.sandbox_establishment,
|
||||
"targetExec": result.target_exec,
|
||||
"launchFailureKind": result.launch_failure_kind,
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -12772,11 +12778,6 @@ fn observe_agent_runtime_command_start(
|
||||
};
|
||||
}
|
||||
};
|
||||
if let Err(error) =
|
||||
prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "command.start")
|
||||
{
|
||||
return agent_runtime_mutation_gate_failure_observation(root, "command.start", &error);
|
||||
}
|
||||
let source_fingerprint = match project_command_source_fingerprint(root) {
|
||||
Ok(fingerprint) => fingerprint,
|
||||
Err(error) => {
|
||||
@@ -12786,7 +12787,7 @@ fn observe_agent_runtime_command_start(
|
||||
summary: "command.start 无法计算启动前源码指纹,进程未启动".to_string(),
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"revisionAdvanced": true,
|
||||
"revisionAdvanced": false,
|
||||
"error": redact_agent_runtime_project_paths(root, &error, 500),
|
||||
})
|
||||
.to_string(),
|
||||
@@ -12794,40 +12795,85 @@ fn observe_agent_runtime_command_start(
|
||||
};
|
||||
}
|
||||
};
|
||||
let result = match start_prepared_process_session_at(
|
||||
let revision_before = match read_game_creator_agent_runtime_project_revision(root) {
|
||||
Ok(revision) => revision.revision,
|
||||
Err(error) => {
|
||||
return agent_runtime_mutation_gate_failure_observation(root, "command.start", &error);
|
||||
}
|
||||
};
|
||||
let mut result = match start_prepared_process_session_at(
|
||||
root,
|
||||
identity,
|
||||
&command_spec,
|
||||
&command_launch,
|
||||
source_fingerprint,
|
||||
|| {
|
||||
prepare_agent_runtime_project_mutation_locked(root, agent_id, run_id, "command.start")
|
||||
.map(|_| ())
|
||||
},
|
||||
) {
|
||||
Ok(result) => result,
|
||||
Err(error) => {
|
||||
let revision_advanced = read_game_creator_agent_runtime_project_revision(root)
|
||||
.map(|revision| (revision.revision > revision_before).to_string())
|
||||
.unwrap_or_else(|_| "unknown".to_string());
|
||||
let needs_reconciliation = error.needs_reconciliation();
|
||||
return AgentRuntimeToolObservation {
|
||||
tool: "command.start".to_string(),
|
||||
status: AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION.to_string(),
|
||||
summary: "command.start 启动结果无法完整确认".to_string(),
|
||||
status: if needs_reconciliation {
|
||||
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
|
||||
} else {
|
||||
"failed"
|
||||
}
|
||||
.to_string(),
|
||||
summary: if needs_reconciliation {
|
||||
"command.start 启动结果无法完整确认".to_string()
|
||||
} else {
|
||||
"command.start 未进入目标执行".to_string()
|
||||
},
|
||||
detail: Some(
|
||||
serde_json::json!({
|
||||
"revisionAdvanced": true,
|
||||
"error": redact_agent_runtime_project_paths(root, &error, 500),
|
||||
"revisionAdvanced": revision_advanced,
|
||||
"launchFailureKind": error.stage().as_str(),
|
||||
"error": redact_agent_runtime_project_paths(root, error.message(), 500),
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
let revision_advanced = read_game_creator_agent_runtime_project_revision(root)
|
||||
.map(|revision| revision.revision > revision_before)
|
||||
.unwrap_or(true);
|
||||
let audit = append_agent_runtime_process_poll_audit(root, "command.start", pending, &result);
|
||||
if let Err(error) = &audit {
|
||||
let _ = mark_process_session_start_audit_failure_at(root, &result.process_id, error);
|
||||
result.status = "needs-reconciliation".to_string();
|
||||
result.stdin_open = false;
|
||||
result.needs_reconciliation = true;
|
||||
result.launch_failure_kind = Some("start-audit-failed".to_string());
|
||||
}
|
||||
let needs_reconciliation = result.needs_reconciliation || audit.is_err();
|
||||
AgentRuntimeToolObservation {
|
||||
tool: "command.start".to_string(),
|
||||
status: process_session_observation_status(needs_reconciliation),
|
||||
status: if needs_reconciliation {
|
||||
AGENT_RUNTIME_TOOL_OBSERVATION_STATUS_NEEDS_RECONCILIATION
|
||||
} else if result.status == "failed" {
|
||||
"failed"
|
||||
} else {
|
||||
"ok"
|
||||
}
|
||||
.to_string(),
|
||||
summary: if audit.is_err() {
|
||||
"command.start 已返回,但安全审计无法完整落盘".to_string()
|
||||
} else {
|
||||
format!("进程会话 {} 状态为 {}", result.process_id, result.status)
|
||||
},
|
||||
detail: Some(agent_runtime_process_poll_detail(&result, false, true)),
|
||||
detail: Some(agent_runtime_process_poll_detail(
|
||||
&result,
|
||||
false,
|
||||
revision_advanced,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -135,17 +135,42 @@ pub(crate) fn stage_command_sandbox_launch(
|
||||
target_executable: &Path,
|
||||
target_arguments: &[OsString],
|
||||
) -> Result<StagedCommandSandboxLaunch, CommandSandboxError> {
|
||||
linux::stage_linux_command_sandbox_launch(launch, target_executable, target_arguments)
|
||||
let metadata = launch.metadata.clone();
|
||||
let gate = LaunchGate::new_for_sandbox_stdin(target_executable, target_arguments)
|
||||
.map_err(|error| CommandSandboxError::new(error, metadata))?;
|
||||
linux::stage_linux_command_sandbox_launch(
|
||||
launch,
|
||||
gate,
|
||||
linux::StagedSandboxTrampoline::SandboxStdin,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub(crate) fn stage_command_sandbox_launch_for_process_session(
|
||||
launch: CommandSandboxLaunch,
|
||||
target_executable: &Path,
|
||||
target_arguments: &[OsString],
|
||||
) -> Result<StagedCommandSandboxLaunch, CommandSandboxError> {
|
||||
let metadata = launch.metadata.clone();
|
||||
let gate = LaunchGate::new_for_sandbox_stdin(target_executable, target_arguments)
|
||||
.map_err(|error| CommandSandboxError::new(error, metadata))?;
|
||||
linux::stage_linux_command_sandbox_launch(
|
||||
launch,
|
||||
gate,
|
||||
linux::StagedSandboxTrampoline::ProcessSession,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux {
|
||||
use super::*;
|
||||
#[cfg(test)]
|
||||
use crate::command_sandbox_trampoline::sandbox_trampoline_test_environment;
|
||||
use crate::command_sandbox_trampoline::{
|
||||
sandbox_trampoline_arguments, LaunchGate, BWRAP_BLOCK_FD, BWRAP_STATUS_FD, TRAMPOLINE_PATH,
|
||||
TRAMPOLINE_SOURCE_FD,
|
||||
process_session_trampoline_arguments, sandbox_trampoline_arguments, LaunchGate,
|
||||
BWRAP_BLOCK_FD, BWRAP_STATUS_FD, TRAMPOLINE_PATH, TRAMPOLINE_SOURCE_FD,
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::command_sandbox_trampoline::{
|
||||
process_session_trampoline_test_environment, sandbox_trampoline_test_environment,
|
||||
};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::fs;
|
||||
@@ -192,10 +217,16 @@ mod linux {
|
||||
fixed_system_read_only: Vec<ReadOnlyMount>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(super) enum StagedSandboxTrampoline {
|
||||
SandboxStdin,
|
||||
ProcessSession,
|
||||
}
|
||||
|
||||
pub(super) fn stage_linux_command_sandbox_launch(
|
||||
mut launch: CommandSandboxLaunch,
|
||||
target_executable: &Path,
|
||||
target_arguments: &[OsString],
|
||||
gate: LaunchGate,
|
||||
trampoline: StagedSandboxTrampoline,
|
||||
) -> Result<StagedCommandSandboxLaunch, CommandSandboxError> {
|
||||
let metadata = launch.metadata.clone();
|
||||
let separator = launch
|
||||
@@ -242,7 +273,12 @@ mod linux {
|
||||
);
|
||||
#[cfg(test)]
|
||||
{
|
||||
let (name, value) = sandbox_trampoline_test_environment();
|
||||
let (name, value) = match trampoline {
|
||||
StagedSandboxTrampoline::SandboxStdin => sandbox_trampoline_test_environment(),
|
||||
StagedSandboxTrampoline::ProcessSession => {
|
||||
process_session_trampoline_test_environment()
|
||||
}
|
||||
};
|
||||
push_option(
|
||||
&mut launch.arguments,
|
||||
"--setenv",
|
||||
@@ -251,10 +287,11 @@ mod linux {
|
||||
}
|
||||
launch.arguments.push(OsString::from("--"));
|
||||
launch.arguments.push(OsString::from(TRAMPOLINE_PATH));
|
||||
launch.arguments.extend(sandbox_trampoline_arguments());
|
||||
launch.arguments.extend(match trampoline {
|
||||
StagedSandboxTrampoline::SandboxStdin => sandbox_trampoline_arguments(),
|
||||
StagedSandboxTrampoline::ProcessSession => process_session_trampoline_arguments(),
|
||||
});
|
||||
|
||||
let gate = LaunchGate::new_for_sandbox_stdin(target_executable, target_arguments)
|
||||
.map_err(|error| CommandSandboxError::new(error, metadata))?;
|
||||
Ok(StagedCommandSandboxLaunch { launch, gate })
|
||||
}
|
||||
|
||||
@@ -1254,10 +1291,13 @@ print("SANDBOX_OK")
|
||||
)
|
||||
.expect("prepare real Linux sandbox");
|
||||
let target_arguments = arguments.iter().map(OsString::from).collect::<Vec<_>>();
|
||||
let gate =
|
||||
LaunchGate::new_for_sandbox_stdin(Path::new("/usr/bin/python3"), &target_arguments)
|
||||
.expect("create real Linux sandbox gate");
|
||||
let staged = stage_linux_command_sandbox_launch(
|
||||
launch,
|
||||
Path::new("/usr/bin/python3"),
|
||||
&target_arguments,
|
||||
gate,
|
||||
StagedSandboxTrampoline::SandboxStdin,
|
||||
)
|
||||
.expect("stage real Linux sandbox");
|
||||
assert!(!staged
|
||||
@@ -1315,6 +1355,65 @@ print("SANDBOX_OK")
|
||||
"COMMITTED"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn staged_gate_variants_share_bwrap_control_mounts() {
|
||||
let launch = CommandSandboxLaunch {
|
||||
executable: PathBuf::from("/usr/bin/bwrap"),
|
||||
arguments: vec![
|
||||
OsString::from("--unshare-all"),
|
||||
OsString::from("--"),
|
||||
OsString::from("/usr/bin/true"),
|
||||
],
|
||||
cwd: PathBuf::from("/workspace/project"),
|
||||
environment: Vec::new(),
|
||||
metadata: CommandSandboxMetadata::enforced_linux(),
|
||||
};
|
||||
let target_arguments = [OsString::from("--version")];
|
||||
|
||||
let stdin = stage_command_sandbox_launch(
|
||||
launch.clone(),
|
||||
Path::new("/usr/bin/true"),
|
||||
&target_arguments,
|
||||
)
|
||||
.expect("stage stdin sandbox launch");
|
||||
let process_session = stage_command_sandbox_launch_for_process_session(
|
||||
launch,
|
||||
Path::new("/usr/bin/true"),
|
||||
&target_arguments,
|
||||
)
|
||||
.expect("stage process-session sandbox launch");
|
||||
|
||||
for staged in [&stdin, &process_session] {
|
||||
assert_eq!(staged.launch.executable, Path::new("/usr/bin/bwrap"));
|
||||
assert_eq!(staged.launch.cwd, Path::new("/workspace/project"));
|
||||
assert_eq!(staged.launch.metadata, stdin.launch.metadata);
|
||||
assert!(has_sequence(
|
||||
&staged.launch.arguments,
|
||||
&["--json-status-fd", "4", "--block-fd", "5"]
|
||||
));
|
||||
assert!(has_sequence(
|
||||
&staged.launch.arguments,
|
||||
&["--ro-bind-fd", "6", TRAMPOLINE_PATH]
|
||||
));
|
||||
}
|
||||
assert!(has_sequence(
|
||||
&stdin.launch.arguments,
|
||||
&[
|
||||
"--setenv",
|
||||
"GENARRATIVE_COMMAND_SANDBOX_TRAMPOLINE_FIXTURE",
|
||||
"stdin"
|
||||
]
|
||||
));
|
||||
assert!(has_sequence(
|
||||
&process_session.launch.arguments,
|
||||
&[
|
||||
"--setenv",
|
||||
"GENARRATIVE_COMMAND_SANDBOX_TRAMPOLINE_FIXTURE",
|
||||
"process-session"
|
||||
]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,7 @@ mod isolated_agent;
|
||||
mod patchset;
|
||||
mod preview;
|
||||
mod process_session;
|
||||
mod process_session_bridge;
|
||||
mod project;
|
||||
mod repository_context;
|
||||
mod runner;
|
||||
@@ -1263,13 +1264,17 @@ fn main() {
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if command_sandbox_trampoline::is_process_session_trampoline_mode(&args) {
|
||||
match command_sandbox_trampoline::run_process_session_trampoline() {
|
||||
Ok(exit_code) => std::process::exit(exit_code),
|
||||
Err(_) => std::process::exit(125),
|
||||
}
|
||||
}
|
||||
#[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);
|
||||
}
|
||||
Err(_) => std::process::exit(125),
|
||||
}
|
||||
}
|
||||
let runtime_config_dir = match take_cli_runtime_config_dir(&mut args) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -30569,6 +30569,170 @@ async fn execute_approved_process_action_for_test(
|
||||
(pending, observation)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn process_session_agent_runtime_start_audit_failure_fixture() {
|
||||
const AGENT_ID: &str = "code-prototype";
|
||||
const RUN_ID: &str = "code-process-start-audit-failure-run";
|
||||
const TARGET_LOG: &str = "start-audit-target.log";
|
||||
clear_process_session_registry_for_tests();
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(
|
||||
&root,
|
||||
"process-start-audit-project",
|
||||
"Agent Runtime 启动审计失败项目",
|
||||
)
|
||||
.expect("project init");
|
||||
let _cleanup = ProcessSessionIntegrationCleanup {
|
||||
root: root.clone(),
|
||||
agent_id: AGENT_ID,
|
||||
run_id: RUN_ID,
|
||||
};
|
||||
fs::write(
|
||||
root.join("package.json"),
|
||||
r#"{"private":true,"scripts":{"dev":"node process-start-audit-fixture.mjs"}}"#,
|
||||
)
|
||||
.expect("write start audit package");
|
||||
fs::write(
|
||||
root.join("process-start-audit-fixture.mjs"),
|
||||
format!(
|
||||
r#"import {{ appendFileSync }} from 'node:fs';
|
||||
appendFileSync('{TARGET_LOG}', 'STARTED\n');
|
||||
setInterval(() => appendFileSync('{TARGET_LOG}', 'TICK\n'), 20);
|
||||
"#
|
||||
),
|
||||
)
|
||||
.expect("write start audit fixture");
|
||||
fs::write(
|
||||
root.join(PROJECT_PERMISSION_POLICY_PATH),
|
||||
r#"{"deniedCommands":[],"confirmCommands":[],"agentPolicies":{}}"#,
|
||||
)
|
||||
.expect("write legacy empty policy");
|
||||
let mut state = start_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
AGENT_ID,
|
||||
"验证 command.start 在 Agent DB 审计失败后终止 target",
|
||||
RUN_ID,
|
||||
"agent-background-task",
|
||||
"准备启动审计失败 fixture",
|
||||
vec!["启动并确认失败关闭".to_string()],
|
||||
)
|
||||
.expect("start runtime");
|
||||
state.loop_iteration = 1;
|
||||
let start_action = AgentRuntimeToolAction {
|
||||
tool: "command.start".to_string(),
|
||||
reason: Some("启动持续写入 fixture 并注入 Agent DB 审计失败".to_string()),
|
||||
input: serde_json::json!({
|
||||
"program": "npm",
|
||||
"args": ["run", "dev"],
|
||||
"cwd": ".",
|
||||
"timeoutSeconds": 30
|
||||
}),
|
||||
};
|
||||
let start_pending = pending_tool_action_for_test(
|
||||
&root,
|
||||
&state,
|
||||
start_action.clone(),
|
||||
AGENT_RUNTIME_PENDING_ACTION_STATUS_APPROVED,
|
||||
None,
|
||||
);
|
||||
write_game_creator_agent_runtime_pending_tool_action(&root, &start_pending)
|
||||
.expect("write approved start action");
|
||||
state.status = "running".to_string();
|
||||
state.phase = "action".to_string();
|
||||
state.pending_tool_action = Some(start_pending.summary());
|
||||
append_game_creator_agent_runtime_task(&root, &state).expect("append approved start task");
|
||||
write_game_creator_agent_runtime_state(&root, &state).expect("write approved start state");
|
||||
write_game_creator_agent_runtime_tool_confirmation(
|
||||
&root,
|
||||
AGENT_ID,
|
||||
RUN_ID,
|
||||
"command.start",
|
||||
&start_pending.action_fingerprint,
|
||||
"确认启动 Agent DB 审计失败 fixture",
|
||||
)
|
||||
.expect("write start confirmation");
|
||||
|
||||
let agent_db = root.join(".agent/agent.db");
|
||||
fs::remove_file(&agent_db).expect("remove agent db before start audit failure");
|
||||
fs::create_dir(&agent_db).expect("replace agent db with directory");
|
||||
|
||||
let observation = execute_game_creator_agent_runtime_tool_action_with_pending_action(
|
||||
&root,
|
||||
AGENT_ID,
|
||||
RUN_ID,
|
||||
&state.current_task,
|
||||
&start_action,
|
||||
Some(&start_pending.action_id),
|
||||
Some(&start_pending),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
observation.status, "needs-reconciliation",
|
||||
"{observation:?}"
|
||||
);
|
||||
let detail: Value = serde_json::from_str(
|
||||
observation
|
||||
.detail
|
||||
.as_deref()
|
||||
.expect("start audit failure detail"),
|
||||
)
|
||||
.expect("parse start audit failure detail");
|
||||
assert_eq!(detail["status"], "needs-reconciliation");
|
||||
assert_eq!(detail["needsReconciliation"], true);
|
||||
assert_eq!(detail["launchFailureKind"], "start-audit-failed");
|
||||
assert_eq!(detail["sandboxEstablishment"], "established");
|
||||
assert_eq!(detail["targetExec"], "established");
|
||||
let process_id = detail["processId"]
|
||||
.as_str()
|
||||
.expect("start audit failure processId");
|
||||
|
||||
let record = active_process_session_records_at(&root, Some(AGENT_ID), Some(RUN_ID))
|
||||
.expect("read start audit failure record")
|
||||
.into_iter()
|
||||
.find(|record| record.process_id == process_id)
|
||||
.expect("start audit failure process record");
|
||||
assert_eq!(record.status, "needs-reconciliation");
|
||||
assert!(record.needs_reconciliation);
|
||||
assert!(!record.stdin_open);
|
||||
assert_eq!(
|
||||
record.launch_failure_kind.as_deref(),
|
||||
Some("start-audit-failed")
|
||||
);
|
||||
assert_eq!(record.sandbox_establishment, "established");
|
||||
assert_eq!(record.target_exec, "established");
|
||||
|
||||
let target_log = root.join(TARGET_LOG);
|
||||
let mut previous_len = usize::MAX;
|
||||
let mut stable_samples = 0;
|
||||
for _ in 0..100 {
|
||||
std::thread::sleep(Duration::from_millis(25));
|
||||
let current_len = fs::metadata(&target_log)
|
||||
.map(|metadata| usize::try_from(metadata.len()).unwrap_or(usize::MAX))
|
||||
.unwrap_or(0);
|
||||
if current_len == previous_len {
|
||||
stable_samples += 1;
|
||||
if stable_samples >= 8 {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
previous_len = current_len;
|
||||
stable_samples = 0;
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
stable_samples, 8,
|
||||
"business target kept writing after start audit failure"
|
||||
);
|
||||
let stopped_content = fs::read(&target_log).unwrap_or_default();
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
assert_eq!(
|
||||
fs::read(&target_log).unwrap_or_default(),
|
||||
stopped_content,
|
||||
"business target resumed after start audit failure"
|
||||
);
|
||||
}
|
||||
|
||||
async fn process_session_agent_runtime_confirmed_lifecycle_fixture() {
|
||||
const READY_SENTINEL: &str = "PROCESS_SESSION_READY_PRIVATE";
|
||||
const STDIN_SENTINEL: &str = "PROCESS_SESSION_STDIN_PRIVATE";
|
||||
@@ -30948,3 +31112,28 @@ fn process_session_agent_runtime_confirmed_lifecycle_runs_in_isolated_registry()
|
||||
.expect("spawn isolated process session integration test");
|
||||
assert!(status.success(), "isolated lifecycle test failed: {status}");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn process_session_agent_runtime_start_audit_failure_terminates_target_in_isolated_registry() {
|
||||
const CHILD_MARKER: &str =
|
||||
"GENARRATIVE_PROCESS_SESSION_AGENT_RUNTIME_START_AUDIT_FAILURE_CHILD";
|
||||
if std::env::var_os(CHILD_MARKER).is_some() {
|
||||
tauri::async_runtime::block_on(process_session_agent_runtime_start_audit_failure_fixture());
|
||||
return;
|
||||
}
|
||||
let status = std::process::Command::new(std::env::current_exe().expect("current test binary"))
|
||||
.env(CHILD_MARKER, "1")
|
||||
.args([
|
||||
"--exact",
|
||||
"tests::process_session_agent_runtime_start_audit_failure_terminates_target_in_isolated_registry",
|
||||
"--nocapture",
|
||||
"--test-threads=1",
|
||||
])
|
||||
.status()
|
||||
.expect("spawn isolated process session start audit failure test");
|
||||
assert!(
|
||||
status.success(),
|
||||
"isolated start audit failure test failed: {status}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4301,3 +4301,17 @@
|
||||
- 决策:bwrap 使用 fd 4/5 接收 status/block,运行中 App 可执行文件由父进程预打开并通过 fd 6 + `--ro-bind-fd` 挂到固定 trampoline 路径。pre-exec 先把全部源复制到 64 以上临时 FD,再统一映射到固定号,避免并发时源/目标 FD 重叠导致通道被覆盖。
|
||||
- 决策:bwrap COMMAND 分隔符固定取 launcher 插入的第一个独立 `--`,不能从目标 argv 末尾反查。durable commit 到 exec verdict 之间禁止 async await;commit 后协议/等待未知和执行后 command log、manifest、Agent DB、verification gate 落盘失败统一投影为 `needs-reconciliation`,只有明确 `TARGET_EXEC_FAILED` 可作为已知未 exec 的普通失败收束。
|
||||
- 边界:本切片只完成 `command.exec / project.verify`。`command.start`、process record v3、PTY 零控制帧泄漏和真实 Provider process-session 仍未完成,不宣称 V1.11.1 已整体交付。
|
||||
|
||||
## 2026-07-14 AI 游戏创作 Agent Runtime V1.11.1 持久进程可信握手
|
||||
|
||||
- 决策:`command.start` 的 Runner 与 PTY child wrapper 使用 Linux abstract Unix socket 建立一次性私有 bridge,并同时校验 32 字节随机 nonce、`SO_PEERCRED` peer pid 和 uid。portable-pty argv 只保留内部 child mode,完整 bwrap/target launch plan 只走 bridge;endpoint、nonce、控制帧和宿主 launch plan 不进入 target argv/env、PTY transcript、process record、receipt 或 Agent DB。
|
||||
- 决策:portable-pty 会关闭 fd 3 以上描述符,bubblewrap 也不会把未被 option 引用的 fd 3 传给最终 COMMAND,因此 process-session trampoline 仍以 fd 0 接收私有 gate。真实 target 的 stdin 由 trampoline 校验 fd 1 是 PTY 后复制同一 slave 得到;一次性命令继续使用 `/dev/null` stdin,两个模式不能混用。
|
||||
- 决策:process record 升级为 v3,新增 `sandboxEstablishment / targetExec / launchFailureKind / sandboxReadyAt / execEstablishedAt`。Runtime 只在 `SANDBOX_READY` 后推进 revision、清除旧验证凭证并最后写入 `launching + established/not-attempted` commit record;只有 `EXEC_ESTABLISHED` 后才写 running、注册 live session、启动业务 timeout 并返回零消费 cursor。明确 `TARGET_EXEC_FAILED` 写同一 processId 的 failed record;commit 后未知写 `launch-unknown + needs-reconciliation`。
|
||||
- 决策:v1/v2 active record 无条件迁移为 `unknown / unknown + legacy-active-record + needs-reconciliation`,不按 PID 重连或自动重放;历史 terminal record 保留原终态和 transcript,可用 `unknown / unknown + legacy-record` 惰性读取。target exit 0/7 均沿原 processId 收束。
|
||||
- 决策:PTY wrapper spawn 前先登记 pending launch reservation;Runner shutdown 和 idle/final 门禁必须看见 reservation,pid 激活前收到 shutdown 也必须取消,激活后终止整个 wrapper 进程组。start Agent DB 审计失败必须终止 live process 并把 record 标为 `start-audit-failed + needs-reconciliation`。
|
||||
- 决策:process-session child 在 sandbox-ready 后只接受父侧显式 `COMMIT_EXEC / ABORT_LAUNCH`,不使用独立 3 秒 commit timeout。durable callback 慢于 launcher setup timeout 时 target 继续保持零执行;父侧失败必须先发送 abort,再 kill/wait/reap containment tree。
|
||||
- 决策:Linux process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,原子完成 setpgid + tcsetpgrp 并恢复信号掩码后才 exec,避免 immediate stdin read 以后台组停在 SIGTTIN。graceful terminate 经 Runtime bridge 和 trampoline 私有控制帧只向 target group 发送 SIGTERM;direct leader 先退出时 trampoline 仍检查同组后代,wrapper/bwrap 保持最多 800ms 宽限并继续承载 PTY,宽限后再强杀外层 containment group。Runner 强杀仍依赖 owner monitor 与 bwrap die-with-parent 回收整个 namespace。
|
||||
- 决策:process record v3 使用封闭 launch 状态矩阵和逐项时间校验。`launch-unknown / start-audit-failed` 必须对应 `needs-reconciliation=true`,明确 target exec failure 只能是 `established/failed`;旧 boot prepared/launching 降级 target 为 unknown,非法 record 读取失败关闭,不能绕过 final/idle。Windows legacy start 的 durable callback 移到首次 action miss 之后,同 action replay 只返回原 processId,不重复推进 revision。
|
||||
- 决策:`command.stdin` 写入和 flush 成功后,若 target 在 writer 释放后先形成可信 terminal,仍按成功返回并持久化 `stdinOpen=false`;只有写入部分失败或结果 record 无法落盘才进入 reconciliation。
|
||||
- 决策:active process session 事实由 live registry、durable active/reconciliation record 和 Linux pending reservation 并集构成。capacity、cancel、final 和 runner idle 都必须先合并 live registry;record 被删除或改名不能让 live session 失败开放,损坏 record 仍读取失败关闭。non-Linux live record 的 started/ready/exec 使用同一 launch 时间点,避免跨秒后违反 v3 时间顺序。
|
||||
- 边界:确定性 bridge、PTY、迁移、fast-exit、target-exec-failed 和零执行测试通过后,只能宣称本地 Runtime 链路完成;真实 Provider `process-session` 与 Runner kill 套件重新通过前,不新增 V1.11.1 Provider PASS 结论。
|
||||
|
||||
@@ -2917,3 +2917,27 @@
|
||||
- 处理:构造器保证 bwrap options 与 COMMAND 之间只有第一个独立 `--` 是 launcher 分隔符;preflight 和 stage 都取第一个位置。不要从用户目标 argv 的末尾推断结构边界。
|
||||
- 验证:真实 staged bwrap 用例必须让目标 argv 带独立 `--`,同时断言 sandbox-ready/commit 前 marker 为零,commit 后才执行成功。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/command_sandbox.rs`、`command_exec.rs`。
|
||||
|
||||
## PTY 私有控制 FD 不能假设会被 bubblewrap 透传
|
||||
|
||||
- 现象:process-session wrapper 在 portable-pty 之后成功创建 fd 3 控制 socket,但 trampoline 收不到 ready/commit 帧;如果直接改用 fd 0,真实 target 又失去交互 stdin。
|
||||
- 原因:portable-pty 在 wrapper exec 前关闭全部 fd 3 以上描述符;wrapper 重新创建 fd 3 后,bubblewrap 仍只保留 stdio 和被 `--json-status-fd / --block-fd / --ro-bind-fd` 明确引用的描述符,未引用 fd 3 不是可靠 COMMAND 继承通道。
|
||||
- 处理:Runner 与 wrapper 先用 abstract Unix socket bridge 传递私有 launch plan;wrapper 内部 gate 继续通过 bwrap 会保留的 fd 0进入 trampoline。process-session trampoline 不把控制 fd 0传给 target,而是确认 fd 1 是 PTY 后复制同一 slave作为 target stdin;一次性命令模式仍显式使用 `/dev/null`。
|
||||
- 验证:真实 PTY 测试必须完成 readiness、stdin echo 和 terminate;trampoline 测试同时断言一次性 stdin 为 null、process-session stdin 可读 PTY,target fd 3/4/5/6 不存在,bridge endpoint/nonce/control frame 不出现在 transcript 和公共持久面。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session_bridge.rs`、`process_session.rs`、`command_sandbox.rs`、`command_sandbox_trampoline.rs`。
|
||||
|
||||
## graceful terminate 不能先杀承载 target 的 wrapper 进程组
|
||||
|
||||
- 现象:target 注册了 SIGTERM 清理逻辑,但 `command.terminate` 只偶尔出现 stopped marker;耗时 300-500ms 的清理经常被提前截断。
|
||||
- 原因:如果先向 wrapper/bwrap/trampoline/target 共用的外层进程组发送 SIGTERM,wrapper 会先退出,bwrap 的 die-with-parent 随即收走 namespace;名义上的 800ms 宽限并没有真正留给 target。
|
||||
- 处理:process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 setpgid + PTY slave tcsetpgrp 并恢复信号掩码后才 exec;不能先 spawn 到后台组再由 parent 设前台,否则 target 可能已经因 immediate read 收到 SIGTTIN。Runtime 通过两级私有控制通道请求 trampoline 只向 target group 发 SIGTERM。direct leader 退出后 trampoline 继续检查同组后代,外层 wrapper/bwrap 在最多 800ms 宽限期保持存活,超时才强杀 containment group。
|
||||
- 验证:使用直接 bash target 启动同组后台子进程;leader 在输出 READY 后自然退出,仍存活的子进程收到 TERM 后由 trap 延迟 400ms 写 marker 并退出,terminate 返回前 marker 必须存在。另跑 immediate stdin/EOF、Runner owner SIGKILL 和后代隔离用例,确认前台切组没有破坏交互或 fail-closed 回收。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`process_session_bridge.rs`、`command_sandbox_trampoline.rs`。
|
||||
|
||||
## 启动记录必须封闭状态组合,child 不能自行猜 durable commit 超时
|
||||
|
||||
- 现象:慢磁盘让 sandbox-ready 后的 child 在父侧持久化完成前自行退出,父侧随后把零执行误记为 launch-unknown;损坏的 `failed + launch-unknown + needsReconciliation=false` record 又可能被 active/final/idle 扫描漏掉。
|
||||
- 原因:child 与父侧各自维护短 timeout,没有统一 commit/abort 决策;record 校验只检查枚举和值存在,没有约束 status、sandbox、target、failure、reconciliation 与时间戳的合法组合。Windows 如果在 action 去重前调用 durable callback,还会在同 action replay 时重复推进 revision。
|
||||
- 处理:sandbox-ready 后 child 阻塞等待父侧显式 commit 或 abort,父侧失败时发送 abort 并回收树。v3 读取使用封闭状态矩阵和 `started <= ready <= exec <= terminal <= updated` 的逐项可选时间校验;旧 boot prepared/launching 及同 action start replay转成 target unknown。非 Linux durable callback 只放在 existing action miss 分支,started/ready/exec 使用同一时间点。live registry 必须与 durable record、pending reservation 合并参与 capacity/final/idle,不能因 record 缺失失败开放。
|
||||
- 验证:durable callback 延迟超过旧 3 秒时 target marker 在 callback 内必须仍不存在、commit 后才出现;构造 launch-unknown/start-audit/target-exec 的非法组合均拒绝读取,旧 boot launching 和 same-action replay必须变成可再次读取的 reconciliation,同 action Windows 测试只调用一次 callback;删除 live record 后 final/idle 仍被 registry 阻断。
|
||||
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`process_session_bridge.rs`、`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`。
|
||||
|
||||
@@ -585,7 +585,13 @@ V1.11.1 必须把 `prepared -> child-created -> sandbox-ready -> commit-persiste
|
||||
|
||||
2026-07-14 第一实现切片已接入一次性命令:Linux launcher 用 `--json-status-fd` 取得 child-created 后才写入 `--block-fd` 放行,运行中 App 可执行文件通过预打开 FD 和 `--ro-bind-fd` 固定挂入 sandbox;trampoline 的随机 nonce 控制帧只走一次性命令原本不用的 stdin socket,真实目标重新获得 `/dev/null` stdin。`command.exec` 与 `project.verify` 共用 `spawn -> child-created -> sandbox-ready -> durable callback -> commit-exec -> exec-established` launcher;revision / 旧验证凭证只在 sandbox-ready 后提交,业务 timeout 只在 exec-established 后开始,无效 `project.verify` 输入和 commit 前失败保留既有凭证。durable commit 到 exec verdict 之间不再出现 async cancellation point;commit 后协议未知、目标/输出等待异常和命令/Agent DB/gate 审计失败统一进入 `needs-reconciliation`,明确的 target exec failed 则记录 `established / failed / target-exec-failed` 并保留已提交状态。真实 bwrap marker、目标 argv 独立 `--`、EOF、错 nonce、target exec 失败、并发 FD 映射、工作区隔离、命令结果与验证门禁定向测试已通过。
|
||||
|
||||
该切片不代表 V1.11.1 全部完成:`command.start` 仍使用 V1.11 的 PTY launch 与 process record v2。下一切片必须由 process child wrapper 在 PTY 外桥接同一控制协议并升级 record v3;在该链路和真实 Provider `process-session` 通过前,不能把持久进程描述为具备可信 exec-ready 原子保证。
|
||||
2026-07-14 第二实现切片已把 `command.start` 接入可信握手。Runner 在 portable-pty spawn 前预留 pending launch,并通过 Linux abstract Unix socket、随机 nonce 与 `SO_PEERCRED` 校验后的 child wrapper交换私有 launch plan和 ready/commit/exec/terminal 帧;wrapper argv 不再携带 bwrap/target plan,bridge FD 显式 CLOEXEC。由于 portable-pty 会关闭 fd 3 以上描述符且 bubblewrap 不可靠透传未引用 fd 3,process-session trampoline 仍以 fd 0承载私有 gate,再从已验证的 PTY fd 1复制 slave作为真实 target stdin;target stdout/stderr继续继承 PTY,一次性命令仍恢复 `/dev/null` stdin。
|
||||
|
||||
process record 已升级为 v3:只在 `SANDBOX_READY` 后执行 revision / verification durable mutation并最后写入 `launching + established/not-attempted + sandboxReadyAt`,`EXEC_ESTABLISHED` 后才写 running、注册 live session、启动 timeout和返回同一 processId 的零消费 cursor。target 不存在或无权限形成 `established/failed/target-exec-failed` terminal record,fast exit 0/7 继续沿同一 processId收束;commit 后未知、exec 后持久化失败和 start Agent DB 审计失败进入 reconciliation。v1/v2 active record 无条件迁移为 unknown + reconciliation,legacy terminal 和 v1/v2 transcript继续可读;pending reservation参与 shutdown、capacity和 idle/final门禁。v3 读取使用封闭状态矩阵和完整时间顺序校验,`launch-unknown / start-audit-failed` 必须 fail-closed 为 reconciliation;旧 boot 的 prepared / launching 及同 action start replay统一降级 target exec 为 unknown,不能通过伪造 failed record 绕过 final / idle。active/capacity/final/idle 扫描先合并 live registry 与 durable record;record 缺失不能让仍运行的 session 失败开放,损坏 record 仍读取失败关闭。
|
||||
|
||||
process-session child 在 `SANDBOX_READY` 后阻塞等待父侧显式 `COMMIT_EXEC / ABORT_LAUNCH`,不设置独立短 commit timeout;durable mutation 超过 3 秒仍保持 target 零执行,父侧失败时显式 abort 并回收 wrapper 树。target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 `setpgid + tcsetpgrp` 并恢复信号掩码后才 exec,避免目标以后台组立即读取 PTY 而停在 SIGTTIN。`command.terminate` 经 Runtime bridge 和 trampoline 私有控制帧只向 target group 发 SIGTERM;即使 direct leader 先退出,trampoline 仍在 800ms 宽限内等待同组后代完成清理,wrapper / bwrap 继续承载 PTY 与控制链,超时后才强杀外层 containment group。正常 EOF 写入成功后即使可信 terminal 先于状态落盘,也按成功返回而不是误标 reconciliation;Windows legacy start 的 durable callback 只在同 action首次创建时执行,ready/exec/started 使用同一时间点避免跨秒倒序。
|
||||
|
||||
本地确定性测试已覆盖真实 PTY stdin/echo/EOF/terminate、direct leader 先退出后同组后代完成 400ms SIGTERM 清理、3.2 秒慢 durable callback、workspace sandbox后代、commit callback失败目标零执行、target exec failure、fast exit 0/7、wrong nonce/peer/乱序、v1/v2 active/terminal迁移、v3 非法组合、旧 boot launching及同 action replay、live record 缺失门禁、start Agent DB 审计失败、start cursor零消费和控制材料不进入 transcript。该实现完成 V1.11.1 本地 Runtime 链路,但真实 Provider `process-session` 与 Runner kill 套件仍须按新协议重跑;在它们通过前,不新增 Provider PASS、统计条数或整体“已验收”结论。
|
||||
|
||||
2026-07-14 最新真实 `gpt-5.5` `llm-runtime` 已按新增 metadata 门禁通过:123 条 task、208 条 event、220 条 Agent DB、15 次成功工具执行、2 次 `command.exec`(先失败后成功)、1 次 `project.verify`、3 个隔离实例、双视口浏览器验证、唯一 completed / assistant;Runner 强杀后 run / session 身份稳定恢复,重复、副作用重放、密钥和诱饵泄漏均为 0。保留现场独立核对 2 条 command.exec 和 1 条 project.verify 审计均为 `bubblewrap / workspace-write / disabled / workspace-v1` 后按 sentinel 清理。
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ V1.11 的受保护仓库控制目录同时包含 `.git / .agent / .agents / .cod
|
||||
|
||||
2026-07-14 V1.11.1 第一切片:`command.exec / project.verify` 已共用受信任 trampoline launcher。bwrap 的 `child-pid` 只推进 child-created,`--block-fd` 放行后仍须收到 `SANDBOX_READY`;Runtime 完成 revision / verification durable callback 后才发送 `COMMIT_EXEC`,收到 `EXEC_ESTABLISHED` 后才计算业务 timeout。当前不把这套 stdin 私有控制通道用于 PTY;`command.start` 与 process record v3 仍是下一切片,相关链路完成前 V1.11.1 保持进行中。
|
||||
|
||||
2026-07-14 V1.11.1 第二切片:`command.start` 已通过 PTY 外 abstract Unix socket bridge 接入同一 ready/commit/exec 状态机,process record 升级 v3;真实 target 的 PTY stdin 由 trampoline 复制已验证的 fd 1 slave,不向 target 泄漏 fd 0控制通道。pending launch和 live registry共同参与 capacity/shutdown/idle/final门禁,旧 v1/v2 active record迁入 reconciliation,target exec failure和fast exit保留同一 processId。child 等待父侧显式 commit/abort,不以短 timeout 猜测持久化失败;target 在 pre-exec 内原子进入 PTY 前台进程组,graceful terminate 只给 target group 发信号,direct leader 先退出后仍给同组后代保留清理宽限。v3 状态组合和时间顺序失败关闭,旧 boot launching及同 action replay降级为 launch-unknown,Windows 同 action replay 不重复执行 durable mutation且使用单一 launch 时间点。确定性测试通过后,V1.11.1 仍等待真实 Provider `process-session` 和 Runner kill复验,不提前标记整体已验收。
|
||||
|
||||
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、重复副作用与密钥扫描继续全部通过。
|
||||
|
||||
Reference in New Issue
Block a user