{"repo": "bigmoostache/context-pilot", "files": {"crates/cp-orchestrator/src/transport/inspect/vitals/mod.rs": {"1664bb6d4b541b942885e38079c2f1915e6f19f0": {"t": 1785746244.9934902, "n": 14560, "h": "f512379b35f58f5e", "c": "//! On-demand service-connectivity **vitals** endpoint (`GET\n//! /api/agent/{id}/vitals`).\n//!\n//! Where [`metrics`](super::metrics) reports the *durable* observability slice\n//! the backend already holds (stream health, rev lag), this endpoint\n//! answers a different, *live* question the operator asks by pressing a button:\n//! \"can each service this agent depends on actually be reached right now?\"\n//!\n//! It runs **real probes** and reports each as `ok` / `error` / `unavailable`,\n//! and — crucially — never fabricates a green. A check the backend genuinely\n//! cannot perform from where it sits returns `unavailable` with an honest\n//! reason, mirroring the inspection plane's derived-state contract.\n//!\n//! # What is probed, and how honestly\n//!\n//! | Service | Probe | Plane it proves |\n//! |---|---|---|\n//! | Orchestrator | self (we are responding) | always `ok` |\n//! | Agent main loop (connection) | heartbeat file freshness + `boot_id` match | the agent process is alive |\n//! | Agent loop status | folded phase + lifecycle from the view | a *status* read, not a connection |\n//! | LLM provider | TCP connect to the provider host `:443` | network path to the picked provider |\n//! | Voyage / Datalab / Brave / Firecrawl | TCP connect to the API host `:443` | network path to the capability |\n//! | Meilisearch | TCP connect to `127.0.0.1:` | the local search daemon is up |\n//! | Console server | `connect()` the agent's `console/server.sock` | the per-agent console daemon is up |\n//!\n//! A TCP connect to `:443` is a deliberate, key-independent reachability check:\n//! it proves DNS + routing + the remote listener without spending a token or\n//! needing the agent's API key (which lives in the agent's environment, not the\n//! orchestrator's). It answers exactly the \"connection to …\" question asked.\n//!\n//! The frontend adds the two checks only *it* can observe (the dev server is up\n//! because it is running, and the round-trip latency of this very request), so\n//! the rendered table covers all twelve services.\n\nuse std::net::{TcpStream, ToSocketAddrs};\nuse std::path::{Path, PathBuf};\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse cp_wire::heartbeat::{DEFAULT_MAX_AGE, Heartbeat};\nuse cp_wire::types::registry::Entry;\n\nuse crate::transport::Backend;\nuse crate::transport::rest::HttpReply;\n\n/// Authenticated API-key validation probes (GitHub username, Brave/Firecrawl/\n/// Voyage/Datalab account info) — proves each key is *valid*, not just that the\n/// host is reachable. See [`keychecks`] for the honesty contract.\nmod keychecks;\n\n/// Per-probe network timeout. Bounds DNS+connect for a single remote check.\nconst PROBE_TIMEOUT: Duration = Duration::from_secs(3);\n\n/// Overall deadline for collecting every concurrent network probe. A probe that\n/// has not reported by this point (e.g. a stalled DNS resolver) is filled in as\n/// a timeout rather than hanging the endpoint.\nconst COLLECT_DEADLINE: Duration = Duration::from_secs(6);\n\n/// `GET /api/agent/{id}/vitals` — run live connectivity checks for one agent.\n///\n/// Returns a JSON array of vital objects (`{name, category, status, latencyMs?,\n/// detail?}`). Unknown agent → `404`.\npub fn agent_vitals(state: &Mutex, agent_id: &str) -> HttpReply {\n let entry = match crate::transport::rest::resolve_entry(state, agent_id) {\n Ok(e) => e,\n Err(reply) => return reply,\n };\n\n let mut vitals: Vec = Vec::new();\n\n // 1. Orchestrator — we are responding, so it is trivially up.\n vitals.push(vital(\"Orchestrator\", \"orchestrator\", \"ok\", Some(0), \"serving\"));\n\n // 2-3. Agent-side reads (heartbeat + folded status), under one short lock.\n let provider = read_provider(state, &entry);\n vitals.push(agent_connection_vital(&entry));\n vitals.push(agent_status_vital(state, agent_id));\n\n // 4. Local infra probes that need a discovered address.\n vitals.push(meilisearch_vital());\n vitals.push(console_vital(&entry.folder));\n\n // 5. LLM provider reachability — a key-independent TCP probe (the provider\n // picked in the agent's config; no cheap whoami exists to validate it).\n let (provider_label, provider_host) = provider_host(&provider);\n let remotes: Vec<(String, String, String)> =\n vec![(provider_label.to_owned(), \"llm\".to_owned(), provider_host.to_owned())];\n vitals.extend(probe_remotes(remotes));\n\n // 6. Keyed services (GitHub, Brave, Firecrawl, Voyage, Datalab) — real\n // authenticated probes that prove the configured API key is valid and\n // report minimal account/identity info (username, credits, quota).\n vitals.extend(keychecks::probe_keyed_services());\n\n HttpReply::ok(&vitals)\n}\n\n// ── Agent-side checks ───────────────────────────────────────────────────\n\n/// Read the agent's configured LLM provider id from its `config.json`\n/// (`modules.core.llm_provider`), defaulting to `anthropic` when unreadable.\nfn read_provider(state: &Mutex, entry: &Entry) -> String {\n let folder = PathBuf::from(&entry.folder);\n let Ok(mut b) = state.lock() else {\n return \"anthropic\".to_owned();\n };\n b.inspect_mut()\n .read_config(&folder)\n .ok()\n .and_then(|cfg| {\n cfg.get(\"modules\")\n .and_then(|m| m.get(\"core\"))\n .and_then(|c| c.get(\"llm_provider\"))\n .and_then(serde_json::Value::as_str)\n .map(str::to_owned)\n })\n .unwrap_or_else(|| \"anthropic\".to_owned())\n}\n\n/// Probe the agent main-loop *connection* via its heartbeat file: a fresh beat\n/// whose `boot_id` matches the registry record means the process is alive.\nfn agent_connection_vital(entry: &Entry) -> serde_json::Value {\n let Ok(bytes) = std::fs::read(&entry.heartbeat_path) else {\n return vital(\"Agent main loop\", \"agent\", \"unavailable\", None, \"no heartbeat file\");\n };\n let Ok(hb) = Heartbeat::decode(&bytes) else {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"corrupt heartbeat\");\n };\n let now_ms = now_ms();\n let max_age_ms = u64::try_from(DEFAULT_MAX_AGE.as_millis()).unwrap_or(5_000);\n let age_ms = now_ms.saturating_sub(hb.timestamp_ms);\n if !hb.matches_boot(&entry.boot_id) {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"boot_id mismatch (pid reused)\");\n }\n if hb.is_fresh(now_ms, max_age_ms) {\n vital(\"Agent main loop\", \"agent\", \"ok\", None, &format!(\"heartbeat {age_ms}ms old\"))\n } else {\n vital(\"Agent main loop\", \"agent\", \"error\", None, &format!(\"stale heartbeat ({age_ms}ms old)\"))\n }\n}\n\n/// Report the agent loop's *status* (phase + lifecycle from the folded view).\n///\n/// This is a status read, not a connection: `ok` carries the live phase as its\n/// detail; a `Stopped`/`Stopping` lifecycle surfaces as `error`.\nfn agent_status_vital(state: &Mutex, agent_id: &str) -> serde_json::Value {\n let snapshot = state.lock().ok().and_then(|b| b.view.get(agent_id).map(|v| (v.phase, v.lifecycle)));\n let Some((phase, lifecycle)) = snapshot else {\n return vital(\"Agent loop status\", \"agent\", \"unavailable\", None, \"no view yet\");\n };\n let phase_str = phase.map_or(\"idle\", phase_label);\n let life_str = lifecycle.map(lifecycle_label);\n let stopped = matches!(life_str, Some(\"stopping\" | \"stopped\"));\n let detail = match life_str {\n Some(l) => format!(\"{phase_str} · {l}\"),\n None => phase_str.to_owned(),\n };\n let status = if stopped { \"error\" } else { \"ok\" };\n vital(\"Agent loop status\", \"agent\", status, None, &detail)\n}\n\n// ── Local infra checks ──────────────────────────────────────────────────\n\n/// Probe Meilisearch by TCP-connecting to the port it advertises in\n/// `~/.context-pilot/meilisearch/port`. No port file → it was never started.\nfn meilisearch_vital() -> serde_json::Value {\n let Some(port) = meili_port() else {\n return vital(\"Meilisearch\", \"infra\", \"unavailable\", None, \"no port file (not started)\");\n };\n let started = Instant::now();\n match TcpStream::connect_timeout(&(std::net::Ipv4Addr::LOCALHOST, port).into(), PROBE_TIMEOUT) {\n Ok(_) => vital(\"Meilisearch\", \"infra\", \"ok\", Some(elapsed_ms(started)), &format!(\"127.0.0.1:{port}\")),\n Err(e) => vital(\"Meilisearch\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n/// Read the Meilisearch port from `~/.context-pilot/meilisearch/port`.\nfn meili_port() -> Option {\n let path = home_dir()?.join(\".context-pilot/meilisearch/port\");\n std::fs::read_to_string(path).ok()?.trim().parse().ok()\n}\n\n/// Probe the agent's console server by connecting its Unix socket at\n/// `/.context-pilot/console/server.sock`.\nfn console_vital(folder: &str) -> serde_json::Value {\n let sock = Path::new(folder).join(\".context-pilot/console/server.sock\");\n if !sock.exists() {\n return vital(\"Console server\", \"infra\", \"unavailable\", None, \"no socket (not started)\");\n }\n let started = Instant::now();\n match std::os::unix::net::UnixStream::connect(&sock) {\n Ok(_) => vital(\"Console server\", \"infra\", \"ok\", Some(elapsed_ms(started)), \"socket connected\"),\n Err(e) => vital(\"Console server\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n// ── Remote reachability probes ──────────────────────────────────────────\n\n/// Run every remote host probe concurrently and collect them under a deadline.\n///\n/// Each probe runs on its own thread (so the wall time is the slowest single\n/// probe, not their sum) and reports over a channel. Any probe that misses the\n/// [`COLLECT_DEADLINE`] — e.g. a wedged DNS resolver — is filled in as a\n/// timeout so the endpoint always returns promptly.\nfn probe_remotes(remotes: Vec<(String, String, String)>) -> Vec {\n let total = remotes.len();\n let (tx, rx) = mpsc::channel::<(usize, serde_json::Value)>();\n for (idx, (label, category, host)) in remotes.into_iter().enumerate() {\n let tx = tx.clone();\n let _handle = thread::spawn(move || {\n let v = tcp_check(&label, &category, &host, 443);\n let _sent = tx.send((idx, v));\n });\n }\n drop(tx); // so the channel closes once every probe thread has reported\n\n let mut slots: Vec> = (0..total).map(|_| None).collect();\n let deadline = Instant::now() + COLLECT_DEADLINE;\n let mut received = 0;\n while received < total {\n let remaining = deadline.saturating_duration_since(Instant::now());\n if remaining.is_zero() {\n break;\n }\n match rx.recv_timeout(remaining) {\n Ok((idx, v)) => {\n if let Some(slot) = slots.get_mut(idx) {\n *slot = Some(v);\n received += 1;\n }\n }\n Err(_) => break, // deadline elapsed or all senders dropped\n }\n }\n\n slots\n .into_iter()\n .map(|s| s.unwrap_or_else(|| vital(\"Service\", \"service\", \"unavailable\", None, \"probe timed out\")))\n .collect()\n}\n\n/// TCP-connect `host:port` with a bounded timeout — a key-independent\n/// reachability check (proves DNS + routing + a live listener).\nfn tcp_check(label: &str, category: &str, host: &str, port: u16) -> serde_json::Value {\n let started = Instant::now();\n let addrs = match (host, port).to_socket_addrs() {\n Ok(it) => it,\n Err(e) => return vital(label, category, \"error\", None, &format!(\"dns failed: {e}\")),\n };\n let Some(addr) = addrs.into_iter().next() else {\n return vital(label, category, \"error\", None, \"dns resolved to no address\");\n };\n match TcpStream::connect_timeout(&addr, PROBE_TIMEOUT) {\n Ok(_) => vital(label, category, \"ok\", Some(elapsed_ms(started)), &format!(\"{host}:{port} reachable\")),\n Err(e) => vital(label, category, \"error\", None, &format!(\"{host}:{port} {e}\")),\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────\n\n/// Map a provider id (`modules.core.llm_provider`) to a display label + the API\n/// host whose `:443` reachability stands in for \"can we reach the provider\".\nfn provider_host(provider: &str) -> (&'static str, &'static str) {\n match provider {\n \"grok\" => (\"xAI Grok\", \"api.x.ai\"),\n \"groq\" => (\"Groq\", \"api.groq.com\"),\n \"deepseek\" => (\"DeepSeek\", \"api.deepseek.com\"),\n \"minimax\" => (\"MiniMax\", \"api.minimax.io\"),\n // anthropic + all claude-code variants hit the Anthropic API host.\n _ => (\"Anthropic\", \"api.anthropic.com\"),\n }\n}\n\n/// Wire label for a folded [`Phase`](cp_wire::types::Phase).\nfn phase_label(phase: cp_wire::types::Phase) -> &'static str {\n match phase {\n cp_wire::types::Phase::Idle => \"idle\",\n cp_wire::types::Phase::Streaming => \"streaming\",\n cp_wire::types::Phase::Tooling => \"tooling\",\n // `Phase` is #[non_exhaustive]; a phase from a newer agent protocol\n // folds to a neutral label rather than failing the build.\n _ => \"unknown\",\n }\n}\n\n/// Wire label for a folded [`LifecycleState`](cp_wire::types::LifecycleState).\nfn lifecycle_label(state: cp_wire::types::LifecycleState) -> &'static str {\n match state {\n cp_wire::types::LifecycleState::Starting => \"starting\",\n cp_wire::types::LifecycleState::Running => \"running\",\n cp_wire::types::LifecycleState::Stopping => \"stopping\",\n cp_wire::types::LifecycleState::Stopped => \"stopped\",\n // `LifecycleState` is #[non_exhaustive]; a state from a newer agent\n // protocol folds to a neutral label rather than failing the build.\n _ => \"unknown\",\n }\n}\n\n/// Build one vital JSON object.\nfn vital(name: &str, category: &str, status: &str, latency_ms: Option, detail: &str) -> serde_json::Value {\n serde_json::json!({\n \"name\": name,\n \"category\": category,\n \"status\": status,\n \"latencyMs\": latency_ms,\n \"detail\": detail,\n })\n}\n\n/// Milliseconds elapsed since `started`, saturating into `u64`.\nfn elapsed_ms(started: Instant) -> u64 {\n u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)\n}\n\n/// Current wall-clock time in epoch milliseconds.\nfn now_ms() -> u64 {\n SystemTime::now().duration_since(UNIX_EPOCH).ok().and_then(|d| u64::try_from(d.as_millis()).ok()).unwrap_or(0)\n}\n\n/// The user's home directory (`$HOME`), if set.\nfn home_dir() -> Option {\n std::env::var_os(\"HOME\").map(PathBuf::from)\n}\n"}, "8c498d0d68e501f48bd245c7087ccce99d777fdc": {"t": 1785761041.3771114, "n": 14560, "h": "f512379b35f58f5e", "c": "//! On-demand service-connectivity **vitals** endpoint (`GET\n//! /api/agent/{id}/vitals`).\n//!\n//! Where [`metrics`](super::metrics) reports the *durable* observability slice\n//! the backend already holds (stream health, rev lag), this endpoint\n//! answers a different, *live* question the operator asks by pressing a button:\n//! \"can each service this agent depends on actually be reached right now?\"\n//!\n//! It runs **real probes** and reports each as `ok` / `error` / `unavailable`,\n//! and — crucially — never fabricates a green. A check the backend genuinely\n//! cannot perform from where it sits returns `unavailable` with an honest\n//! reason, mirroring the inspection plane's derived-state contract.\n//!\n//! # What is probed, and how honestly\n//!\n//! | Service | Probe | Plane it proves |\n//! |---|---|---|\n//! | Orchestrator | self (we are responding) | always `ok` |\n//! | Agent main loop (connection) | heartbeat file freshness + `boot_id` match | the agent process is alive |\n//! | Agent loop status | folded phase + lifecycle from the view | a *status* read, not a connection |\n//! | LLM provider | TCP connect to the provider host `:443` | network path to the picked provider |\n//! | Voyage / Datalab / Brave / Firecrawl | TCP connect to the API host `:443` | network path to the capability |\n//! | Meilisearch | TCP connect to `127.0.0.1:` | the local search daemon is up |\n//! | Console server | `connect()` the agent's `console/server.sock` | the per-agent console daemon is up |\n//!\n//! A TCP connect to `:443` is a deliberate, key-independent reachability check:\n//! it proves DNS + routing + the remote listener without spending a token or\n//! needing the agent's API key (which lives in the agent's environment, not the\n//! orchestrator's). It answers exactly the \"connection to …\" question asked.\n//!\n//! The frontend adds the two checks only *it* can observe (the dev server is up\n//! because it is running, and the round-trip latency of this very request), so\n//! the rendered table covers all twelve services.\n\nuse std::net::{TcpStream, ToSocketAddrs};\nuse std::path::{Path, PathBuf};\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse cp_wire::heartbeat::{DEFAULT_MAX_AGE, Heartbeat};\nuse cp_wire::types::registry::Entry;\n\nuse crate::transport::Backend;\nuse crate::transport::rest::HttpReply;\n\n/// Authenticated API-key validation probes (GitHub username, Brave/Firecrawl/\n/// Voyage/Datalab account info) — proves each key is *valid*, not just that the\n/// host is reachable. See [`keychecks`] for the honesty contract.\nmod keychecks;\n\n/// Per-probe network timeout. Bounds DNS+connect for a single remote check.\nconst PROBE_TIMEOUT: Duration = Duration::from_secs(3);\n\n/// Overall deadline for collecting every concurrent network probe. A probe that\n/// has not reported by this point (e.g. a stalled DNS resolver) is filled in as\n/// a timeout rather than hanging the endpoint.\nconst COLLECT_DEADLINE: Duration = Duration::from_secs(6);\n\n/// `GET /api/agent/{id}/vitals` — run live connectivity checks for one agent.\n///\n/// Returns a JSON array of vital objects (`{name, category, status, latencyMs?,\n/// detail?}`). Unknown agent → `404`.\npub fn agent_vitals(state: &Mutex, agent_id: &str) -> HttpReply {\n let entry = match crate::transport::rest::resolve_entry(state, agent_id) {\n Ok(e) => e,\n Err(reply) => return reply,\n };\n\n let mut vitals: Vec = Vec::new();\n\n // 1. Orchestrator — we are responding, so it is trivially up.\n vitals.push(vital(\"Orchestrator\", \"orchestrator\", \"ok\", Some(0), \"serving\"));\n\n // 2-3. Agent-side reads (heartbeat + folded status), under one short lock.\n let provider = read_provider(state, &entry);\n vitals.push(agent_connection_vital(&entry));\n vitals.push(agent_status_vital(state, agent_id));\n\n // 4. Local infra probes that need a discovered address.\n vitals.push(meilisearch_vital());\n vitals.push(console_vital(&entry.folder));\n\n // 5. LLM provider reachability — a key-independent TCP probe (the provider\n // picked in the agent's config; no cheap whoami exists to validate it).\n let (provider_label, provider_host) = provider_host(&provider);\n let remotes: Vec<(String, String, String)> =\n vec![(provider_label.to_owned(), \"llm\".to_owned(), provider_host.to_owned())];\n vitals.extend(probe_remotes(remotes));\n\n // 6. Keyed services (GitHub, Brave, Firecrawl, Voyage, Datalab) — real\n // authenticated probes that prove the configured API key is valid and\n // report minimal account/identity info (username, credits, quota).\n vitals.extend(keychecks::probe_keyed_services());\n\n HttpReply::ok(&vitals)\n}\n\n// ── Agent-side checks ───────────────────────────────────────────────────\n\n/// Read the agent's configured LLM provider id from its `config.json`\n/// (`modules.core.llm_provider`), defaulting to `anthropic` when unreadable.\nfn read_provider(state: &Mutex, entry: &Entry) -> String {\n let folder = PathBuf::from(&entry.folder);\n let Ok(mut b) = state.lock() else {\n return \"anthropic\".to_owned();\n };\n b.inspect_mut()\n .read_config(&folder)\n .ok()\n .and_then(|cfg| {\n cfg.get(\"modules\")\n .and_then(|m| m.get(\"core\"))\n .and_then(|c| c.get(\"llm_provider\"))\n .and_then(serde_json::Value::as_str)\n .map(str::to_owned)\n })\n .unwrap_or_else(|| \"anthropic\".to_owned())\n}\n\n/// Probe the agent main-loop *connection* via its heartbeat file: a fresh beat\n/// whose `boot_id` matches the registry record means the process is alive.\nfn agent_connection_vital(entry: &Entry) -> serde_json::Value {\n let Ok(bytes) = std::fs::read(&entry.heartbeat_path) else {\n return vital(\"Agent main loop\", \"agent\", \"unavailable\", None, \"no heartbeat file\");\n };\n let Ok(hb) = Heartbeat::decode(&bytes) else {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"corrupt heartbeat\");\n };\n let now_ms = now_ms();\n let max_age_ms = u64::try_from(DEFAULT_MAX_AGE.as_millis()).unwrap_or(5_000);\n let age_ms = now_ms.saturating_sub(hb.timestamp_ms);\n if !hb.matches_boot(&entry.boot_id) {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"boot_id mismatch (pid reused)\");\n }\n if hb.is_fresh(now_ms, max_age_ms) {\n vital(\"Agent main loop\", \"agent\", \"ok\", None, &format!(\"heartbeat {age_ms}ms old\"))\n } else {\n vital(\"Agent main loop\", \"agent\", \"error\", None, &format!(\"stale heartbeat ({age_ms}ms old)\"))\n }\n}\n\n/// Report the agent loop's *status* (phase + lifecycle from the folded view).\n///\n/// This is a status read, not a connection: `ok` carries the live phase as its\n/// detail; a `Stopped`/`Stopping` lifecycle surfaces as `error`.\nfn agent_status_vital(state: &Mutex, agent_id: &str) -> serde_json::Value {\n let snapshot = state.lock().ok().and_then(|b| b.view.get(agent_id).map(|v| (v.phase, v.lifecycle)));\n let Some((phase, lifecycle)) = snapshot else {\n return vital(\"Agent loop status\", \"agent\", \"unavailable\", None, \"no view yet\");\n };\n let phase_str = phase.map_or(\"idle\", phase_label);\n let life_str = lifecycle.map(lifecycle_label);\n let stopped = matches!(life_str, Some(\"stopping\" | \"stopped\"));\n let detail = match life_str {\n Some(l) => format!(\"{phase_str} · {l}\"),\n None => phase_str.to_owned(),\n };\n let status = if stopped { \"error\" } else { \"ok\" };\n vital(\"Agent loop status\", \"agent\", status, None, &detail)\n}\n\n// ── Local infra checks ──────────────────────────────────────────────────\n\n/// Probe Meilisearch by TCP-connecting to the port it advertises in\n/// `~/.context-pilot/meilisearch/port`. No port file → it was never started.\nfn meilisearch_vital() -> serde_json::Value {\n let Some(port) = meili_port() else {\n return vital(\"Meilisearch\", \"infra\", \"unavailable\", None, \"no port file (not started)\");\n };\n let started = Instant::now();\n match TcpStream::connect_timeout(&(std::net::Ipv4Addr::LOCALHOST, port).into(), PROBE_TIMEOUT) {\n Ok(_) => vital(\"Meilisearch\", \"infra\", \"ok\", Some(elapsed_ms(started)), &format!(\"127.0.0.1:{port}\")),\n Err(e) => vital(\"Meilisearch\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n/// Read the Meilisearch port from `~/.context-pilot/meilisearch/port`.\nfn meili_port() -> Option {\n let path = home_dir()?.join(\".context-pilot/meilisearch/port\");\n std::fs::read_to_string(path).ok()?.trim().parse().ok()\n}\n\n/// Probe the agent's console server by connecting its Unix socket at\n/// `/.context-pilot/console/server.sock`.\nfn console_vital(folder: &str) -> serde_json::Value {\n let sock = Path::new(folder).join(\".context-pilot/console/server.sock\");\n if !sock.exists() {\n return vital(\"Console server\", \"infra\", \"unavailable\", None, \"no socket (not started)\");\n }\n let started = Instant::now();\n match std::os::unix::net::UnixStream::connect(&sock) {\n Ok(_) => vital(\"Console server\", \"infra\", \"ok\", Some(elapsed_ms(started)), \"socket connected\"),\n Err(e) => vital(\"Console server\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n// ── Remote reachability probes ──────────────────────────────────────────\n\n/// Run every remote host probe concurrently and collect them under a deadline.\n///\n/// Each probe runs on its own thread (so the wall time is the slowest single\n/// probe, not their sum) and reports over a channel. Any probe that misses the\n/// [`COLLECT_DEADLINE`] — e.g. a wedged DNS resolver — is filled in as a\n/// timeout so the endpoint always returns promptly.\nfn probe_remotes(remotes: Vec<(String, String, String)>) -> Vec {\n let total = remotes.len();\n let (tx, rx) = mpsc::channel::<(usize, serde_json::Value)>();\n for (idx, (label, category, host)) in remotes.into_iter().enumerate() {\n let tx = tx.clone();\n let _handle = thread::spawn(move || {\n let v = tcp_check(&label, &category, &host, 443);\n let _sent = tx.send((idx, v));\n });\n }\n drop(tx); // so the channel closes once every probe thread has reported\n\n let mut slots: Vec> = (0..total).map(|_| None).collect();\n let deadline = Instant::now() + COLLECT_DEADLINE;\n let mut received = 0;\n while received < total {\n let remaining = deadline.saturating_duration_since(Instant::now());\n if remaining.is_zero() {\n break;\n }\n match rx.recv_timeout(remaining) {\n Ok((idx, v)) => {\n if let Some(slot) = slots.get_mut(idx) {\n *slot = Some(v);\n received += 1;\n }\n }\n Err(_) => break, // deadline elapsed or all senders dropped\n }\n }\n\n slots\n .into_iter()\n .map(|s| s.unwrap_or_else(|| vital(\"Service\", \"service\", \"unavailable\", None, \"probe timed out\")))\n .collect()\n}\n\n/// TCP-connect `host:port` with a bounded timeout — a key-independent\n/// reachability check (proves DNS + routing + a live listener).\nfn tcp_check(label: &str, category: &str, host: &str, port: u16) -> serde_json::Value {\n let started = Instant::now();\n let addrs = match (host, port).to_socket_addrs() {\n Ok(it) => it,\n Err(e) => return vital(label, category, \"error\", None, &format!(\"dns failed: {e}\")),\n };\n let Some(addr) = addrs.into_iter().next() else {\n return vital(label, category, \"error\", None, \"dns resolved to no address\");\n };\n match TcpStream::connect_timeout(&addr, PROBE_TIMEOUT) {\n Ok(_) => vital(label, category, \"ok\", Some(elapsed_ms(started)), &format!(\"{host}:{port} reachable\")),\n Err(e) => vital(label, category, \"error\", None, &format!(\"{host}:{port} {e}\")),\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────\n\n/// Map a provider id (`modules.core.llm_provider`) to a display label + the API\n/// host whose `:443` reachability stands in for \"can we reach the provider\".\nfn provider_host(provider: &str) -> (&'static str, &'static str) {\n match provider {\n \"grok\" => (\"xAI Grok\", \"api.x.ai\"),\n \"groq\" => (\"Groq\", \"api.groq.com\"),\n \"deepseek\" => (\"DeepSeek\", \"api.deepseek.com\"),\n \"minimax\" => (\"MiniMax\", \"api.minimax.io\"),\n // anthropic + all claude-code variants hit the Anthropic API host.\n _ => (\"Anthropic\", \"api.anthropic.com\"),\n }\n}\n\n/// Wire label for a folded [`Phase`](cp_wire::types::Phase).\nfn phase_label(phase: cp_wire::types::Phase) -> &'static str {\n match phase {\n cp_wire::types::Phase::Idle => \"idle\",\n cp_wire::types::Phase::Streaming => \"streaming\",\n cp_wire::types::Phase::Tooling => \"tooling\",\n // `Phase` is #[non_exhaustive]; a phase from a newer agent protocol\n // folds to a neutral label rather than failing the build.\n _ => \"unknown\",\n }\n}\n\n/// Wire label for a folded [`LifecycleState`](cp_wire::types::LifecycleState).\nfn lifecycle_label(state: cp_wire::types::LifecycleState) -> &'static str {\n match state {\n cp_wire::types::LifecycleState::Starting => \"starting\",\n cp_wire::types::LifecycleState::Running => \"running\",\n cp_wire::types::LifecycleState::Stopping => \"stopping\",\n cp_wire::types::LifecycleState::Stopped => \"stopped\",\n // `LifecycleState` is #[non_exhaustive]; a state from a newer agent\n // protocol folds to a neutral label rather than failing the build.\n _ => \"unknown\",\n }\n}\n\n/// Build one vital JSON object.\nfn vital(name: &str, category: &str, status: &str, latency_ms: Option, detail: &str) -> serde_json::Value {\n serde_json::json!({\n \"name\": name,\n \"category\": category,\n \"status\": status,\n \"latencyMs\": latency_ms,\n \"detail\": detail,\n })\n}\n\n/// Milliseconds elapsed since `started`, saturating into `u64`.\nfn elapsed_ms(started: Instant) -> u64 {\n u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)\n}\n\n/// Current wall-clock time in epoch milliseconds.\nfn now_ms() -> u64 {\n SystemTime::now().duration_since(UNIX_EPOCH).ok().and_then(|d| u64::try_from(d.as_millis()).ok()).unwrap_or(0)\n}\n\n/// The user's home directory (`$HOME`), if set.\nfn home_dir() -> Option {\n std::env::var_os(\"HOME\").map(PathBuf::from)\n}\n"}, "093948156f1b9b5dd76c1b981ecbd2b4ae23ef86": {"t": 1785761042.681892, "n": 14392, "h": "aff11a9b639f52f7", "c": "//! On-demand service-connectivity **vitals** endpoint (`GET\n//! /api/agent/{id}/vitals`).\n//!\n//! Where [`metrics`](super::metrics) reports the *durable* observability slice\n//! the backend already holds (stream health, rev lag), this endpoint\n//! answers a different, *live* question the operator asks by pressing a button:\n//! \"can each service this agent depends on actually be reached right now?\"\n//!\n//! It runs **real probes** and reports each as `ok` / `error` / `unavailable`,\n//! and — crucially — never fabricates a green. A check the backend genuinely\n//! cannot perform from where it sits returns `unavailable` with an honest\n//! reason, mirroring the inspection plane's derived-state contract.\n//!\n//! # What is probed, and how honestly\n//!\n//! | Service | Probe | Plane it proves |\n//! |---|---|---|\n//! | Orchestrator | self (we are responding) | always `ok` |\n//! | Agent main loop (connection) | heartbeat file freshness + `boot_id` match | the agent process is alive |\n//! | Agent loop status | folded phase + lifecycle from the view | a *status* read, not a connection |\n//! | LLM provider | TCP connect to the provider host `:443` | network path to the picked provider |\n//! | Voyage / Datalab / Brave / Firecrawl | TCP connect to the API host `:443` | network path to the capability |\n//! | Meilisearch | TCP connect to `127.0.0.1:` | the local search daemon is up |\n//! | Console server | `connect()` the agent's `console/server.sock` | the per-agent console daemon is up |\n//!\n//! A TCP connect to `:443` is a deliberate, key-independent reachability check:\n//! it proves DNS + routing + the remote listener without spending a token or\n//! needing the agent's API key (which lives in the agent's environment, not the\n//! orchestrator's). It answers exactly the \"connection to …\" question asked.\n//!\n//! The frontend adds the two checks only *it* can observe (the dev server is up\n//! because it is running, and the round-trip latency of this very request), so\n//! the rendered table covers all twelve services.\n\nuse std::net::{TcpStream, ToSocketAddrs};\nuse std::path::{Path, PathBuf};\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse cp_wire::heartbeat::{DEFAULT_MAX_AGE, Heartbeat};\nuse cp_wire::types::registry::Entry;\n\nuse crate::transport::Backend;\nuse crate::transport::rest::HttpReply;\n\n/// Authenticated API-key validation probes (GitHub username, Brave/Firecrawl/\n/// Voyage/Datalab account info) — proves each key is *valid*, not just that the\n/// host is reachable. See [`keychecks`] for the honesty contract.\nmod keychecks;\n\n/// Per-probe network timeout. Bounds DNS+connect for a single remote check.\nconst PROBE_TIMEOUT: Duration = Duration::from_secs(3);\n\n/// Overall deadline for collecting every concurrent network probe. A probe that\n/// has not reported by this point (e.g. a stalled DNS resolver) is filled in as\n/// a timeout rather than hanging the endpoint.\nconst COLLECT_DEADLINE: Duration = Duration::from_secs(6);\n\n/// `GET /api/agent/{id}/vitals` — run live connectivity checks for one agent.\n///\n/// Returns a JSON array of vital objects (`{name, category, status, latencyMs?,\n/// detail?}`). Unknown agent → `404`.\npub fn agent_vitals(state: &Mutex, agent_id: &str) -> HttpReply {\n let entry = match crate::transport::rest::resolve_entry(state, agent_id) {\n Ok(e) => e,\n Err(reply) => return reply,\n };\n\n let mut vitals: Vec = Vec::new();\n\n // 1. Orchestrator — we are responding, so it is trivially up.\n vitals.push(vital(\"Orchestrator\", \"orchestrator\", \"ok\", Some(0), \"serving\"));\n\n // 2-3. Agent-side reads (heartbeat + folded status), under one short lock.\n let provider = read_provider(state, &entry);\n vitals.push(agent_connection_vital(&entry));\n vitals.push(agent_status_vital(state, agent_id));\n\n // 4. Local infra probes that need a discovered address.\n vitals.push(meilisearch_vital());\n vitals.push(console_vital(&entry.folder));\n\n // 5. LLM provider reachability — a key-independent TCP probe (the provider\n // picked in the agent's config; no cheap whoami exists to validate it).\n let (provider_label, provider_host) = provider_host(&provider);\n let remotes: Vec<(String, String, String)> =\n vec![(provider_label.to_owned(), \"llm\".to_owned(), provider_host.to_owned())];\n vitals.extend(probe_remotes(remotes));\n\n // 6. Keyed services (GitHub, Brave, Firecrawl, Voyage, Datalab) — real\n // authenticated probes that prove the configured API key is valid and\n // report minimal account/identity info (username, credits, quota).\n vitals.extend(keychecks::probe_keyed_services());\n\n HttpReply::ok(&vitals)\n}\n\n// ── Agent-side checks ───────────────────────────────────────────────────\n\n/// Read the agent's configured LLM provider id from its `config.json`\n/// (`modules.core.llm_provider`), defaulting to `anthropic` when unreadable.\nfn read_provider(state: &Mutex, entry: &Entry) -> String {\n let folder = PathBuf::from(&entry.folder);\n let Ok(mut b) = state.lock() else {\n return \"anthropic\".to_owned();\n };\n b.inspect_mut()\n .read_config(&folder)\n .ok()\n .and_then(|cfg| {\n cfg.get(\"modules\")\n .and_then(|m| m.get(\"core\"))\n .and_then(|c| c.get(\"llm_provider\"))\n .and_then(serde_json::Value::as_str)\n .map(str::to_owned)\n })\n .unwrap_or_else(|| \"anthropic\".to_owned())\n}\n\n/// Probe the agent main-loop *connection* via its heartbeat file: a fresh beat\n/// whose `boot_id` matches the registry record means the process is alive.\nfn agent_connection_vital(entry: &Entry) -> serde_json::Value {\n let Ok(bytes) = std::fs::read(&entry.heartbeat_path) else {\n return vital(\"Agent main loop\", \"agent\", \"unavailable\", None, \"no heartbeat file\");\n };\n let Ok(hb) = Heartbeat::decode(&bytes) else {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"corrupt heartbeat\");\n };\n let now_ms = now_ms();\n let max_age_ms = u64::try_from(DEFAULT_MAX_AGE.as_millis()).unwrap_or(5_000);\n let age_ms = now_ms.saturating_sub(hb.timestamp_ms);\n if !hb.matches_boot(&entry.boot_id) {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"boot_id mismatch (pid reused)\");\n }\n if hb.is_fresh(now_ms, max_age_ms) {\n vital(\"Agent main loop\", \"agent\", \"ok\", None, &format!(\"heartbeat {age_ms}ms old\"))\n } else {\n vital(\"Agent main loop\", \"agent\", \"error\", None, &format!(\"stale heartbeat ({age_ms}ms old)\"))\n }\n}\n\n/// Report the agent loop's *status* (phase + lifecycle from the folded view).\n///\n/// This is a status read, not a connection: `ok` carries the live phase as its\n/// detail; a `Stopped`/`Stopping` lifecycle surfaces as `error`.\nfn agent_status_vital(state: &Mutex, agent_id: &str) -> serde_json::Value {\n let snapshot = state.lock().ok().and_then(|b| b.view.get(agent_id).map(|v| (v.phase, v.lifecycle)));\n let Some((phase, lifecycle)) = snapshot else {\n return vital(\"Agent loop status\", \"agent\", \"unavailable\", None, \"no view yet\");\n };\n let phase_str = phase.map_or(\"idle\", phase_label);\n let life_str = lifecycle.map(lifecycle_label);\n let stopped = matches!(life_str, Some(\"stopping\" | \"stopped\"));\n let detail = match life_str {\n Some(l) => format!(\"{phase_str} · {l}\"),\n None => phase_str.to_owned(),\n };\n let status = if stopped { \"error\" } else { \"ok\" };\n vital(\"Agent loop status\", \"agent\", status, None, &detail)\n}\n\n// ── Local infra checks ──────────────────────────────────────────────────\n\n/// Probe Meilisearch by TCP-connecting to the port it advertises in\n/// `~/.context-pilot/meilisearch/port`. No port file → it was never started.\nfn meilisearch_vital() -> serde_json::Value {\n let Some(port) = meili_port() else {\n return vital(\"Meilisearch\", \"infra\", \"unavailable\", None, \"no port file (not started)\");\n };\n let started = Instant::now();\n match TcpStream::connect_timeout(&(std::net::Ipv4Addr::LOCALHOST, port).into(), PROBE_TIMEOUT) {\n Ok(_) => vital(\"Meilisearch\", \"infra\", \"ok\", Some(elapsed_ms(started)), &format!(\"127.0.0.1:{port}\")),\n Err(e) => vital(\"Meilisearch\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n/// Read the Meilisearch port from `~/.context-pilot/meilisearch/port`.\nfn meili_port() -> Option {\n let path = home_dir()?.join(\".context-pilot/meilisearch/port\");\n std::fs::read_to_string(path).ok()?.trim().parse().ok()\n}\n\n/// Probe the agent's console server by connecting its Unix socket at\n/// `/.context-pilot/console/server.sock`.\nfn console_vital(folder: &str) -> serde_json::Value {\n let sock = Path::new(folder).join(\".context-pilot/console/server.sock\");\n if !sock.exists() {\n return vital(\"Console server\", \"infra\", \"unavailable\", None, \"no socket (not started)\");\n }\n let started = Instant::now();\n match std::os::unix::net::UnixStream::connect(&sock) {\n Ok(_) => vital(\"Console server\", \"infra\", \"ok\", Some(elapsed_ms(started)), \"socket connected\"),\n Err(e) => vital(\"Console server\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n// ── Remote reachability probes ──────────────────────────────────────────\n\n/// Run every remote host probe concurrently and collect them under a deadline.\n///\n/// Each probe runs on its own thread (so the wall time is the slowest single\n/// probe, not their sum) and reports over a channel. Any probe that misses the\n/// [`COLLECT_DEADLINE`] — e.g. a wedged DNS resolver — is filled in as a\n/// timeout so the endpoint always returns promptly.\nfn probe_remotes(remotes: Vec<(String, String, String)>) -> Vec {\n let total = remotes.len();\n let (tx, rx) = mpsc::channel::<(usize, serde_json::Value)>();\n for (idx, (label, category, host)) in remotes.into_iter().enumerate() {\n let tx = tx.clone();\n let _handle = thread::spawn(move || {\n let v = tcp_check(&label, &category, &host, 443);\n let _sent = tx.send((idx, v));\n });\n }\n drop(tx); // so the channel closes once every probe thread has reported\n\n let mut slots: Vec> = (0..total).map(|_| None).collect();\n let deadline = Instant::now() + COLLECT_DEADLINE;\n let mut received = 0;\n while received < total {\n let remaining = deadline.saturating_duration_since(Instant::now());\n if remaining.is_zero() {\n break;\n }\n match rx.recv_timeout(remaining) {\n Ok((idx, v)) => {\n if let Some(slot) = slots.get_mut(idx) {\n *slot = Some(v);\n received += 1;\n }\n }\n Err(_) => break, // deadline elapsed or all senders dropped\n }\n }\n\n slots\n .into_iter()\n .map(|s| s.unwrap_or_else(|| vital(\"Service\", \"service\", \"unavailable\", None, \"probe timed out\")))\n .collect()\n}\n\n/// TCP-connect `host:port` with a bounded timeout — a key-independent\n/// reachability check (proves DNS + routing + a live listener).\nfn tcp_check(label: &str, category: &str, host: &str, port: u16) -> serde_json::Value {\n let started = Instant::now();\n let addrs = match (host, port).to_socket_addrs() {\n Ok(it) => it,\n Err(e) => return vital(label, category, \"error\", None, &format!(\"dns failed: {e}\")),\n };\n let Some(addr) = addrs.into_iter().next() else {\n return vital(label, category, \"error\", None, \"dns resolved to no address\");\n };\n match TcpStream::connect_timeout(&addr, PROBE_TIMEOUT) {\n Ok(_) => vital(label, category, \"ok\", Some(elapsed_ms(started)), &format!(\"{host}:{port} reachable\")),\n Err(e) => vital(label, category, \"error\", None, &format!(\"{host}:{port} {e}\")),\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────\n\n/// Map a provider id (`modules.core.llm_provider`) to a display label + the API\n/// host whose `:443` reachability stands in for \"can we reach the provider\".\nfn provider_host(provider: &str) -> (&'static str, &'static str) {\n match provider {\n \"grok\" => (\"xAI Grok\", \"api.x.ai\"),\n \"groq\" => (\"Groq\", \"api.groq.com\"),\n \"deepseek\" => (\"DeepSeek\", \"api.deepseek.com\"),\n \"minimax\" => (\"MiniMax\", \"api.minimax.io\"),\n // anthropic + all claude-code variants hit the Anthropic API host.\n _ => (\"Anthropic\", \"api.anthropic.com\"),\n }\n}\n\n/// Wire label for a folded [`Phase`](cp_wire::types::Phase).\nfn phase_label(phase: cp_wire::types::Phase) -> &'static str {\n match phase {\n cp_wire::types::Phase::Idle => \"idle\",\n cp_wire::types::Phase::Streaming => \"streaming\",\n cp_wire::types::Phase::Tooling => \"tooling\",\n }\n}\n\n/// Wire label for a folded [`LifecycleState`](cp_wire::types::LifecycleState).\nfn lifecycle_label(state: cp_wire::types::LifecycleState) -> &'static str {\n match state {\n cp_wire::types::LifecycleState::Starting => \"starting\",\n cp_wire::types::LifecycleState::Running => \"running\",\n cp_wire::types::LifecycleState::Stopping => \"stopping\",\n cp_wire::types::LifecycleState::Stopped => \"stopped\",\n // `LifecycleState` is #[non_exhaustive]; a state from a newer agent\n // protocol folds to a neutral label rather than failing the build.\n _ => \"unknown\",\n }\n}\n\n/// Build one vital JSON object.\nfn vital(name: &str, category: &str, status: &str, latency_ms: Option, detail: &str) -> serde_json::Value {\n serde_json::json!({\n \"name\": name,\n \"category\": category,\n \"status\": status,\n \"latencyMs\": latency_ms,\n \"detail\": detail,\n })\n}\n\n/// Milliseconds elapsed since `started`, saturating into `u64`.\nfn elapsed_ms(started: Instant) -> u64 {\n u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)\n}\n\n/// Current wall-clock time in epoch milliseconds.\nfn now_ms() -> u64 {\n SystemTime::now().duration_since(UNIX_EPOCH).ok().and_then(|d| u64::try_from(d.as_millis()).ok()).unwrap_or(0)\n}\n\n/// The user's home directory (`$HOME`), if set.\nfn home_dir() -> Option {\n std::env::var_os(\"HOME\").map(PathBuf::from)\n}\n"}, "ace893ad573eeff9ee6aa91187b3dfeccc994edf": {"t": 1785761044.1520154, "n": 14215, "h": "3b63b40b852cf99c", "c": "//! On-demand service-connectivity **vitals** endpoint (`GET\n//! /api/agent/{id}/vitals`).\n//!\n//! Where [`metrics`](super::metrics) reports the *durable* observability slice\n//! the backend already holds (stream health, rev lag), this endpoint\n//! answers a different, *live* question the operator asks by pressing a button:\n//! \"can each service this agent depends on actually be reached right now?\"\n//!\n//! It runs **real probes** and reports each as `ok` / `error` / `unavailable`,\n//! and — crucially — never fabricates a green. A check the backend genuinely\n//! cannot perform from where it sits returns `unavailable` with an honest\n//! reason, mirroring the inspection plane's derived-state contract.\n//!\n//! # What is probed, and how honestly\n//!\n//! | Service | Probe | Plane it proves |\n//! |---|---|---|\n//! | Orchestrator | self (we are responding) | always `ok` |\n//! | Agent main loop (connection) | heartbeat file freshness + `boot_id` match | the agent process is alive |\n//! | Agent loop status | folded phase + lifecycle from the view | a *status* read, not a connection |\n//! | LLM provider | TCP connect to the provider host `:443` | network path to the picked provider |\n//! | Voyage / Datalab / Brave / Firecrawl | TCP connect to the API host `:443` | network path to the capability |\n//! | Meilisearch | TCP connect to `127.0.0.1:` | the local search daemon is up |\n//! | Console server | `connect()` the agent's `console/server.sock` | the per-agent console daemon is up |\n//!\n//! A TCP connect to `:443` is a deliberate, key-independent reachability check:\n//! it proves DNS + routing + the remote listener without spending a token or\n//! needing the agent's API key (which lives in the agent's environment, not the\n//! orchestrator's). It answers exactly the \"connection to …\" question asked.\n//!\n//! The frontend adds the two checks only *it* can observe (the dev server is up\n//! because it is running, and the round-trip latency of this very request), so\n//! the rendered table covers all twelve services.\n\nuse std::net::{TcpStream, ToSocketAddrs};\nuse std::path::{Path, PathBuf};\nuse std::sync::Mutex;\nuse std::sync::mpsc;\nuse std::thread;\nuse std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};\n\nuse cp_wire::heartbeat::{DEFAULT_MAX_AGE, Heartbeat};\nuse cp_wire::types::registry::Entry;\n\nuse crate::transport::Backend;\nuse crate::transport::rest::HttpReply;\n\n/// Authenticated API-key validation probes (GitHub username, Brave/Firecrawl/\n/// Voyage/Datalab account info) — proves each key is *valid*, not just that the\n/// host is reachable. See [`keychecks`] for the honesty contract.\nmod keychecks;\n\n/// Per-probe network timeout. Bounds DNS+connect for a single remote check.\nconst PROBE_TIMEOUT: Duration = Duration::from_secs(3);\n\n/// Overall deadline for collecting every concurrent network probe. A probe that\n/// has not reported by this point (e.g. a stalled DNS resolver) is filled in as\n/// a timeout rather than hanging the endpoint.\nconst COLLECT_DEADLINE: Duration = Duration::from_secs(6);\n\n/// `GET /api/agent/{id}/vitals` — run live connectivity checks for one agent.\n///\n/// Returns a JSON array of vital objects (`{name, category, status, latencyMs?,\n/// detail?}`). Unknown agent → `404`.\npub fn agent_vitals(state: &Mutex, agent_id: &str) -> HttpReply {\n let entry = match crate::transport::rest::resolve_entry(state, agent_id) {\n Ok(e) => e,\n Err(reply) => return reply,\n };\n\n let mut vitals: Vec = Vec::new();\n\n // 1. Orchestrator — we are responding, so it is trivially up.\n vitals.push(vital(\"Orchestrator\", \"orchestrator\", \"ok\", Some(0), \"serving\"));\n\n // 2-3. Agent-side reads (heartbeat + folded status), under one short lock.\n let provider = read_provider(state, &entry);\n vitals.push(agent_connection_vital(&entry));\n vitals.push(agent_status_vital(state, agent_id));\n\n // 4. Local infra probes that need a discovered address.\n vitals.push(meilisearch_vital());\n vitals.push(console_vital(&entry.folder));\n\n // 5. LLM provider reachability — a key-independent TCP probe (the provider\n // picked in the agent's config; no cheap whoami exists to validate it).\n let (provider_label, provider_host) = provider_host(&provider);\n let remotes: Vec<(String, String, String)> =\n vec![(provider_label.to_owned(), \"llm\".to_owned(), provider_host.to_owned())];\n vitals.extend(probe_remotes(remotes));\n\n // 6. Keyed services (GitHub, Brave, Firecrawl, Voyage, Datalab) — real\n // authenticated probes that prove the configured API key is valid and\n // report minimal account/identity info (username, credits, quota).\n vitals.extend(keychecks::probe_keyed_services());\n\n HttpReply::ok(&vitals)\n}\n\n// ── Agent-side checks ───────────────────────────────────────────────────\n\n/// Read the agent's configured LLM provider id from its `config.json`\n/// (`modules.core.llm_provider`), defaulting to `anthropic` when unreadable.\nfn read_provider(state: &Mutex, entry: &Entry) -> String {\n let folder = PathBuf::from(&entry.folder);\n let Ok(mut b) = state.lock() else {\n return \"anthropic\".to_owned();\n };\n b.inspect_mut()\n .read_config(&folder)\n .ok()\n .and_then(|cfg| {\n cfg.get(\"modules\")\n .and_then(|m| m.get(\"core\"))\n .and_then(|c| c.get(\"llm_provider\"))\n .and_then(serde_json::Value::as_str)\n .map(str::to_owned)\n })\n .unwrap_or_else(|| \"anthropic\".to_owned())\n}\n\n/// Probe the agent main-loop *connection* via its heartbeat file: a fresh beat\n/// whose `boot_id` matches the registry record means the process is alive.\nfn agent_connection_vital(entry: &Entry) -> serde_json::Value {\n let Ok(bytes) = std::fs::read(&entry.heartbeat_path) else {\n return vital(\"Agent main loop\", \"agent\", \"unavailable\", None, \"no heartbeat file\");\n };\n let Ok(hb) = Heartbeat::decode(&bytes) else {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"corrupt heartbeat\");\n };\n let now_ms = now_ms();\n let max_age_ms = u64::try_from(DEFAULT_MAX_AGE.as_millis()).unwrap_or(5_000);\n let age_ms = now_ms.saturating_sub(hb.timestamp_ms);\n if !hb.matches_boot(&entry.boot_id) {\n return vital(\"Agent main loop\", \"agent\", \"error\", None, \"boot_id mismatch (pid reused)\");\n }\n if hb.is_fresh(now_ms, max_age_ms) {\n vital(\"Agent main loop\", \"agent\", \"ok\", None, &format!(\"heartbeat {age_ms}ms old\"))\n } else {\n vital(\"Agent main loop\", \"agent\", \"error\", None, &format!(\"stale heartbeat ({age_ms}ms old)\"))\n }\n}\n\n/// Report the agent loop's *status* (phase + lifecycle from the folded view).\n///\n/// This is a status read, not a connection: `ok` carries the live phase as its\n/// detail; a `Stopped`/`Stopping` lifecycle surfaces as `error`.\nfn agent_status_vital(state: &Mutex, agent_id: &str) -> serde_json::Value {\n let snapshot = state.lock().ok().and_then(|b| b.view.get(agent_id).map(|v| (v.phase, v.lifecycle)));\n let Some((phase, lifecycle)) = snapshot else {\n return vital(\"Agent loop status\", \"agent\", \"unavailable\", None, \"no view yet\");\n };\n let phase_str = phase.map_or(\"idle\", phase_label);\n let life_str = lifecycle.map(lifecycle_label);\n let stopped = matches!(life_str, Some(\"stopping\" | \"stopped\"));\n let detail = match life_str {\n Some(l) => format!(\"{phase_str} · {l}\"),\n None => phase_str.to_owned(),\n };\n let status = if stopped { \"error\" } else { \"ok\" };\n vital(\"Agent loop status\", \"agent\", status, None, &detail)\n}\n\n// ── Local infra checks ──────────────────────────────────────────────────\n\n/// Probe Meilisearch by TCP-connecting to the port it advertises in\n/// `~/.context-pilot/meilisearch/port`. No port file → it was never started.\nfn meilisearch_vital() -> serde_json::Value {\n let Some(port) = meili_port() else {\n return vital(\"Meilisearch\", \"infra\", \"unavailable\", None, \"no port file (not started)\");\n };\n let started = Instant::now();\n match TcpStream::connect_timeout(&(std::net::Ipv4Addr::LOCALHOST, port).into(), PROBE_TIMEOUT) {\n Ok(_) => vital(\"Meilisearch\", \"infra\", \"ok\", Some(elapsed_ms(started)), &format!(\"127.0.0.1:{port}\")),\n Err(e) => vital(\"Meilisearch\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n/// Read the Meilisearch port from `~/.context-pilot/meilisearch/port`.\nfn meili_port() -> Option {\n let path = home_dir()?.join(\".context-pilot/meilisearch/port\");\n std::fs::read_to_string(path).ok()?.trim().parse().ok()\n}\n\n/// Probe the agent's console server by connecting its Unix socket at\n/// `/.context-pilot/console/server.sock`.\nfn console_vital(folder: &str) -> serde_json::Value {\n let sock = Path::new(folder).join(\".context-pilot/console/server.sock\");\n if !sock.exists() {\n return vital(\"Console server\", \"infra\", \"unavailable\", None, \"no socket (not started)\");\n }\n let started = Instant::now();\n match std::os::unix::net::UnixStream::connect(&sock) {\n Ok(_) => vital(\"Console server\", \"infra\", \"ok\", Some(elapsed_ms(started)), \"socket connected\"),\n Err(e) => vital(\"Console server\", \"infra\", \"error\", None, &format!(\"connect failed: {e}\")),\n }\n}\n\n// ── Remote reachability probes ──────────────────────────────────────────\n\n/// Run every remote host probe concurrently and collect them under a deadline.\n///\n/// Each probe runs on its own thread (so the wall time is the slowest single\n/// probe, not their sum) and reports over a channel. Any probe that misses the\n/// [`COLLECT_DEADLINE`] — e.g. a wedged DNS resolver — is filled in as a\n/// timeout so the endpoint always returns promptly.\nfn probe_remotes(remotes: Vec<(String, String, String)>) -> Vec {\n let total = remotes.len();\n let (tx, rx) = mpsc::channel::<(usize, serde_json::Value)>();\n for (idx, (label, category, host)) in remotes.into_iter().enumerate() {\n let tx = tx.clone();\n let _handle = thread::spawn(move || {\n let v = tcp_check(&label, &category, &host, 443);\n let _sent = tx.send((idx, v));\n });\n }\n drop(tx); // so the channel closes once every probe thread has reported\n\n let mut slots: Vec> = (0..total).map(|_| None).collect();\n let deadline = Instant::now() + COLLECT_DEADLINE;\n let mut received = 0;\n while received < total {\n let remaining = deadline.saturating_duration_since(Instant::now());\n if remaining.is_zero() {\n break;\n }\n match rx.recv_timeout(remaining) {\n Ok((idx, v)) => {\n if let Some(slot) = slots.get_mut(idx) {\n *slot = Some(v);\n received += 1;\n }\n }\n Err(_) => break, // deadline elapsed or all senders dropped\n }\n }\n\n slots\n .into_iter()\n .map(|s| s.unwrap_or_else(|| vital(\"Service\", \"service\", \"unavailable\", None, \"probe timed out\")))\n .collect()\n}\n\n/// TCP-connect `host:port` with a bounded timeout — a key-independent\n/// reachability check (proves DNS + routing + a live listener).\nfn tcp_check(label: &str, category: &str, host: &str, port: u16) -> serde_json::Value {\n let started = Instant::now();\n let addrs = match (host, port).to_socket_addrs() {\n Ok(it) => it,\n Err(e) => return vital(label, category, \"error\", None, &format!(\"dns failed: {e}\")),\n };\n let Some(addr) = addrs.into_iter().next() else {\n return vital(label, category, \"error\", None, \"dns resolved to no address\");\n };\n match TcpStream::connect_timeout(&addr, PROBE_TIMEOUT) {\n Ok(_) => vital(label, category, \"ok\", Some(elapsed_ms(started)), &format!(\"{host}:{port} reachable\")),\n Err(e) => vital(label, category, \"error\", None, &format!(\"{host}:{port} {e}\")),\n }\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────\n\n/// Map a provider id (`modules.core.llm_provider`) to a display label + the API\n/// host whose `:443` reachability stands in for \"can we reach the provider\".\nfn provider_host(provider: &str) -> (&'static str, &'static str) {\n match provider {\n \"grok\" => (\"xAI Grok\", \"api.x.ai\"),\n \"groq\" => (\"Groq\", \"api.groq.com\"),\n \"deepseek\" => (\"DeepSeek\", \"api.deepseek.com\"),\n \"minimax\" => (\"MiniMax\", \"api.minimax.io\"),\n // anthropic + all claude-code variants hit the Anthropic API host.\n _ => (\"Anthropic\", \"api.anthropic.com\"),\n }\n}\n\n/// Wire label for a folded [`Phase`](cp_wire::types::Phase).\nfn phase_label(phase: cp_wire::types::Phase) -> &'static str {\n match phase {\n cp_wire::types::Phase::Idle => \"idle\",\n cp_wire::types::Phase::Streaming => \"streaming\",\n cp_wire::types::Phase::Tooling => \"tooling\",\n }\n}\n\n/// Wire label for a folded [`LifecycleState`](cp_wire::types::LifecycleState).\nfn lifecycle_label(state: cp_wire::types::LifecycleState) -> &'static str {\n match state {\n cp_wire::types::LifecycleState::Starting => \"starting\",\n cp_wire::types::LifecycleState::Running => \"running\",\n cp_wire::types::LifecycleState::Stopping => \"stopping\",\n cp_wire::types::LifecycleState::Stopped => \"stopped\",\n }\n}\n\n/// Build one vital JSON object.\nfn vital(name: &str, category: &str, status: &str, latency_ms: Option, detail: &str) -> serde_json::Value {\n serde_json::json!({\n \"name\": name,\n \"category\": category,\n \"status\": status,\n \"latencyMs\": latency_ms,\n \"detail\": detail,\n })\n}\n\n/// Milliseconds elapsed since `started`, saturating into `u64`.\nfn elapsed_ms(started: Instant) -> u64 {\n u64::try_from(started.elapsed().as_millis()).unwrap_or(u64::MAX)\n}\n\n/// Current wall-clock time in epoch milliseconds.\nfn now_ms() -> u64 {\n SystemTime::now().duration_since(UNIX_EPOCH).ok().and_then(|d| u64::try_from(d.as_millis()).ok()).unwrap_or(0)\n}\n\n/// The user's home directory (`$HOME`), if set.\nfn home_dir() -> Option {\n std::env::var_os(\"HOME\").map(PathBuf::from)\n}\n"}}}}