Skip to main content

khive_runtime/
daemon.rs

1//! khived daemon server — persistent warm runtime over a Unix socket.
2//!
3//! The daemon binds `~/.khive/khived.sock`, accepts length-prefixed request
4//! frames, dispatches them through a [`DaemonDispatch`] implementor, and serves
5//! results back. It is transport-agnostic: the MCP crate provides the dispatch
6//! impl, but any future client (CLI, HTTP gateway) can reuse this server.
7//!
8//! The client side (forwarding, auto-spawn) lives in the transport crate
9//! (e.g. `khive-mcp`), not here.
10
11use std::sync::Arc;
12
13mod boot_guard;
14mod connection;
15mod dispatch_trait;
16mod locks;
17mod process_files;
18mod run_daemon;
19
20pub use dispatch_trait::*;
21
22#[cfg(all(unix, feature = "fault-injection"))]
23pub use connection::handle_conn_for_test;
24use connection::*;
25
26#[cfg(unix)]
27pub use run_daemon::*;
28
29#[cfg(unix)]
30pub use boot_guard::*;
31
32#[cfg(all(test, unix))]
33use locks::open_lock_file;
34#[cfg(unix)]
35pub use locks::{
36    acquire_daemon_boot_guard, acquire_recovery_lock, try_acquire_daemon_boot_guard_until,
37    try_acquire_recoverer_lock_until, DaemonBootGuard,
38};
39#[cfg(unix)]
40pub(crate) use locks::{peer_uid, uid_is_permitted};
41#[cfg(unix)]
42use locks::{socket_identity, SocketIdentity};
43pub use process_files::*;
44
45mod store_guard;
46mod store_identity;
47#[cfg(unix)]
48use store_guard::ensure_claimed_parent_identity;
49#[cfg(unix)]
50pub use store_guard::{acquire_daemon_store_guards, bind_daemon_store_files, claim_stores};
51pub use store_guard::{assert_daemon_store_identities, DaemonStoreGuard};
52#[cfg(unix)]
53pub use store_identity::claimed_daemon_store_identity;
54mod supervisor_marker;
55#[cfg(unix)]
56pub use supervisor_marker::supervisor_marker_path;
57
58#[cfg(unix)]
59use std::io::Write as _;
60#[cfg(unix)]
61use std::os::unix::fs::{MetadataExt, PermissionsExt};
62#[cfg(unix)]
63use std::os::unix::io::AsRawFd;
64#[cfg(all(test, unix))]
65use std::path::PathBuf;
66
67#[cfg(unix)]
68use async_trait::async_trait;
69#[cfg(unix)]
70use libc;
71#[cfg(unix)]
72use serde::Deserialize;
73#[cfg(unix)]
74use tokio::io::AsyncReadExt;
75#[cfg(all(test, unix))]
76use tokio::io::AsyncWriteExt;
77#[cfg(unix)]
78use tokio::net::{UnixListener, UnixStream};
79
80#[cfg(unix)]
81use crate::pack::RequestIdentity;
82#[cfg(unix)]
83use khive_db::{run_checkpoint_task, CheckpointConfig, CheckpointLifecycleOwner, ConnectionPool};
84
85// Unix-only: gated inside the file, because the error-constructor census
86// admits only attribute-free submodule declarations.
87mod exit_record;
88mod lifecycle;
89#[cfg(unix)]
90use lifecycle::khive_dir;
91#[cfg(test)]
92use lifecycle::khive_root_from;
93#[cfg(unix)]
94use lifecycle::{
95    current_supervisor_claim, default_socket_path, ensure_rendezvous_overrides_paired,
96    read_supervisor_marker_claim, DaemonLifecycle, PID_PATH_ENV, SOCKET_PATH_ENV,
97};
98pub use lifecycle::{
99    lock_path, volume_lock_dir, DaemonLifecyclePhase, DaemonLifecycleSnapshot, DaemonLifetime,
100    DaemonOptions, DaemonShutdownReason, DaemonStartupReport, DEFAULT_DEMAND_IDLE_SECS,
101};
102#[cfg(unix)]
103pub use lifecycle::{
104    pid_path, recoverer_lock_path, rendezvous_split, socket_path, RendezvousSplit,
105    SUPERVISOR_CLAIM_ENV,
106};
107
108mod load_limits;
109#[cfg(unix)]
110use load_limits::{admit_or_refuse_busy, ConnectionAdmission};
111pub use load_limits::{
112    recall_ledger_snapshot, track_recall_ledger_task, ConnectionCapSnapshot, RecallLedgerSnapshot,
113};
114
115/// Maximum frame size accepted in either direction.
116pub const MAX_FRAME_BYTES: usize = 8 * 1024 * 1024;
117
118/// Wire protocol version for the daemon IPC framing.
119///
120/// Increment this constant whenever the request or response frame shape
121/// changes in a backward-incompatible way. The client sends its version
122/// in every request; the daemon rejects mismatches with an explicit error
123/// that names both sides so the operator knows exactly what to do
124/// (`make local` rebuilds the client binary).
125/// See `docs/api/daemon.md#protocol_version` for the version-by-version history.
126pub const PROTOCOL_VERSION: u32 = 8;
127
128/// Internal signal carried in a dispatch result until the daemon moves it to
129/// response-frame metadata. It must never be sent in `result`: older v8
130/// clients publish that string without inspecting its contents.
131#[doc(hidden)]
132pub const DAEMON_LEXICAL_TIMEOUT_MARKER: &str = "__khive_daemon_lexical_timeout";
133
134const DEFAULT_DRAIN_TIMEOUT_SECS: u64 = 10;
135/// An accepted local socket must finish its first frame within this window.
136/// Dispatch deadlines start only after decoding, so they cannot reap peers
137/// that connect and then stop sending request bytes.
138#[cfg(unix)]
139const INITIAL_FRAME_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
140
141#[cfg(unix)]
142fn next_accept_error_backoff(previous: Option<std::time::Duration>) -> std::time::Duration {
143    previous
144        .map(|delay| delay.saturating_mul(2))
145        .unwrap_or_else(|| std::time::Duration::from_millis(10))
146        .min(std::time::Duration::from_secs(1))
147}
148
149// ── wire types ────────────────────────────────────────────────────────────────
150
151mod config_id;
152#[cfg(test)]
153use config_id::parse_config_id;
154pub use config_id::{
155    config_id_extra_embedder_exclusions, config_ids_compatible, first_config_mismatch_field,
156};
157
158mod wire_frames;
159#[cfg(unix)]
160pub use wire_frames::{read_frame, write_frame};
161#[cfg(unix)]
162use wire_frames::{read_initial_frame, take_daemon_lexical_timeout_marker};
163pub use wire_frames::{
164    CheckpointStoreMetrics, DaemonDispatchError, DaemonRequestFrame, DaemonResponseFrame,
165    MetricsSnapshot, ERROR_DETAIL_NESTING_DEPTH_LIMIT,
166};
167
168/// Returns `true` for non-empty env values that are not `"0"` or `"false"`.
169#[cfg(unix)]
170pub fn env_truthy(key: &str) -> bool {
171    std::env::var(key)
172        .map(|v| {
173            let v = v.trim();
174            !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false")
175        })
176        .unwrap_or(false)
177}
178
179include!("daemon_khive_root_tests.rs");
180
181/// Serve one already-admitted test connection through the production frame handler.
182///
183/// This seam owns no socket path, PID, boot guard, background components, or
184/// process-wide shutdown state. The caller owns and joins the connection task.
185/// It deliberately does not exercise listener admission or daemon lifecycle.
186#[cfg(all(unix, any(test, feature = "test-internals")))]
187#[doc(hidden)]
188pub async fn serve_connection_for_test<D: DaemonDispatch>(stream: UnixStream, dispatcher: D) {
189    handle_conn_with_shutdown(
190        stream,
191        dispatcher,
192        None,
193        tokio::time::Instant::now() + INITIAL_FRAME_READ_TIMEOUT,
194    )
195    .await;
196}
197
198#[cfg(all(test, unix))]
199#[path = "daemon_tests.rs"]
200mod tests;