// OpenFang Kernel integration for ZClaw desktop app // Supports OpenFang Kernel (successor to OpenClaw Gateway) // - Port: 4200 (was 18789) // - Binary: openfang (was openclaw) // - Config: ~/.openfang/openfang.toml (was ~/.openclaw/openclaw.json) // Viking CLI sidecar module for local memory operations mod viking_commands; mod viking_server; // Memory extraction and context building modules (supplement CLI) mod memory; mod llm; // Browser automation module (Fantoccini-based Browser Hand) mod browser; // Secure storage module for OS keyring/keychain mod secure_storage; // Memory commands for persistent storage mod memory_commands; // Intelligence Layer (migrated from frontend lib/) mod intelligence; use serde::Serialize; use serde_json::{json, Value}; use std::fs; use std::net::{TcpStream, ToSocketAddrs}; use std::path::PathBuf; use std::process::Command; use std::thread; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use tauri::{AppHandle, Manager}; #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct LocalGatewayStatus { supported: bool, cli_available: bool, runtime_source: Option, runtime_path: Option, service_label: Option, service_loaded: bool, service_status: Option, config_ok: bool, port: Option, port_status: Option, probe_url: Option, listener_pids: Vec, error: Option, raw: Value, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct LocalGatewayAuth { config_path: Option, gateway_token: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct LocalGatewayPrepareResult { config_path: Option, origins_updated: bool, gateway_restarted: bool, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct LocalGatewayPairingApprovalResult { approved: bool, request_id: Option, device_id: Option, } struct OpenFangRuntime { source: String, executable: PathBuf, pre_args: Vec, display_path: PathBuf, } struct OpenFangCommandOutput { stdout: String, runtime: OpenFangRuntime, } /// Default OpenFang Kernel port const OPENFANG_DEFAULT_PORT: u16 = 4200; const TAURI_ALLOWED_ORIGINS: [&str; 2] = ["http://tauri.localhost", "tauri://localhost"]; fn command_error(runtime: &OpenFangRuntime, error: std::io::Error) -> String { if error.kind() == std::io::ErrorKind::NotFound { match runtime.source.as_str() { "bundled" => format!( "未找到 ZCLAW 内置 OpenFang 运行时:{}", runtime.display_path.display() ), "development" => format!( "未找到开发态 OpenFang 运行时:{}", runtime.display_path.display() ), "override" => format!( "未找到 ZCLAW_OPENFANG_BIN 指定的 OpenFang 运行时:{}", runtime.display_path.display() ), _ => "未找到 OpenFang 运行时。请重新安装 ZCLAW,或在开发环境中安装 OpenFang CLI。" .to_string(), } } else { format!("运行 OpenFang 失败: {error}") } } fn runtime_path_string(runtime: &OpenFangRuntime) -> String { runtime.display_path.display().to_string() } fn binary_extension() -> &'static str { if cfg!(target_os = "windows") { ".exe" } else { "" } } fn openfang_sidecar_filename() -> String { format!("openfang-{}{}", env!("TARGET"), binary_extension()) } fn openfang_plain_filename() -> String { format!("openfang{}", binary_extension()) } fn push_runtime_candidate(candidates: &mut Vec, source: &str, executable: PathBuf) { if candidates.iter().any(|candidate| candidate.display_path == executable) { return; } candidates.push(OpenFangRuntime { source: source.to_string(), display_path: executable.clone(), executable, pre_args: Vec::new(), }); } /// Build binary runtime (OpenFang is a single binary, not npm package) fn build_binary_runtime(source: &str, root_dir: &PathBuf) -> Option { // Try platform-specific binary names let binary_names = get_platform_binary_names(); for name in binary_names { let binary_path = root_dir.join(&name); if binary_path.is_file() { return Some(OpenFangRuntime { source: source.to_string(), executable: binary_path.clone(), pre_args: Vec::new(), display_path: binary_path, }); } } None } /// Get platform-specific binary names for OpenFang fn get_platform_binary_names() -> Vec { let mut names = Vec::new(); if cfg!(target_os = "windows") { names.push("openfang.exe".to_string()); names.push(format!("openfang-{}.exe", env!("TARGET"))); } else if cfg!(target_os = "macos") { if cfg!(target_arch = "aarch64") { names.push("openfang-aarch64-apple-darwin".to_string()); } else { names.push("openfang-x86_64-apple-darwin".to_string()); } names.push(format!("openfang-{}", env!("TARGET"))); names.push("openfang".to_string()); } else { // Linux if cfg!(target_arch = "aarch64") { names.push("openfang-aarch64-unknown-linux-gnu".to_string()); } else { names.push("openfang-x86_64-unknown-linux-gnu".to_string()); } names.push(format!("openfang-{}", env!("TARGET"))); names.push("openfang".to_string()); } names } /// Legacy: Build staged runtime using Node.js (for backward compatibility) #[allow(dead_code)] fn build_staged_runtime_legacy(source: &str, root_dir: PathBuf) -> Option { let node_executable = root_dir.join(if cfg!(target_os = "windows") { "node.exe" } else { "node" }); let entrypoint = root_dir .join("node_modules") .join("openfang") .join("openfang.mjs"); if !node_executable.is_file() || !entrypoint.is_file() { return None; } Some(OpenFangRuntime { source: source.to_string(), executable: node_executable, pre_args: vec![entrypoint.display().to_string()], display_path: root_dir, }) } /// Build staged runtime - prefers binary, falls back to Node.js for legacy support fn build_staged_runtime(source: &str, root_dir: PathBuf) -> Option { // First, try to find the binary directly if let Some(runtime) = build_binary_runtime(source, &root_dir) { return Some(runtime); } // Fallback to Node.js-based runtime for backward compatibility build_staged_runtime_legacy(source, root_dir) } fn push_staged_runtime_candidate(candidates: &mut Vec, source: &str, root_dir: PathBuf) { if candidates.iter().any(|candidate| candidate.display_path == root_dir) { return; } if let Some(runtime) = build_staged_runtime(source, root_dir) { candidates.push(runtime); } } fn bundled_runtime_candidates(app: &AppHandle) -> Vec { let mut candidates = Vec::new(); let sidecar_name = openfang_sidecar_filename(); let plain_name = openfang_plain_filename(); let platform_names = get_platform_binary_names(); if let Ok(resource_dir) = app.path().resource_dir() { // Primary: openfang-runtime directory (contains binary + manifest) push_staged_runtime_candidate( &mut candidates, "bundled", resource_dir.join("openfang-runtime"), ); // Alternative: binaries directory for name in &platform_names { push_runtime_candidate( &mut candidates, "bundled", resource_dir.join("binaries").join(name), ); } // Alternative: root level binaries push_runtime_candidate(&mut candidates, "bundled", resource_dir.join(&plain_name)); push_runtime_candidate(&mut candidates, "bundled", resource_dir.join(&sidecar_name)); } if let Ok(current_exe) = std::env::current_exe() { if let Some(exe_dir) = current_exe.parent() { // Windows NSIS installer location push_staged_runtime_candidate( &mut candidates, "bundled", exe_dir.join("resources").join("openfang-runtime"), ); // Alternative: binaries next to exe for name in &platform_names { push_runtime_candidate( &mut candidates, "bundled", exe_dir.join("binaries").join(name), ); } push_runtime_candidate(&mut candidates, "bundled", exe_dir.join(&plain_name)); push_runtime_candidate(&mut candidates, "bundled", exe_dir.join(&sidecar_name)); } } // Development mode let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); push_staged_runtime_candidate( &mut candidates, "development", manifest_dir.join("resources").join("openfang-runtime"), ); for name in &platform_names { push_runtime_candidate( &mut candidates, "development", manifest_dir.join("binaries").join(name), ); } candidates } /// Resolve OpenFang runtime location /// Priority: ZCLAW_OPENFANG_BIN env > bundled > system PATH fn resolve_openfang_runtime(app: &AppHandle) -> OpenFangRuntime { if let Ok(override_path) = std::env::var("ZCLAW_OPENFANG_BIN") { let override_path = PathBuf::from(override_path); if override_path.is_dir() { if let Some(runtime) = build_staged_runtime("override", override_path.clone()) { return runtime; } } return OpenFangRuntime { source: "override".to_string(), display_path: override_path.clone(), executable: override_path, pre_args: Vec::new(), }; } if let Some(runtime) = bundled_runtime_candidates(app) .into_iter() .find(|candidate| candidate.executable.is_file()) { return runtime; } OpenFangRuntime { source: "system".to_string(), display_path: PathBuf::from("openfang"), executable: PathBuf::from("openfang"), pre_args: Vec::new(), } } /// Resolve OpenFang config path (TOML format) /// Priority: OPENFANG_HOME env > ~/.openfang/ fn resolve_openfang_config_path() -> Option { if let Ok(value) = std::env::var("OPENFANG_HOME") { return Some(PathBuf::from(value).join("openfang.toml")); } if let Ok(value) = std::env::var("HOME") { return Some(PathBuf::from(value).join(".openfang").join("openfang.toml")); } if let Ok(value) = std::env::var("USERPROFILE") { return Some(PathBuf::from(value).join(".openfang").join("openfang.toml")); } None } /// Parse TOML config and extract gateway token fn read_local_gateway_auth() -> Result { let config_path = resolve_openfang_config_path() .ok_or_else(|| "未找到 OpenFang 配置目录。".to_string())?; let config_text = fs::read_to_string(&config_path) .map_err(|error| format!("读取 OpenFang 配置失败: {error}"))?; // Parse TOML format - simple extraction for gateway.token let gateway_token = extract_toml_token(&config_text); Ok(LocalGatewayAuth { config_path: Some(config_path.display().to_string()), gateway_token, }) } /// Extract gateway.token from TOML config text fn extract_toml_token(config_text: &str) -> Option { // Simple TOML parsing for gateway.token // Format: token = "value" under [gateway] section let mut in_gateway_section = false; for line in config_text.lines() { let trimmed = line.trim(); if trimmed.starts_with("[gateway") { in_gateway_section = true; continue; } if trimmed.starts_with('[') && !trimmed.starts_with("[gateway") { in_gateway_section = false; continue; } if in_gateway_section && trimmed.starts_with("token") { if let Some(eq_pos) = trimmed.find('=') { let value = trimmed[eq_pos + 1..].trim(); // Remove quotes let value = value.trim_matches('"').trim_matches('\''); if !value.is_empty() { return Some(value.to_string()); } } } } None } /// Ensure Tauri origins are allowed in OpenFang config fn ensure_tauri_allowed_origins(config_text: &str) -> (String, bool) { let mut lines: Vec = config_text.lines().map(|s| s.to_string()).collect(); let mut changed = false; let mut in_control_ui = false; let mut has_allowed_origins = false; // Find or create [gateway.controlUi] section with allowedOrigins for i in 0..lines.len() { let trimmed = lines[i].trim(); if trimmed.starts_with("[gateway.controlUi") || trimmed == "[gateway.controlUi]" { in_control_ui = true; } else if trimmed.starts_with('[') && in_control_ui { in_control_ui = false; } if in_control_ui && trimmed.starts_with("allowedOrigins") { has_allowed_origins = true; // Check if all required origins are present for origin in TAURI_ALLOWED_ORIGINS { if !lines[i].contains(origin) { // Append origin to the array // This is a simple approach - for production, use proper TOML parsing if lines[i].ends_with(']') { let insert_pos = lines[i].len() - 1; lines[i].insert_str(insert_pos, &format!(", \"{}\"", origin)); changed = true; } } } } } // If no allowedOrigins found, add the section if !has_allowed_origins { // Find [gateway] section and add controlUi after it for i in 0..lines.len() { if lines[i].trim().starts_with("[gateway]") || lines[i].trim() == "[gateway]" { // Insert controlUi section after gateway let origins: String = TAURI_ALLOWED_ORIGINS .iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(", "); lines.insert(i + 1, format!("[gateway.controlUi]")); lines.insert(i + 2, format!("allowedOrigins = [{}]", origins)); changed = true; break; } } // If no [gateway] section found, create it if !changed { let origins: String = TAURI_ALLOWED_ORIGINS .iter() .map(|s| format!("\"{}\"", s)) .collect::>() .join(", "); lines.push("[gateway]".to_string()); lines.push("[gateway.controlUi]".to_string()); lines.push(format!("allowedOrigins = [{}]", origins)); changed = true; } } (lines.join("\n"), changed) } fn ensure_local_gateway_ready_for_tauri(app: &AppHandle) -> Result { let config_path = resolve_openfang_config_path() .ok_or_else(|| "未找到 OpenFang 配置目录。".to_string())?; let config_text = fs::read_to_string(&config_path) .map_err(|error| format!("读取 OpenFang 配置失败: {error}"))?; let (updated_config, origins_updated) = ensure_tauri_allowed_origins(&config_text); if origins_updated { fs::write(&config_path, format!("{}\n", updated_config)) .map_err(|error| format!("写入 OpenFang 配置失败: {error}"))?; } let mut gateway_restarted = false; if origins_updated { if let Ok(status) = read_gateway_status(app) { if status.port_status.as_deref() == Some("busy") || !status.listener_pids.is_empty() { run_openfang(app, &["gateway", "restart", "--json"])?; thread::sleep(Duration::from_millis(1200)); gateway_restarted = true; } } } Ok(LocalGatewayPrepareResult { config_path: Some(config_path.display().to_string()), origins_updated, gateway_restarted, }) } fn approve_local_device_pairing( app: &AppHandle, device_id: &str, public_key_base64: &str, url: Option<&str>, ) -> Result { let local_auth = read_local_gateway_auth()?; let gateway_token = local_auth .gateway_token .ok_or_else(|| "本地 Gateway token 不可用,无法自动批准设备配对。".to_string())?; let devices_output = run_openfang(app, &["devices", "list", "--json"])?; let devices_json = parse_json_output(&devices_output.stdout)?; let pending = devices_json .get("pending") .and_then(Value::as_array) .ok_or_else(|| "设备列表输出缺少 pending 数组。".to_string())?; let pending_request = pending.iter().find(|entry| { entry.get("deviceId").and_then(Value::as_str) == Some(device_id) && entry.get("publicKey").and_then(Value::as_str) == Some(public_key_base64) }); let Some(request) = pending_request else { return Ok(LocalGatewayPairingApprovalResult { approved: false, request_id: None, device_id: Some(device_id.to_string()), }); }; let request_id = request .get("requestId") .and_then(Value::as_str) .ok_or_else(|| "待批准设备缺少 requestId。".to_string())? .to_string(); // Use OpenFang default port 4200 let gateway_url = url.unwrap_or("ws://127.0.0.1:4200").to_string(); let args = vec![ "devices".to_string(), "approve".to_string(), request_id.clone(), "--json".to_string(), "--token".to_string(), gateway_token, "--url".to_string(), gateway_url, ]; let arg_refs = args.iter().map(|value| value.as_str()).collect::>(); run_openfang(app, &arg_refs)?; thread::sleep(Duration::from_millis(300)); Ok(LocalGatewayPairingApprovalResult { approved: true, request_id: Some(request_id), device_id: Some(device_id.to_string()), }) } fn run_openfang(app: &AppHandle, args: &[&str]) -> Result { let runtime = resolve_openfang_runtime(app); let mut command = Command::new(&runtime.executable); command.args(&runtime.pre_args).args(args); let output = command.output().map_err(|error| command_error(&runtime, error))?; if output.status.success() { Ok(OpenFangCommandOutput { stdout: String::from_utf8_lossy(&output.stdout).trim().to_string(), runtime, }) } else { let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); let message = if stderr.is_empty() { stdout } else if stdout.is_empty() { stderr } else { format!("{stderr}\n{stdout}") }; if message.is_empty() { Err(format!("OpenFang {:?} 执行失败: {}", args, output.status)) } else { Err(message) } } } fn parse_json_output(stdout: &str) -> Result { if let Ok(raw) = serde_json::from_str::(stdout) { return Ok(raw); } if let Some(index) = stdout.find('{') { let trimmed = &stdout[index..]; return serde_json::from_str::(trimmed) .map_err(|error| format!("解析 Gateway 状态失败: {error}")); } Err("Gateway 状态输出不包含可解析的 JSON。".to_string()) } fn unavailable_status(error: String, runtime: Option<&OpenFangRuntime>) -> LocalGatewayStatus { LocalGatewayStatus { supported: true, cli_available: false, runtime_source: runtime.map(|value| value.source.clone()), runtime_path: runtime.map(runtime_path_string), service_label: None, service_loaded: false, service_status: None, config_ok: false, port: None, port_status: None, probe_url: None, listener_pids: Vec::new(), error: Some(error), raw: json!({}), } } fn parse_gateway_status(raw: Value, runtime: &OpenFangRuntime) -> LocalGatewayStatus { let listener_pids = raw .get("port") .and_then(|port| port.get("listeners")) .and_then(Value::as_array) .map(|listeners| { listeners .iter() .filter_map(|listener| listener.get("pid").and_then(Value::as_u64)) .filter_map(|pid| u32::try_from(pid).ok()) .collect::>() }) .unwrap_or_default(); LocalGatewayStatus { supported: true, cli_available: true, runtime_source: Some(runtime.source.clone()), runtime_path: Some(runtime_path_string(runtime)), service_label: raw .get("service") .and_then(|service| service.get("label")) .and_then(Value::as_str) .map(ToOwned::to_owned), service_loaded: raw .get("service") .and_then(|service| service.get("loaded")) .and_then(Value::as_bool) .unwrap_or(false), service_status: raw .get("service") .and_then(|service| service.get("runtime")) .and_then(|runtime| runtime.get("status")) .and_then(Value::as_str) .map(ToOwned::to_owned), config_ok: raw .get("service") .and_then(|service| service.get("configAudit")) .and_then(|config_audit| config_audit.get("ok")) .and_then(Value::as_bool) .unwrap_or(false), port: raw .get("gateway") .and_then(|gateway| gateway.get("port")) .and_then(Value::as_u64) .and_then(|port| u16::try_from(port).ok()) .or(Some(OPENFANG_DEFAULT_PORT)), port_status: raw .get("port") .and_then(|port| port.get("status")) .and_then(Value::as_str) .map(ToOwned::to_owned), probe_url: raw .get("gateway") .and_then(|gateway| gateway.get("probeUrl")) .and_then(Value::as_str) .map(ToOwned::to_owned), listener_pids, error: None, raw, } } fn read_gateway_status(app: &AppHandle) -> Result { match run_openfang(app, &["gateway", "status", "--json", "--no-probe"]) { Ok(result) => { let raw = parse_json_output(&result.stdout)?; Ok(parse_gateway_status(raw, &result.runtime)) } Err(error) => { let runtime = resolve_openfang_runtime(app); Ok(unavailable_status(error, Some(&runtime))) } } } // ============================================================================ // Tauri Commands - OpenFang (with backward-compatible aliases) // ============================================================================ /// Get OpenFang Kernel status #[tauri::command] fn openfang_status(app: AppHandle) -> Result { read_gateway_status(&app) } /// Start OpenFang Kernel #[tauri::command] fn openfang_start(app: AppHandle) -> Result { ensure_local_gateway_ready_for_tauri(&app)?; run_openfang(&app, &["gateway", "start", "--json"])?; thread::sleep(Duration::from_millis(800)); read_gateway_status(&app) } /// Stop OpenFang Kernel #[tauri::command] fn openfang_stop(app: AppHandle) -> Result { run_openfang(&app, &["gateway", "stop", "--json"])?; thread::sleep(Duration::from_millis(800)); read_gateway_status(&app) } /// Restart OpenFang Kernel #[tauri::command] fn openfang_restart(app: AppHandle) -> Result { ensure_local_gateway_ready_for_tauri(&app)?; run_openfang(&app, &["gateway", "restart", "--json"])?; thread::sleep(Duration::from_millis(1200)); read_gateway_status(&app) } /// Get local auth token from OpenFang config #[tauri::command] fn openfang_local_auth() -> Result { read_local_gateway_auth() } /// Prepare OpenFang for Tauri (update allowed origins) #[tauri::command] fn openfang_prepare_for_tauri(app: AppHandle) -> Result { ensure_local_gateway_ready_for_tauri(&app) } /// Approve device pairing request #[tauri::command] fn openfang_approve_device_pairing( app: AppHandle, device_id: String, public_key_base64: String, url: Option, ) -> Result { approve_local_device_pairing(&app, &device_id, &public_key_base64, url.as_deref()) } /// Run OpenFang doctor to diagnose issues #[tauri::command] fn openfang_doctor(app: AppHandle) -> Result { let result = run_openfang(&app, &["doctor", "--json"])?; Ok(result.stdout) } // ============================================================================ // Process Monitoring Commands // ============================================================================ /// Process information structure #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ProcessInfo { pid: u32, name: String, status: String, cpu_percent: Option, memory_mb: Option, uptime_seconds: Option, } /// Process list response #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ProcessListResponse { processes: Vec, total_count: usize, runtime_source: Option, } /// Process logs response #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ProcessLogsResponse { pid: Option, logs: String, lines: usize, runtime_source: Option, } /// Version information response #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct VersionResponse { version: String, commit: Option, build_date: Option, runtime_source: Option, raw: Value, } /// List OpenFang processes #[tauri::command] fn openfang_process_list(app: AppHandle) -> Result { let result = run_openfang(&app, &["process", "list", "--json"])?; let raw = parse_json_output(&result.stdout).unwrap_or_else(|_| json!({"processes": []})); let processes: Vec = raw .get("processes") .and_then(Value::as_array) .map(|arr| { arr.iter() .filter_map(|p| { Some(ProcessInfo { pid: p.get("pid").and_then(Value::as_u64)?.try_into().ok()?, name: p.get("name").and_then(Value::as_str)?.to_string(), status: p .get("status") .and_then(Value::as_str) .unwrap_or("unknown") .to_string(), cpu_percent: p.get("cpuPercent").and_then(Value::as_f64), memory_mb: p.get("memoryMb").and_then(Value::as_f64), uptime_seconds: p.get("uptimeSeconds").and_then(Value::as_u64), }) }) .collect() }) .unwrap_or_default(); Ok(ProcessListResponse { total_count: processes.len(), processes, runtime_source: Some(result.runtime.source), }) } /// Get OpenFang process logs #[tauri::command] fn openfang_process_logs( app: AppHandle, pid: Option, lines: Option, ) -> Result { let line_count = lines.unwrap_or(100); let lines_str = line_count.to_string(); // Build owned strings first to avoid lifetime issues let args: Vec = if let Some(pid_value) = pid { vec![ "process".to_string(), "logs".to_string(), "--pid".to_string(), pid_value.to_string(), "--lines".to_string(), lines_str, "--json".to_string(), ] } else { vec![ "process".to_string(), "logs".to_string(), "--lines".to_string(), lines_str, "--json".to_string(), ] }; // Convert to &str for the command let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); let result = run_openfang(&app, &args_refs)?; // Parse the logs - could be JSON array or plain text let logs = if let Ok(json) = parse_json_output(&result.stdout) { // If JSON format, extract logs array or convert to string if let Some(log_lines) = json.get("logs").and_then(Value::as_array) { log_lines .iter() .filter_map(|l| l.as_str()) .collect::>() .join("\n") } else if let Some(log_text) = json.get("log").and_then(Value::as_str) { log_text.to_string() } else { result.stdout.clone() } } else { result.stdout.clone() }; let log_lines_count = logs.lines().count(); Ok(ProcessLogsResponse { pid, logs, lines: log_lines_count, runtime_source: Some(result.runtime.source), }) } /// Get OpenFang version information #[tauri::command] fn openfang_version(app: AppHandle) -> Result { let result = run_openfang(&app, &["--version", "--json"])?; let raw = parse_json_output(&result.stdout).unwrap_or_else(|_| { // Fallback: try to parse plain text version output json!({ "version": result.stdout.trim(), "raw": result.stdout.trim() }) }); let version = raw .get("version") .and_then(Value::as_str) .unwrap_or("unknown") .to_string(); let commit = raw.get("commit").and_then(Value::as_str).map(ToOwned::to_owned); let build_date = raw.get("buildDate").and_then(Value::as_str).map(ToOwned::to_owned); Ok(VersionResponse { version, commit, build_date, runtime_source: Some(result.runtime.source), raw, }) } // ============================================================================ // Health Check Commands // ============================================================================ /// Health status enum #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "lowercase")] enum HealthStatus { Healthy, Unhealthy, Unknown, } /// Port check result #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct PortCheckResult { port: u16, accessible: bool, latency_ms: Option, error: Option, } /// Process health details #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct ProcessHealthDetails { pid: Option, name: Option, status: Option, uptime_seconds: Option, cpu_percent: Option, memory_mb: Option, } /// Health check response #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] struct HealthCheckResponse { status: HealthStatus, process: ProcessHealthDetails, port_check: PortCheckResult, last_check_timestamp: u64, checks_performed: Vec, issues: Vec, runtime_source: Option, } /// Check if a TCP port is accessible fn check_port_accessibility(host: &str, port: u16, timeout_ms: u64) -> PortCheckResult { let addr = format!("{}:{}", host, port); // Resolve the address let socket_addr = match addr.to_socket_addrs() { Ok(mut addrs) => addrs.next(), Err(e) => { return PortCheckResult { port, accessible: false, latency_ms: None, error: Some(format!("Failed to resolve address: {}", e)), }; } }; let Some(socket_addr) = socket_addr else { return PortCheckResult { port, accessible: false, latency_ms: None, error: Some("Failed to resolve address".to_string()), }; }; // Try to connect with timeout let start = Instant::now(); // Use a simple TCP connect with timeout simulation let result = TcpStream::connect_timeout(&socket_addr, Duration::from_millis(timeout_ms)); match result { Ok(_) => { let latency = start.elapsed().as_millis() as u64; PortCheckResult { port, accessible: true, latency_ms: Some(latency), error: None, } } Err(e) => PortCheckResult { port, accessible: false, latency_ms: None, error: Some(format!("Connection failed: {}", e)), }, } } /// Get process uptime from status command fn get_process_uptime(status: &LocalGatewayStatus) -> Option { // Try to extract uptime from raw status data status .raw .get("process") .and_then(|p| p.get("uptimeSeconds")) .and_then(Value::as_u64) } /// Perform comprehensive health check on OpenFang Kernel #[tauri::command] fn openfang_health_check( app: AppHandle, port: Option, timeout_ms: Option, ) -> Result { let check_port = port.unwrap_or(OPENFANG_DEFAULT_PORT); let timeout = timeout_ms.unwrap_or(3000); let mut checks_performed = Vec::new(); let mut issues = Vec::new(); // Get current timestamp let last_check_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); // 1. Check if OpenFang CLI is available let runtime = resolve_openfang_runtime(&app); let cli_available = runtime.executable.is_file(); if !cli_available { return Ok(HealthCheckResponse { status: HealthStatus::Unhealthy, process: ProcessHealthDetails { pid: None, name: None, status: None, uptime_seconds: None, cpu_percent: None, memory_mb: None, }, port_check: PortCheckResult { port: check_port, accessible: false, latency_ms: None, error: Some("OpenFang CLI not available".to_string()), }, last_check_timestamp, checks_performed: vec!["cli_availability".to_string()], issues: vec![format!( "OpenFang runtime not found at: {}", runtime.display_path.display() )], runtime_source: Some(runtime.source), }); } checks_performed.push("cli_availability".to_string()); // 2. Get gateway status let gateway_status = read_gateway_status(&app)?; checks_performed.push("gateway_status".to_string()); // Check for configuration issues if !gateway_status.config_ok { issues.push("Gateway configuration has issues".to_string()); } // 3. Check port accessibility let port_check = check_port_accessibility("127.0.0.1", check_port, timeout); checks_performed.push("port_accessibility".to_string()); if !port_check.accessible { issues.push(format!( "Port {} is not accessible: {}", check_port, port_check.error.as_deref().unwrap_or("unknown error") )); } // 4. Extract process information let process_health = if !gateway_status.listener_pids.is_empty() { // Get the first listener PID let pid = gateway_status.listener_pids[0]; // Try to get detailed process info from process list let process_info = run_openfang(&app, &["process", "list", "--json"]) .ok() .and_then(|result| parse_json_output(&result.stdout).ok()) .and_then(|json| json.get("processes").and_then(Value::as_array).cloned()); let (cpu, memory, uptime) = if let Some(ref processes) = process_info { let matching = processes .iter() .find(|p| p.get("pid").and_then(Value::as_u64) == Some(pid as u64)); matching.map_or((None, None, None), |p| { ( p.get("cpuPercent").and_then(Value::as_f64), p.get("memoryMb").and_then(Value::as_f64), p.get("uptimeSeconds").and_then(Value::as_u64), ) }) } else { (None, None, get_process_uptime(&gateway_status)) }; ProcessHealthDetails { pid: Some(pid), name: Some("openfang".to_string()), status: Some( gateway_status .service_status .clone() .unwrap_or_else(|| "running".to_string()), ), uptime_seconds: uptime, cpu_percent: cpu, memory_mb: memory, } } else { ProcessHealthDetails { pid: None, name: None, status: gateway_status.service_status.clone(), uptime_seconds: None, cpu_percent: None, memory_mb: None, } }; // Check if process is running but no listeners if gateway_status.service_status.as_deref() == Some("running") && gateway_status.listener_pids.is_empty() { issues.push("Service reports running but no listener processes found".to_string()); } // 5. Determine overall health status let status = if !cli_available { HealthStatus::Unhealthy } else if !port_check.accessible { HealthStatus::Unhealthy } else if gateway_status.listener_pids.is_empty() { HealthStatus::Unhealthy } else if !issues.is_empty() { // Has some issues but core functionality is working HealthStatus::Healthy } else { HealthStatus::Healthy }; Ok(HealthCheckResponse { status, process: process_health, port_check, last_check_timestamp, checks_performed, issues, runtime_source: Some(runtime.source), }) } /// Quick ping to check if OpenFang is alive (lightweight check) #[tauri::command] fn openfang_ping(app: AppHandle) -> Result { let port_check = check_port_accessibility("127.0.0.1", OPENFANG_DEFAULT_PORT, 1000); if port_check.accessible { return Ok(true); } // Fallback: check via status command match run_openfang(&app, &["gateway", "status", "--json", "--no-probe"]) { Ok(result) => { if let Ok(status) = parse_json_output(&result.stdout) { // Check if there are any listener PIDs let has_listeners = status .get("port") .and_then(|p| p.get("listeners")) .and_then(Value::as_array) .map(|arr| !arr.is_empty()) .unwrap_or(false); Ok(has_listeners) } else { Ok(false) } } Err(_) => Ok(false), } } // ============================================================================ // Backward-compatible aliases (OpenClaw naming) // These delegate to OpenFang commands for backward compatibility // ============================================================================ #[tauri::command] fn gateway_status(app: AppHandle) -> Result { openfang_status(app) } #[tauri::command] fn gateway_start(app: AppHandle) -> Result { openfang_start(app) } #[tauri::command] fn gateway_stop(app: AppHandle) -> Result { openfang_stop(app) } #[tauri::command] fn gateway_restart(app: AppHandle) -> Result { openfang_restart(app) } #[tauri::command] fn gateway_local_auth() -> Result { openfang_local_auth() } #[tauri::command] fn gateway_prepare_for_tauri(app: AppHandle) -> Result { openfang_prepare_for_tauri(app) } #[tauri::command] fn gateway_approve_device_pairing( app: AppHandle, device_id: String, public_key_base64: String, url: Option, ) -> Result { openfang_approve_device_pairing(app, device_id, public_key_base64, url) } #[tauri::command] fn gateway_doctor(app: AppHandle) -> Result { openfang_doctor(app) } #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { // Initialize browser state let browser_state = browser::commands::BrowserState::new(); // Initialize memory store state let memory_state: memory_commands::MemoryStoreState = std::sync::Arc::new(tokio::sync::Mutex::new(None)); // Initialize intelligence layer state let heartbeat_state: intelligence::HeartbeatEngineState = std::sync::Arc::new(tokio::sync::Mutex::new(std::collections::HashMap::new())); let reflection_state: intelligence::ReflectionEngineState = std::sync::Arc::new(tokio::sync::Mutex::new(intelligence::ReflectionEngine::new(None))); let identity_state: intelligence::IdentityManagerState = std::sync::Arc::new(tokio::sync::Mutex::new(intelligence::AgentIdentityManager::new())); tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .manage(browser_state) .manage(memory_state) .manage(heartbeat_state) .manage(reflection_state) .manage(identity_state) .invoke_handler(tauri::generate_handler![ // OpenFang commands (new naming) openfang_status, openfang_start, openfang_stop, openfang_restart, openfang_local_auth, openfang_prepare_for_tauri, openfang_approve_device_pairing, openfang_doctor, openfang_health_check, // Process monitoring commands openfang_process_list, openfang_process_logs, openfang_version, // Health check commands openfang_health_check, openfang_ping, // Backward-compatible aliases (OpenClaw naming) gateway_status, gateway_start, gateway_stop, gateway_restart, gateway_local_auth, gateway_prepare_for_tauri, gateway_approve_device_pairing, gateway_doctor, // OpenViking CLI sidecar commands viking_commands::viking_status, viking_commands::viking_add, viking_commands::viking_add_inline, viking_commands::viking_find, viking_commands::viking_grep, viking_commands::viking_ls, viking_commands::viking_read, viking_commands::viking_remove, viking_commands::viking_tree, // Viking server management (local deployment) viking_server::viking_server_status, viking_server::viking_server_start, viking_server::viking_server_stop, viking_server::viking_server_restart, // Memory extraction commands (supplement CLI) memory::extractor::extract_session_memories, memory::context_builder::estimate_content_tokens, // LLM commands (for extraction) llm::llm_complete, // Browser automation commands (Fantoccini-based Browser Hand) browser::commands::browser_create_session, browser::commands::browser_close_session, browser::commands::browser_list_sessions, browser::commands::browser_get_session, browser::commands::browser_navigate, browser::commands::browser_back, browser::commands::browser_forward, browser::commands::browser_refresh, browser::commands::browser_get_url, browser::commands::browser_get_title, browser::commands::browser_find_element, browser::commands::browser_find_elements, browser::commands::browser_click, browser::commands::browser_type, browser::commands::browser_get_text, browser::commands::browser_get_attribute, browser::commands::browser_wait_for_element, browser::commands::browser_execute_script, browser::commands::browser_screenshot, browser::commands::browser_element_screenshot, browser::commands::browser_get_source, browser::commands::browser_scrape_page, browser::commands::browser_fill_form, // Secure storage commands (OS keyring/keychain) secure_storage::secure_store_set, secure_storage::secure_store_get, secure_storage::secure_store_delete, secure_storage::secure_store_is_available, // Memory persistence commands (Phase 1 Intelligence Layer Migration) memory_commands::memory_init, memory_commands::memory_store, memory_commands::memory_get, memory_commands::memory_search, memory_commands::memory_delete, memory_commands::memory_delete_all, memory_commands::memory_stats, memory_commands::memory_export, memory_commands::memory_import, memory_commands::memory_db_path, // Intelligence Layer commands (Phase 2-3) // Heartbeat Engine intelligence::heartbeat::heartbeat_init, intelligence::heartbeat::heartbeat_start, intelligence::heartbeat::heartbeat_stop, intelligence::heartbeat::heartbeat_tick, intelligence::heartbeat::heartbeat_get_config, intelligence::heartbeat::heartbeat_update_config, intelligence::heartbeat::heartbeat_get_history, // Context Compactor intelligence::compactor::compactor_estimate_tokens, intelligence::compactor::compactor_estimate_messages_tokens, intelligence::compactor::compactor_check_threshold, intelligence::compactor::compactor_compact, // Reflection Engine intelligence::reflection::reflection_init, intelligence::reflection::reflection_record_conversation, intelligence::reflection::reflection_should_reflect, intelligence::reflection::reflection_reflect, intelligence::reflection::reflection_get_history, intelligence::reflection::reflection_get_state, // Agent Identity Manager intelligence::identity::identity_get, intelligence::identity::identity_get_file, intelligence::identity::identity_build_prompt, intelligence::identity::identity_update_user_profile, intelligence::identity::identity_append_user_profile, intelligence::identity::identity_propose_change, intelligence::identity::identity_approve_proposal, intelligence::identity::identity_reject_proposal, intelligence::identity::identity_get_pending_proposals, intelligence::identity::identity_update_file, intelligence::identity::identity_get_snapshots, intelligence::identity::identity_restore_snapshot, intelligence::identity::identity_list_agents, intelligence::identity::identity_delete_agent ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); }