use super::{dispatch::*, endpoint::*, project_owner::*, protocol::*, state::*}; use crate::{ AgentRuntimeContextCompactionResult, GameCreatorManifestInvalidationEventSink, GameCreatorMcpCatalog, }; use serde_json::Value; use sha2::{Digest as _, Sha256}; use std::ffi::OsString; use std::fs; use std::io::{self, BufRead, BufReader, Read, Write}; use std::net::{Ipv4Addr, SocketAddrV4, TcpStream}; use std::path::{Path, PathBuf}; use std::process::{Child, Command, Stdio}; use std::sync::{Mutex, OnceLock}; use std::thread; use std::time::{Duration, Instant}; const AGENT_RUNNER_LOG_FILE_NAME: &str = "agent-runner.log"; const AGENT_RUNNER_LOG_INPUT_LINE_MAX_BYTES: usize = 8 * 1024; const AGENT_RUNNER_LOG_OUTPUT_MAX_CHARS: usize = 1_024; const AGENT_RUNNER_CLIENT_EXIT_TIMEOUT: Duration = Duration::from_secs(15); #[derive(Default)] pub(super) struct ExternalAgentRunnerGuiOwnerAttachmentState { generation: u64, registration: Option, } struct ExternalAgentRunnerGuiOwnerRegistration { generation: u64, config_dir: PathBuf, params: ExternalAgentRunnerRequestParams, attached_boot_id: Option, } static EXTERNAL_AGENT_RUNNER_GUI_OWNER_ATTACHMENT_STATE: OnceLock< Mutex, > = OnceLock::new(); fn external_agent_runner_gui_owner_attachment_state( ) -> &'static Mutex { EXTERNAL_AGENT_RUNNER_GUI_OWNER_ATTACHMENT_STATE .get_or_init(|| Mutex::new(ExternalAgentRunnerGuiOwnerAttachmentState::default())) } pub(super) fn register_external_agent_runner_gui_owner_attachment( state: &Mutex, config_dir: &Path, params: ExternalAgentRunnerRequestParams, ) { let mut state = lock_unpoisoned(state); state.generation = state.generation.wrapping_add(1); let generation = state.generation; state.registration = Some(ExternalAgentRunnerGuiOwnerRegistration { generation, config_dir: config_dir.to_path_buf(), params, attached_boot_id: None, }); } pub(super) fn attach_registered_external_agent_runner_gui_owner_if_needed_with( state: &Mutex, config_dir: &Path, endpoint: &ExternalAgentRunnerEndpoint, attach: F, ) -> Result<(), String> where F: FnOnce(&ExternalAgentRunnerEndpoint, ExternalAgentRunnerRequestParams) -> Result<(), String>, { let Some((generation, params)) = ({ let state = lock_unpoisoned(state); state.registration.as_ref().and_then(|registration| { (registration.config_dir == config_dir && registration.attached_boot_id.as_deref() != Some(endpoint.boot_id.as_str())) .then(|| (registration.generation, registration.params.clone())) }) }) else { return Ok(()); }; attach(endpoint, params)?; let mut state = lock_unpoisoned(state); if let Some(registration) = state.registration.as_mut() { if registration.generation == generation && registration.config_dir == config_dir { registration.attached_boot_id = Some(endpoint.boot_id.clone()); } } Ok(()) } fn redact_url_queries(line: &str) -> String { line.split_whitespace() .map(|token| { if (token.starts_with("http://") || token.starts_with("https://")) && token.contains('?') { let base = token.split_once('?').map(|(base, _)| base).unwrap_or(token); format!("{base}?") } else { token.to_string() } }) .collect::>() .join(" ") } fn sanitize_agent_runner_output(line: &str, config_dir: &Path) -> String { let lowercase = line.to_ascii_lowercase(); if [ "authorization", "bearer ", "api_key", "apikey", "api key", "x-api-key", "token=", "token:", "credential", "password", "cookie", "set-cookie", "secret", "access_token", "refresh_token", "\"token\"", "'token'", ] .iter() .any(|marker| lowercase.contains(marker)) { return "".to_string(); } if lowercase.contains("panic") { return "".to_string(); } let safe_internal_detail = lowercase.starts_with("agent.runner.failed:") || lowercase.starts_with("runner."); if !safe_internal_detail { let summary = if ["error", "failed", "failure", "失败", "错误", "异常"] .iter() .any(|marker| lowercase.contains(marker)) { "" } else if ["warning", "warn:"] .iter() .any(|marker| lowercase.contains(marker)) { "" } else { "" }; return summary.to_string(); } crate::sanitize_diagnostic_message(&redact_url_queries(line), Some(config_dir)) .chars() .take(AGENT_RUNNER_LOG_OUTPUT_MAX_CHARS) .collect() } fn read_bounded_agent_runner_line( reader: &mut R, ) -> io::Result> { let mut content = Vec::new(); let mut truncated = false; let mut saw_bytes = false; loop { let available = reader.fill_buf()?; if available.is_empty() { return if saw_bytes { Ok(Some(( String::from_utf8_lossy(&content).into_owned(), truncated, ))) } else { Ok(None) }; } saw_bytes = true; let newline = available.iter().position(|byte| *byte == b'\n'); let consumed = newline.map(|index| index + 1).unwrap_or(available.len()); let payload_len = newline.unwrap_or(available.len()); let remaining = AGENT_RUNNER_LOG_INPUT_LINE_MAX_BYTES.saturating_sub(content.len()); let copied = payload_len.min(remaining); content.extend_from_slice(&available[..copied]); if copied < payload_len { truncated = true; } reader.consume(consumed); if newline.is_some() { return Ok(Some(( String::from_utf8_lossy(&content).into_owned(), truncated, ))); } } } fn spawn_agent_runner_log_pump( stream: R, stream_name: &'static str, log_path: PathBuf, config_dir: PathBuf, ) where R: Read + Send + 'static, { let _ = thread::Builder::new() .name(format!("agent-runner-{stream_name}-log")) .spawn(move || { let mut reader = BufReader::new(stream); loop { match read_bounded_agent_runner_line(&mut reader) { Ok(None) => break, Ok(Some((line, truncated))) => { let line = sanitize_agent_runner_output(line.trim(), &config_dir); let _ = crate::append_bounded_diagnostic_line( &log_path, &format!( "runner.{stream_name} truncated={} {line}", if truncated { "true" } else { "false" } ), ); } Err(_) => { let _ = crate::append_bounded_diagnostic_line( &log_path, &format!("runner.{stream_name}.read-failed details=redacted"), ); break; } } } }); } pub(super) struct LaunchedExternalAgentRunner { child: Child, #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] runner_job: crate::WindowsKillOnCloseJob, } #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] fn terminate_failed_external_agent_runner_launch(child: &mut Child, error: String) -> String { let kill_error = child.kill().err(); let wait_error = child.wait().err(); match (kill_error, wait_error) { (None, None) => error, (kill_error, wait_error) => format!( "{error};清理启动失败的 Agent Runner 时出错:kill={},wait={}", kill_error .map(|error| error.to_string()) .unwrap_or_else(|| "ok".to_string()), wait_error .map(|error| error.to_string()) .unwrap_or_else(|| "ok".to_string()) ), } } pub(super) fn launch_external_agent_runner( config_dir: &Path, ) -> Result { let executable = std::env::current_exe() .map_err(|error| format!("读取 Agent Runner 当前二进制失败:{error}"))?; let runner_log_path = config_dir.join(AGENT_RUNNER_LOG_FILE_NAME); let _ = crate::append_bounded_diagnostic_line(&runner_log_path, "runner.launch.begin"); let mut command = Command::new(executable); let gui_owner_required = EXTERNAL_AGENT_RUNNER_GUI_OWNER_REQUIRED_CLIENT.load(std::sync::atomic::Ordering::Acquire); command .args(external_agent_runner_launch_arguments( config_dir, gui_owner_required, )) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); #[cfg(unix)] { use std::os::unix::process::CommandExt; // SAFETY: the closure only calls the async-signal-safe setsid syscall before exec. unsafe { command.pre_exec(|| { if libc::setsid() == -1 { Err(io::Error::last_os_error()) } else { Ok(()) } }); } } #[cfg(windows)] { #[cfg(all(not(debug_assertions), feature = "game-chat-release"))] crate::configure_windows_suspended_background_std_command(&mut command, true); #[cfg(not(all(not(debug_assertions), feature = "game-chat-release")))] crate::configure_windows_background_std_command(&mut command, true); } let mut child = command .spawn() .map_err(|error| format!("启动外部 Agent Runner 失败:{error}"))?; #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] let runner_job = match crate::WindowsKillOnCloseJob::assign_runner(&child) { Ok(job) => job, Err(error) => { return Err(terminate_failed_external_agent_runner_launch( &mut child, error, )); } }; #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] if let Err(error) = runner_job.resume_suspended_runner(&child) { drop(runner_job); return Err(terminate_failed_external_agent_runner_launch( &mut child, error, )); } if let Some(stdout) = child.stdout.take() { spawn_agent_runner_log_pump( stdout, "stdout", runner_log_path.clone(), config_dir.to_path_buf(), ); } if let Some(stderr) = child.stderr.take() { spawn_agent_runner_log_pump( stderr, "stderr", runner_log_path.clone(), config_dir.to_path_buf(), ); } #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] let _ = crate::append_bounded_diagnostic_line( &runner_log_path, "runner.launch.job.assigned-and-resumed", ); let _ = crate::append_bounded_diagnostic_line(&runner_log_path, "runner.launch.spawned"); Ok(LaunchedExternalAgentRunner { child, #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] runner_job, }) } pub(super) fn external_agent_runner_launch_arguments( config_dir: &Path, gui_owner_required: bool, ) -> Vec { let mut arguments = vec![ OsString::from("--agent-runner"), OsString::from("--config-dir"), config_dir.as_os_str().to_os_string(), ]; if gui_owner_required { arguments.push(OsString::from("--gui-owner-required")); } arguments } pub(super) fn send_external_agent_runner_request_with_protocol_and_id( endpoint: &ExternalAgentRunnerEndpoint, protocol_version: u32, request_id: String, method: &str, params: ExternalAgentRunnerRequestParams, ) -> Result { send_external_agent_runner_request_with_protocol_and_id_and_timeouts( endpoint, protocol_version, request_id, method, params, EXTERNAL_AGENT_RUNNER_CONNECT_TIMEOUT, external_agent_runner_client_read_timeout(method), EXTERNAL_AGENT_RUNNER_IO_TIMEOUT, ) } fn send_external_agent_runner_request_with_protocol_and_id_and_timeouts( endpoint: &ExternalAgentRunnerEndpoint, protocol_version: u32, request_id: String, method: &str, params: ExternalAgentRunnerRequestParams, connect_timeout: Duration, read_timeout: Duration, write_timeout: Duration, ) -> Result { if endpoint.protocol_version != protocol_version { return Err("Agent Runner endpoint 协议版本不兼容".to_string()); } let request = ExternalAgentRunnerRequest { protocol_version, request_id: request_id.clone(), token: endpoint.token.clone(), method: method.to_string(), params, }; let payload = serde_json::to_vec(&request).map_err(|_| "序列化 Agent Runner 请求失败".to_string())?; let address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, endpoint.port).into(); let mut stream = TcpStream::connect_timeout(&address, connect_timeout) .map_err(|error| format!("连接 Agent Runner 失败:{error}"))?; stream .set_read_timeout(Some(read_timeout)) .and_then(|_| stream.set_write_timeout(Some(write_timeout))) .map_err(|error| format!("配置 Agent Runner 客户端超时失败:{error}"))?; write_external_agent_runner_frame(&mut stream, &payload) .map_err(|error| format!("写入 Agent Runner 请求失败:{error}"))?; stream .flush() .map_err(|error| format!("刷新 Agent Runner 请求失败:{error}"))?; let response_payload = read_external_agent_runner_frame(&mut stream) .map_err(|error| format!("读取 Agent Runner 响应失败:{error}"))?; let response = serde_json::from_slice::(&response_payload) .map_err(|_| "解析 Agent Runner 响应失败".to_string())?; if response.protocol_version != protocol_version { return Err("Agent Runner 响应协议版本不兼容".to_string()); } if response.request_id != request_id { return Err("Agent Runner 响应 requestId 不匹配".to_string()); } if response.ok { return Ok(response.result.unwrap_or(Value::Null)); } let error = response.error.unwrap_or(ExternalAgentRunnerProtocolError { code: "runner-error".to_string(), message: "Agent Runner 请求失败".to_string(), }); Err(redact_runner_secret( &format!("{}: {}", error.code, error.message), &endpoint.token, )) } pub(super) fn external_agent_runner_client_read_timeout(method: &str) -> Duration { match method { "runtime.compact" => EXTERNAL_AGENT_RUNNER_CONTEXT_COMPACTION_IO_TIMEOUT, "mcp.status" => EXTERNAL_AGENT_RUNNER_MCP_STATUS_IO_TIMEOUT, _ => EXTERNAL_AGENT_RUNNER_IO_TIMEOUT, } } pub(super) fn send_external_agent_runner_request_with_id( endpoint: &ExternalAgentRunnerEndpoint, request_id: String, method: &str, params: ExternalAgentRunnerRequestParams, ) -> Result { send_external_agent_runner_request_with_protocol_and_id( endpoint, EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, request_id, method, params, ) } pub(super) fn send_external_agent_runner_request( endpoint: &ExternalAgentRunnerEndpoint, method: &str, params: ExternalAgentRunnerRequestParams, ) -> Result { let request_id = random_identifier(b"genarrative-agent-runner-request-id")?; send_external_agent_runner_request_with_id(endpoint, request_id, method, params) } pub(super) fn ping_external_agent_runner( endpoint: &ExternalAgentRunnerEndpoint, ) -> Result<(), String> { send_external_agent_runner_request( endpoint, "runner.ping", ExternalAgentRunnerRequestParams::default(), ) .map(|_| ()) } pub(super) fn retire_incompatible_external_agent_runner( endpoint_path: &Path, endpoint: &ExternalAgentRunnerEndpoint, allow_force_busy_migration: bool, ) -> Result<(), String> { if !request_external_agent_runner_shutdown_if_idle_at(endpoint_path, endpoint)? { if !allow_force_busy_migration { return Err( "Agent Runner 版本与当前客户端不一致,但旧 Runner 仍有任务,暂不能重启".to_string(), ); } force_terminate_external_agent_runner_and_cleanup(endpoint_path, endpoint, true) .map_err(|error| format!("旧 Agent Runner busy 且安全迁移失败:{error}"))?; } Ok(()) } pub(super) fn verify_external_agent_runner_ping_identity( endpoint: &ExternalAgentRunnerEndpoint, ) -> Result<(), String> { let result = send_external_agent_runner_request_with_protocol_and_id( endpoint, endpoint.protocol_version, random_identifier(b"genarrative-agent-runner-identity-ping-id")?, "runner.ping", ExternalAgentRunnerRequestParams::default(), )?; if result.get("pid").and_then(Value::as_u64) != Some(endpoint.pid as u64) || result.get("bootId").and_then(Value::as_str) != Some(endpoint.boot_id.as_str()) { return Err("Agent Runner ping 身份与 endpoint 不匹配".to_string()); } Ok(()) } fn request_external_agent_runner_shutdown_if_idle_at( endpoint_path: &Path, endpoint: &ExternalAgentRunnerEndpoint, ) -> Result { let request_id = random_identifier(b"genarrative-agent-runner-upgrade-request-id")?; let result = send_external_agent_runner_request_with_protocol_and_id( endpoint, endpoint.protocol_version, request_id, "runner.shutdown_if_idle", ExternalAgentRunnerRequestParams::default(), )?; let idle = result .get("idle") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner shutdown_if_idle 响应缺少 idle".to_string())?; if !idle { return Ok(false); } wait_for_external_agent_runner_boot_exit( endpoint_path, endpoint, EXTERNAL_AGENT_RUNNER_START_TIMEOUT, "旧 Agent Runner 未在版本切换期限内退出", )?; Ok(true) } fn wait_for_external_agent_runner_boot_exit( endpoint_path: &Path, endpoint: &ExternalAgentRunnerEndpoint, timeout: Duration, timeout_error: &str, ) -> Result<(), String> { let deadline = Instant::now() + timeout; let lock_path = endpoint_path .parent() .map(external_agent_runner_lock_path) .ok_or_else(|| "Agent Runner endpoint 缺少 AppData 父目录".to_string())?; loop { match read_external_agent_runner_endpoint(endpoint_path) { Ok(current) if current.boot_id == endpoint.boot_id => {} Ok(_) => return Ok(()), Err(_) => { if let Some(lock) = try_open_external_agent_runner_lock(&lock_path, "Agent Runner 单实例锁")? { drop(lock); return Ok(()); } } } if Instant::now() >= deadline { return Err(timeout_error.to_string()); } thread::sleep(Duration::from_millis(50)); } } fn read_external_agent_runner_endpoint_for_shutdown( config_dir: &Path, ) -> Result, String> { let endpoint_path = external_agent_runner_endpoint_path(config_dir); let lock_path = external_agent_runner_lock_path(config_dir); let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_START_TIMEOUT; loop { match fs::symlink_metadata(&endpoint_path) { Ok(metadata) if metadata.file_type().is_symlink() => { return Err("Agent Runner endpoint 不允许符号链接".to_string()); } Ok(_) => { let endpoint = read_external_agent_runner_endpoint(&endpoint_path)?; return Ok(Some((endpoint_path, endpoint))); } Err(error) if error.kind() == io::ErrorKind::NotFound => { if let Some(lock) = try_open_external_agent_runner_lock(&lock_path, "Agent Runner 单实例锁")? { drop(lock); return Ok(None); } if Instant::now() >= deadline { return Err( "Agent Runner 启动锁仍被占用,但 endpoint 未在期限内就绪".to_string() ); } thread::sleep(Duration::from_millis(50)); } Err(error) => { return Err(format!( "读取 Agent Runner endpoint 元数据失败:{}: {error}", endpoint_path.display() )); } } } } pub(super) fn shutdown_external_agent_runner_if_idle_at(config_dir: &Path) -> Result { let endpoint_path = external_agent_runner_endpoint_path(config_dir); let lock_path = external_agent_runner_lock_path(config_dir); let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_START_TIMEOUT; loop { match fs::symlink_metadata(&endpoint_path) { Ok(metadata) if metadata.file_type().is_symlink() => { return Err("Agent Runner endpoint 不允许符号链接".to_string()); } Ok(_) => break, Err(error) if error.kind() == io::ErrorKind::NotFound => { if let Some(lock) = try_open_external_agent_runner_lock(&lock_path, "Agent Runner 单实例锁")? { drop(lock); return Ok(true); } if Instant::now() >= deadline { return Err( "Agent Runner 启动锁仍被占用,但 endpoint 未在期限内就绪".to_string() ); } thread::sleep(Duration::from_millis(50)); } Err(error) => { return Err(format!( "读取 Agent Runner endpoint 元数据失败:{}: {error}", endpoint_path.display() )); } } } let endpoint = read_external_agent_runner_endpoint(&endpoint_path)?; request_external_agent_runner_shutdown_if_idle_at(&endpoint_path, &endpoint) } pub(crate) fn shutdown_external_agent_runner_if_idle() -> Result { let config_dir = external_agent_runner_config_dir() .ok_or_else(|| "外部 Agent Runner 尚未配置 AppData;请显式传入 --config-dir".to_string())?; shutdown_external_agent_runner_if_idle_at(&config_dir) } pub(super) fn shutdown_external_agent_runner_at(config_dir: &Path) -> Result<(), String> { let endpoint_path = external_agent_runner_endpoint_path(config_dir); let lock_path = external_agent_runner_lock_path(config_dir); let endpoint_deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_IO_TIMEOUT; loop { match fs::symlink_metadata(&endpoint_path) { Ok(metadata) if metadata.file_type().is_symlink() => { return Err("Agent Runner endpoint 不允许符号链接".to_string()); } Ok(_) => break, Err(error) if error.kind() == io::ErrorKind::NotFound => { if let Some(lock) = try_open_external_agent_runner_lock(&lock_path, "Agent Runner 单实例锁")? { drop(lock); return Ok(()); } if Instant::now() >= endpoint_deadline { return Err( "Agent Runner 实例锁仍被占用,但 endpoint 未在 GUI 关闭期限内出现" .to_string(), ); } thread::sleep(Duration::from_millis(25)); } Err(error) => { return Err(format!( "读取 Agent Runner endpoint 元数据失败:{}: {error}", endpoint_path.display() )); } } } let endpoint = read_external_agent_runner_endpoint(&endpoint_path)?; let graceful_shutdown = send_external_agent_runner_request_with_protocol_and_id_and_timeouts( &endpoint, endpoint.protocol_version, random_identifier(b"genarrative-agent-runner-gui-shutdown-id")?, "runner.shutdown", ExternalAgentRunnerRequestParams::default(), EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_CONNECT_TIMEOUT, EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_IO_TIMEOUT, EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_IO_TIMEOUT, ) .and_then(|result| { result .get("willShutdown") .and_then(Value::as_bool) .unwrap_or(false) .then_some(()) .ok_or_else(|| "Agent Runner shutdown 响应未确认退出".to_string()) }); match graceful_shutdown { Ok(()) => { let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_GUI_SHUTDOWN_EXIT_TIMEOUT; loop { match read_external_agent_runner_endpoint(&endpoint_path) { Ok(current) if current.boot_id == endpoint.boot_id => {} Ok(_) => return Ok(()), Err(_) => { if let Some(lock) = try_open_external_agent_runner_lock( &lock_path, "Agent Runner 单实例锁", )? { drop(lock); return Ok(()); } } } if Instant::now() >= deadline { return force_terminate_external_agent_runner_and_cleanup( &endpoint_path, &endpoint, false, ); } thread::sleep(Duration::from_millis(25)); } } Err(graceful_error) => { force_terminate_external_agent_runner_and_cleanup(&endpoint_path, &endpoint, true) .map_err(|force_error| { format!("{graceful_error};强制终止 Agent Runner 失败:{force_error}") }) } } } fn force_terminate_external_agent_runner_and_cleanup( endpoint_path: &Path, endpoint: &ExternalAgentRunnerEndpoint, allow_verified_legacy_identity: bool, ) -> Result<(), String> { force_terminate_external_agent_runner_process(endpoint, allow_verified_legacy_identity)?; let config_dir = endpoint_path .parent() .ok_or_else(|| "Agent Runner endpoint 缺少 AppData 父目录".to_string())?; let lock_path = external_agent_runner_lock_path(config_dir); let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_GUI_FORCE_TERMINATE_GRACE; loop { if let Some(lock) = try_open_external_agent_runner_lock(&lock_path, "Agent Runner 单实例锁")? { if read_external_agent_runner_endpoint(endpoint_path) .ok() .is_some_and(|current| current.boot_id == endpoint.boot_id) { fs::remove_file(endpoint_path) .map_err(|error| format!("清理已终止 Agent Runner endpoint 失败:{error}"))?; } drop(lock); return Ok(()); } if Instant::now() >= deadline { return Err("Agent Runner 已终止但实例锁未在期限内释放".to_string()); } thread::sleep(Duration::from_millis(25)); } } #[cfg(target_os = "linux")] struct ExternalAgentRunnerPidFd(i32); #[cfg(target_os = "linux")] impl Drop for ExternalAgentRunnerPidFd { fn drop(&mut self) { // SAFETY: self.0 is an owned pidfd returned by pidfd_open. unsafe { libc::close(self.0) }; } } #[cfg(target_os = "linux")] fn open_external_agent_runner_pidfd(pid: i32) -> Result, String> { // SAFETY: pidfd_open receives a range-checked pid and flags=0. let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) }; if fd < 0 { let error = io::Error::last_os_error(); if error.raw_os_error() == Some(libc::ESRCH) { return Ok(None); } return Err(format!("打开 Agent Runner pidfd 失败:{error}")); } Ok(Some(ExternalAgentRunnerPidFd(fd as i32))) } #[cfg(target_os = "linux")] fn signal_external_agent_runner_pidfd( pidfd: &ExternalAgentRunnerPidFd, signal: i32, ) -> Result { // SAFETY: pidfd is owned and live; null siginfo with flags=0 matches pidfd_send_signal. let result = unsafe { libc::syscall( libc::SYS_pidfd_send_signal, pidfd.0, signal, std::ptr::null::(), 0, ) }; if result == 0 { return Ok(true); } let error = io::Error::last_os_error(); if error.raw_os_error() == Some(libc::ESRCH) { Ok(false) } else { Err(format!("通过 pidfd 发送信号失败:{error}")) } } #[cfg(target_os = "linux")] fn force_terminate_external_agent_runner_process( endpoint: &ExternalAgentRunnerEndpoint, allow_verified_legacy_identity: bool, ) -> Result<(), String> { if endpoint.pid == std::process::id() { return Err("拒绝终止当前 GUI 进程".to_string()); } let pid = i32::try_from(endpoint.pid).map_err(|_| "Agent Runner pid 超出范围".to_string())?; let Some(pidfd) = open_external_agent_runner_pidfd(pid)? else { return Ok(()); }; if let Some(expected_start_identity) = endpoint.process_start_identity.as_deref() { let actual_start_identity = external_agent_runner_process_start_identity(endpoint.pid)? .ok_or_else(|| "当前平台未返回 Agent Runner 进程启动身份".to_string())?; if actual_start_identity != expected_start_identity { return Err("Agent Runner pid 已被其他进程复用,拒绝终止".to_string()); } } else if allow_verified_legacy_identity { verify_external_agent_runner_ping_identity(endpoint)?; } else { return Err("旧 Agent Runner endpoint 缺少进程启动身份,拒绝强制终止".to_string()); } if !signal_external_agent_runner_pidfd(&pidfd, libc::SIGTERM)? { return Ok(()); } let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_GUI_FORCE_TERMINATE_GRACE; while signal_external_agent_runner_pidfd(&pidfd, 0)? && Instant::now() < deadline { thread::sleep(Duration::from_millis(25)); } if !signal_external_agent_runner_pidfd(&pidfd, 0)? { return Ok(()); } signal_external_agent_runner_pidfd(&pidfd, libc::SIGKILL)?; Ok(()) } #[cfg(windows)] fn force_terminate_external_agent_runner_process( endpoint: &ExternalAgentRunnerEndpoint, allow_verified_legacy_identity: bool, ) -> Result<(), String> { use std::ffi::c_void; #[repr(C)] struct FileTime { low_date_time: u32, high_date_time: u32, } #[link(name = "kernel32")] unsafe extern "system" { fn OpenProcess(access: u32, inherit_handle: i32, process_id: u32) -> *mut c_void; fn GetProcessTimes( process: *mut c_void, creation_time: *mut FileTime, exit_time: *mut FileTime, kernel_time: *mut FileTime, user_time: *mut FileTime, ) -> i32; fn TerminateProcess(process: *mut c_void, exit_code: u32) -> i32; fn WaitForSingleObject(handle: *mut c_void, milliseconds: u32) -> u32; fn CloseHandle(handle: *mut c_void) -> i32; } const PROCESS_TERMINATE: u32 = 0x0001; const SYNCHRONIZE: u32 = 0x0010_0000; const PROCESS_QUERY_LIMITED_INFORMATION: u32 = 0x1000; if endpoint.pid == std::process::id() { return Err("拒绝终止当前 GUI 进程".to_string()); } // SAFETY: OpenProcess returns an owned kernel handle or null; it is closed below. let process = unsafe { OpenProcess( PROCESS_TERMINATE | SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, 0, endpoint.pid, ) }; if process.is_null() { let error = io::Error::last_os_error(); if error.raw_os_error() == Some(87) { return Ok(()); } return Err(format!("打开 Agent Runner 进程失败:{error}")); } let result = (|| { if let Some(expected_start_identity) = endpoint.process_start_identity.as_deref() { // SAFETY: FileTime is plain data filled by GetProcessTimes. let mut creation = unsafe { std::mem::zeroed::() }; let mut exit = unsafe { std::mem::zeroed::() }; let mut kernel = unsafe { std::mem::zeroed::() }; let mut user = unsafe { std::mem::zeroed::() }; // SAFETY: process is live and all output pointers refer to writable FileTime values. if unsafe { GetProcessTimes(process, &mut creation, &mut exit, &mut kernel, &mut user) } == 0 { return Err(format!( "读取 Agent Runner 进程启动身份失败:{}", io::Error::last_os_error() )); } let actual_start_identity = ((creation.high_date_time as u64) << 32 | creation.low_date_time as u64) .to_string(); if actual_start_identity != expected_start_identity { return Err("Agent Runner pid 已被其他进程复用,拒绝终止".to_string()); } } else if allow_verified_legacy_identity { verify_external_agent_runner_ping_identity(endpoint)?; } else { return Err("旧 Agent Runner endpoint 缺少进程启动身份,拒绝强制终止".to_string()); } // SAFETY: the handle includes PROCESS_TERMINATE and its process start identity was verified. if unsafe { TerminateProcess(process, 1) } == 0 { return Err(format!( "终止 Agent Runner 失败:{}", io::Error::last_os_error() )); } // SAFETY: the handle includes SYNCHRONIZE and remains valid for this bounded wait. let wait = unsafe { WaitForSingleObject(process, 1_000) }; if wait != 0 { return Err(format!("等待 Agent Runner 退出失败:waitResult={wait}")); } Ok(()) })(); // SAFETY: process is an owned non-null handle returned by OpenProcess. unsafe { CloseHandle(process) }; result } #[cfg(target_os = "macos")] fn force_terminate_external_agent_runner_process( _endpoint: &ExternalAgentRunnerEndpoint, _allow_verified_legacy_identity: bool, ) -> Result<(), String> { Err("macOS 不提供可绑定进程实例的安全强制终止句柄,拒绝按裸 pid 终止 Agent Runner".to_string()) } #[cfg(not(any(target_os = "linux", windows, target_os = "macos")))] fn force_terminate_external_agent_runner_process( _endpoint: &ExternalAgentRunnerEndpoint, _allow_verified_legacy_identity: bool, ) -> Result<(), String> { Err("当前平台不支持核验并强制终止 Agent Runner".to_string()) } pub(crate) fn shutdown_external_agent_runner() -> Result<(), String> { let config_dir = external_agent_runner_config_dir() .ok_or_else(|| "外部 Agent Runner 尚未配置 AppData;请显式传入 --config-dir".to_string())?; shutdown_external_agent_runner_at(&config_dir) } pub(crate) fn attach_external_agent_runner_gui_owner( event_sink: &GameCreatorManifestInvalidationEventSink, ) -> Result<(), String> { EXTERNAL_AGENT_RUNNER_GUI_OWNER_REQUIRED_CLIENT .store(true, std::sync::atomic::Ordering::Release); let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); let config_dir = external_agent_runner_config_dir() .ok_or_else(|| "外部 Agent Runner 尚未配置 AppData;请显式传入 --config-dir".to_string())?; register_external_agent_runner_gui_owner_attachment( external_agent_runner_gui_owner_attachment_state(), &config_dir, ExternalAgentRunnerRequestParams { event_sink_port: Some(event_sink.port), event_sink_token: Some(event_sink.token.clone()), ..ExternalAgentRunnerRequestParams::default() }, ); ensure_external_agent_runner(&config_dir).map(|_| ()) } pub(super) fn validate_external_agent_runner_gui_owner_attachment_result( result: &Value, ) -> Result<(), String> { if result.get("attached").and_then(Value::as_bool) == Some(true) && result.get("eventSinkAttached").and_then(Value::as_bool) == Some(true) { Ok(()) } else { Err("Agent Runner attach_gui_owner 响应未确认 owner 与事件接收端".to_string()) } } fn attach_external_agent_runner_gui_owner_at( endpoint: &ExternalAgentRunnerEndpoint, params: ExternalAgentRunnerRequestParams, ) -> Result<(), String> { let result = send_external_agent_runner_request(endpoint, "runner.attach_gui_owner", params)?; validate_external_agent_runner_gui_owner_attachment_result(&result) } fn attach_registered_external_agent_runner_gui_owner_if_needed( config_dir: &Path, endpoint: &ExternalAgentRunnerEndpoint, ) -> Result<(), String> { attach_registered_external_agent_runner_gui_owner_if_needed_with( external_agent_runner_gui_owner_attachment_state(), config_dir, endpoint, attach_external_agent_runner_gui_owner_at, ) } pub(super) fn shutdown_external_agent_runner_for_client_exit_at( config_dir: &Path, ) -> Result { let Some((endpoint_path, endpoint)) = read_external_agent_runner_endpoint_for_shutdown(config_dir)? else { return Ok(true); }; let request_id = random_identifier(b"genarrative-agent-runner-client-exit-request-id")?; let result = match send_external_agent_runner_request_with_protocol_and_id( &endpoint, endpoint.protocol_version, request_id, "runner.shutdown_for_client_exit", ExternalAgentRunnerRequestParams::default(), ) { Ok(result) => result, Err(error) => { return match read_external_agent_runner_endpoint(&endpoint_path) { Ok(current) if current.boot_id == endpoint.boot_id => Err(error), _ => Ok(true), }; } }; let accepted = result .get("accepted") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner shutdown_for_client_exit 响应缺少 accepted".to_string())?; let busy = result .get("busy") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner shutdown_for_client_exit 响应缺少 busy".to_string())?; let will_shutdown = result .get("willShutdown") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner shutdown_for_client_exit 响应缺少 willShutdown".to_string())?; match (accepted, busy, will_shutdown) { (false, true, false) => return Ok(false), (true, false, true) => {} _ => return Err("Agent Runner shutdown_for_client_exit 响应状态不一致".to_string()), } wait_for_external_agent_runner_boot_exit( &endpoint_path, &endpoint, AGENT_RUNNER_CLIENT_EXIT_TIMEOUT, "Agent Runner 未在客户端退出期限内停止", )?; Ok(true) } pub(crate) fn shutdown_external_agent_runner_for_client_exit() -> Result { let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); let Some(config_dir) = external_agent_runner_config_dir() else { return Ok(true); }; shutdown_external_agent_runner_for_client_exit_at(&config_dir) } pub(super) fn wait_for_external_agent_runner( config_dir: &Path, child: &mut Child, executable_fingerprint: &str, ) -> Result { let endpoint_path = external_agent_runner_endpoint_path(config_dir); let deadline = Instant::now() + EXTERNAL_AGENT_RUNNER_START_TIMEOUT; loop { if let Some(endpoint) = read_current_external_agent_runner_endpoint(&endpoint_path, executable_fingerprint) { if ping_external_agent_runner(&endpoint).is_ok() { return Ok(endpoint); } } if let Some(status) = child .try_wait() .map_err(|error| format!("检查外部 Agent Runner 子进程失败:{error}"))? { return Err(format!("外部 Agent Runner 在就绪前退出:{status}")); } if Instant::now() >= deadline { return Err("外部 Agent Runner 未在启动期限内就绪".to_string()); } thread::sleep(Duration::from_millis(50)); } } pub(super) fn ensure_external_agent_runner( config_dir: &Path, ) -> Result { let endpoint_path = external_agent_runner_endpoint_path(config_dir); let executable_fingerprint = current_external_agent_runner_executable_fingerprint()?; if let Ok(endpoint) = read_external_agent_runner_endpoint(&endpoint_path) { match external_agent_runner_endpoint_reuse_decision(&endpoint, &executable_fingerprint) { ExternalAgentRunnerReuseDecision::Reuse => { if ping_external_agent_runner(&endpoint).is_ok() { attach_registered_external_agent_runner_gui_owner_if_needed( config_dir, &endpoint, )?; return Ok(endpoint); } } ExternalAgentRunnerReuseDecision::Retire => { let incompatible_ping = send_external_agent_runner_request_with_protocol_and_id( &endpoint, endpoint.protocol_version, random_identifier(b"genarrative-agent-runner-upgrade-ping-id")?, "runner.ping", ExternalAgentRunnerRequestParams::default(), ); if incompatible_ping.is_ok() { retire_incompatible_external_agent_runner( &endpoint_path, &endpoint, EXTERNAL_AGENT_RUNNER_GUI_OWNER_REQUIRED_CLIENT .load(std::sync::atomic::Ordering::Acquire), )?; } } } } let mut launched = launch_external_agent_runner(config_dir)?; match wait_for_external_agent_runner(config_dir, &mut launched.child, &executable_fingerprint) { Ok(endpoint) => { thread::Builder::new() .name("agent-runner-reaper".to_string()) .spawn(move || { #[cfg(all(windows, not(debug_assertions), feature = "game-chat-release"))] let _runner_job = launched.runner_job; let _ = launched.child.wait(); }) .map_err(|error| format!("启动 Agent Runner 子进程回收线程失败:{error}"))?; attach_registered_external_agent_runner_gui_owner_if_needed(config_dir, &endpoint)?; Ok(endpoint) } Err(error) => { let _ = launched.child.kill(); let _ = launched.child.wait(); Err(error) } } } pub(crate) fn configure_external_agent_runner(config_dir: impl AsRef) -> Result<(), String> { let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); let config_dir = normalize_external_agent_runner_config_dir(config_dir.as_ref())?; set_external_agent_runner_config_dir(config_dir); Ok(()) } pub(crate) fn configure_external_agent_runner_read_only( config_dir: impl AsRef, ) -> Result<(), String> { let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); let config_dir = inspect_external_agent_runner_config_dir(config_dir.as_ref())?; set_external_agent_runner_config_dir(config_dir); Ok(()) } pub(crate) fn ensure_external_agent_runner_started() -> Result<(), String> { let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); let config_dir = external_agent_runner_config_dir() .ok_or_else(|| "外部 Agent Runner 尚未配置 AppData;请显式传入 --config-dir".to_string())?; ensure_external_agent_runner(&config_dir).map(|_| ()) } pub(crate) fn ensure_external_agent_runner_started_for_gui() -> Result<(), String> { EXTERNAL_AGENT_RUNNER_GUI_OWNER_REQUIRED_CLIENT .store(true, std::sync::atomic::Ordering::Release); ensure_external_agent_runner_started() } pub(crate) fn require_external_agent_runner_for_cli_runtime_write( root: &Path, ) -> Result<(), String> { require_external_agent_runner_configured_for_cli_runtime_write(root)?; ensure_external_agent_runner_started() } pub(crate) fn require_external_agent_runner_configured_for_cli_runtime_write( root: &Path, ) -> Result<(), String> { if external_agent_runner_is_server_process() { return Err("Agent Runner 进程不能作为普通 CLI 执行 Runtime 写命令".to_string()); } let config_dir = external_agent_runner_config_dir().ok_or_else(|| { "Agent Runtime 写命令必须显式传入 --config-dir <项目外 AppData 绝对路径>".to_string() })?; if !root.is_absolute() { return Err("Agent Runtime 写命令的项目路径必须是绝对路径".to_string()); } crate::validate_game_creator_runtime_config_dir_outside_project(&config_dir, root) } pub(super) fn parse_external_agent_runner_notification_kind( kind: &str, ) -> Result<(&'static str, Option), String> { match kind.trim() { "wake_pending" | "runtime.wake_pending" => Ok(("runtime.wake_pending", None)), "resume" | "runtime.resume" => Ok(("runtime.resume", None)), "shutdown_if_idle" | "runner.shutdown_if_idle" => Ok(("runner.shutdown_if_idle", None)), value => { let agent = value .strip_prefix("continue_action:") .or_else(|| value.strip_prefix("runtime.continue_action:")) .map(str::trim) .filter(|value| !value.is_empty()); match agent { Some(agent) => Ok(("runtime.continue_action", Some(agent.to_string()))), None => Err("未知 Agent Runner 通知类型".to_string()), } } } } pub(super) fn send_external_agent_runner_runtime_request( root: &Path, method: &str, agent: Option<&str>, run_id: Option<&str>, action_id: Option<&str>, steer_id: Option<&str>, ) -> Result { send_external_agent_runner_runtime_request_with_stable_identity( root, method, agent, run_id, action_id, steer_id, None, ) } pub(super) fn send_external_agent_runner_runtime_request_with_stable_identity( root: &Path, method: &str, agent: Option<&str>, run_id: Option<&str>, action_id: Option<&str>, steer_id: Option<&str>, stable_identity: Option<&str>, ) -> Result { let root = canonicalize_external_agent_runner_project_root(root)?; let root = root .to_str() .ok_or_else(|| "通知 Agent Runner 的项目 root 必须是 UTF-8 路径".to_string())?; let config_dir = external_agent_runner_config_dir() .ok_or_else(|| "外部 Agent Runner 尚未配置".to_string())?; crate::validate_game_creator_runtime_config_dir_outside_project(&config_dir, Path::new(root))?; // Establish liveness before the write request. The write itself is sent exactly once. let endpoint = { let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); ensure_external_agent_runner(&config_dir)? }; let params = ExternalAgentRunnerRequestParams { root: Some(root.to_string()), agent: agent.map(str::to_string), session_id: None, run_id: run_id.map(str::to_string), action_id: action_id.map(str::to_string), steer_id: steer_id.map(str::to_string), event_sink_port: None, event_sink_token: None, }; match stable_identity { Some(stable_identity) => { let identity = format!("{root}\n{method}\n{stable_identity}"); let fingerprint = format!("{:x}", Sha256::digest(identity.as_bytes())); send_external_agent_runner_request_with_id( &endpoint, format!("runtime-parent-wake-{}", &fingerprint[..32]), method, params, ) } None => send_external_agent_runner_request(&endpoint, method, params), } } pub(crate) fn compact_external_agent_runner_context( root: &Path, agent: &str, session_id: Option<&str>, ) -> Result { let agent = agent.trim(); if agent.is_empty() { return Err("手动压缩 Agent 上下文必须提供 agent".to_string()); } let root = canonicalize_external_agent_runner_project_root(root)?; let root_text = root .to_str() .ok_or_else(|| "通知 Agent Runner 的项目 root 必须是 UTF-8 路径".to_string())?; let config_dir = external_agent_runner_config_dir() .ok_or_else(|| "外部 Agent Runner 尚未配置".to_string())?; crate::validate_game_creator_runtime_config_dir_outside_project(&config_dir, &root)?; let endpoint = { let _configure = lock_unpoisoned(external_agent_runner_configure_lock()); ensure_external_agent_runner(&config_dir)? }; let result = send_external_agent_runner_request( &endpoint, "runtime.compact", ExternalAgentRunnerRequestParams { root: Some(root_text.to_string()), agent: Some(agent.to_string()), session_id: session_id .map(str::trim) .filter(|value| !value.is_empty()) .map(str::to_string), ..ExternalAgentRunnerRequestParams::default() }, )?; serde_json::from_value(result) .map_err(|error| format!("解析 Agent Runner 上下文压缩结果失败:{error}")) } pub(crate) fn read_external_agent_runner_mcp_catalog( root: &Path, ) -> Result { let result = send_external_agent_runner_runtime_request(root, "mcp.status", None, None, None, None)?; serde_json::from_value(result) .map_err(|error| format!("解析 Agent Runner MCP catalog 失败:{error}")) } pub(crate) fn wake_external_agent_runner_pending(root: &Path) -> Result<(), String> { send_external_agent_runner_runtime_request(root, "runtime.wake_pending", None, None, None, None) .map(|_| ()) } pub(crate) fn wake_external_agent_runner_pending_for_run( root: &Path, agent: &str, run_id: &str, loop_iteration: u32, ) -> Result<(), String> { if agent.trim().is_empty() || run_id.trim().is_empty() { return Err("父 run wake 必须同时提供 agent 和 runId".to_string()); } let stable_identity = format!("{agent}\n{run_id}\n{loop_iteration}"); send_external_agent_runner_runtime_request_with_stable_identity( root, "runtime.wake_pending", Some(agent), Some(run_id), None, None, Some(&stable_identity), ) .map(|_| ()) } pub(crate) fn resume_external_agent_runner(root: &Path) -> Result<(), String> { send_external_agent_runner_runtime_request(root, "runtime.resume", None, None, None, None) .map(|_| ()) } pub(crate) fn continue_external_agent_runner_action( root: &Path, agent: &str, run_id: &str, action_id: &str, ) -> Result<(), String> { if [agent, run_id, action_id] .into_iter() .any(|value| value.trim().is_empty()) { return Err("继续 Agent Runtime 动作必须同时提供 agent/runId/actionId".to_string()); } send_external_agent_runner_runtime_request( root, "runtime.continue_action", Some(agent), Some(run_id), Some(action_id), None, ) .map(|_| ()) } pub(crate) fn steer_external_agent_runner( root: &Path, agent: &str, run_id: &str, steer_id: &str, ) -> Result { if [agent, run_id, steer_id] .into_iter() .any(|value| value.trim().is_empty()) { return Err("追加 Agent 指令必须同时提供 agent/runId/steerId".to_string()); } let agent = agent.trim(); let run_id = run_id.trim(); let steer_id = steer_id.trim(); let result = send_external_agent_runner_runtime_request( root, "runtime.steer", Some(agent), Some(run_id), None, Some(steer_id), )?; parse_external_agent_runner_steer_result(&result) } pub(crate) fn interrupt_external_agent_runner_provider_for_steer_decision( root: &Path, agent: &str, run_id: &str, steer_id: &str, ) -> Result { if [agent, run_id, steer_id] .into_iter() .any(|value| value.trim().is_empty()) { return Err("LLM steer 中断判定必须同时提供 agent/runId/steerId".to_string()); } let result = send_external_agent_runner_runtime_request( root, "runtime.interrupt_for_steer_decision", Some(agent.trim()), Some(run_id.trim()), None, Some(steer_id.trim()), )?; parse_external_agent_runner_steer_result(&result) } pub(crate) fn pause_external_agent_runner( root: &Path, agent: &str, run_id: &str, ) -> Result { if [agent, run_id] .into_iter() .any(|value| value.trim().is_empty()) { return Err("暂停 Agent Goal 必须同时提供 agent/runId".to_string()); } let result = send_external_agent_runner_runtime_request( root, "runtime.pause", Some(agent.trim()), Some(run_id.trim()), None, None, )?; result .get("providerInterrupted") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner runtime.pause 响应缺少 providerInterrupted".to_string()) } pub(crate) fn cancel_external_agent_runner_goal( root: &Path, agent: &str, run_id: &str, ) -> Result { if [agent, run_id] .into_iter() .any(|value| value.trim().is_empty()) { return Err("清理 Agent Goal 必须同时提供 agent/runId".to_string()); } let result = send_external_agent_runner_runtime_request( root, "runtime.cancel", Some(agent.trim()), Some(run_id.trim()), None, None, )?; result .get("providerInterrupted") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner runtime.cancel 响应缺少 providerInterrupted".to_string()) } pub(super) fn parse_external_agent_runner_steer_result(result: &Value) -> Result { result .get("providerInterrupted") .and_then(Value::as_bool) .ok_or_else(|| "Agent Runner runtime.steer 响应缺少 providerInterrupted".to_string()) } pub(crate) fn notify_external_agent_runner(root: &Path, kind: &str) -> Result<(), String> { let (method, agent) = parse_external_agent_runner_notification_kind(kind)?; if method == "runtime.continue_action" { return Err( "continue_action 通知必须改用 typed helper 并绑定 agent/runId/actionId".to_string(), ); } send_external_agent_runner_runtime_request(root, method, agent.as_deref(), None, None, None) .map(|_| ()) } pub(super) fn read_external_agent_runner_status_at( config_dir: Option<&Path>, ) -> ExternalAgentRunnerStatus { let Some(config_dir) = config_dir else { return ExternalAgentRunnerStatus::disabled(); }; let endpoint_path = external_agent_runner_endpoint_path(config_dir); let endpoint = match read_external_agent_runner_endpoint(&endpoint_path) { Ok(endpoint) => endpoint, Err(error) => { return ExternalAgentRunnerStatus { enabled: true, running: false, protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION, pid: None, boot_id: None, port: None, heartbeat_at: None, error: Some(error), }; } }; let mut fallback = ExternalAgentRunnerStatus::from_endpoint(&endpoint, false); if endpoint.protocol_version != EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION { fallback.error = Some("Agent Runner endpoint 协议版本不兼容".to_string()); return fallback; } match send_external_agent_runner_request( &endpoint, "runner.status", ExternalAgentRunnerRequestParams::default(), ) { Ok(value) => match serde_json::from_value::(value) { Ok(mut status) => { status.enabled = true; status.error = None; status } Err(_) => { fallback.error = Some("解析 Agent Runner 状态失败".to_string()); fallback } }, Err(error) => { fallback.error = Some(redact_runner_secret(&error, &endpoint.token)); fallback } } } pub(crate) fn read_external_agent_runner_status() -> ExternalAgentRunnerStatus { let config_dir = external_agent_runner_config_dir(); read_external_agent_runner_status_at(config_dir.as_deref()) } #[cfg(test)] mod diagnostic_log_tests { use super::*; #[test] fn runner_log_output_redacts_config_paths_and_credentials() { let config_dir = Path::new(r"C:\Users\example\AppData\Roaming\game-chat"); assert_eq!( sanitize_agent_runner_output( r"agent.runner.failed: failed to open C:\Users\example\AppData\Roaming\game-chat\state.json", config_dir, ), "agent.runner.failed: failed to open \\state.json" ); assert_eq!( sanitize_agent_runner_output("Authorization: Bearer secret", config_dir), "" ); assert_eq!( sanitize_agent_runner_output( r"agent.runner.failed: project C:\private\game\index.html failed", config_dir, ), "agent.runner.failed: project failed" ); assert_eq!( sanitize_agent_runner_output("normal model response body", config_dir), "" ); assert_eq!( sanitize_agent_runner_output( "agent.runner.failed: request failed https://example.invalid/api?value=1", config_dir, ), "agent.runner.failed: request failed https://example.invalid/api?" ); assert_eq!( sanitize_agent_runner_output("error password=hunter2", config_dir), "" ); } #[test] fn runner_log_line_reader_caps_long_lines_and_drains_to_next_line() { let mut input = vec![b'x'; AGENT_RUNNER_LOG_INPUT_LINE_MAX_BYTES + 500]; input.extend_from_slice(b"\nerror: second line\n"); let mut reader = BufReader::new(std::io::Cursor::new(input)); let (first, first_truncated) = read_bounded_agent_runner_line(&mut reader) .expect("read first line") .expect("first line exists"); assert_eq!(first.len(), AGENT_RUNNER_LOG_INPUT_LINE_MAX_BYTES); assert!(first_truncated); let (second, second_truncated) = read_bounded_agent_runner_line(&mut reader) .expect("read second line") .expect("second line exists"); assert_eq!(second, "error: second line"); assert!(!second_truncated); } }