//! Central host-side state paths under `/var/lib/hyperhive`. //! //! Historically these were flat string literals scattered across many //! modules (`broker.sqlite`, `matrix-admin-token`, `agent-ports.json`, //! …) directly under the state root. This module groups the **strictly //! host-side** ones (read/written by hive-c0re alone, no nix-module or //! container coupling) into subdirs: `db/`, `forge/`, `matrix/`, `run/`. //! //! Nix-coupled paths (`forge-core-token`, `matrix-register-token`, //! `gateway/`, `meta/`, `agents/`) are intentionally **not** moved here //! — they cross into nix modules / bind mounts and are tracked //! separately so the Rust path and the nix default can move in lockstep. //! //! [`relocate_legacy_state`] moves any file still at the old flat //! location into its new subdir on startup, before the broker db is //! opened. use std::path::{Path, PathBuf}; /// Root of all hive-c0re persistent state. pub const STATE_ROOT: &str = "/var/lib/hyperhive"; /// Default broker db path (`db/broker.sqlite`). Exposed as a `&str` for /// the `--broker-db` clap `default_value`; `build_logs.sqlite` is placed /// alongside it (the build-logs store keys off the broker db's parent). pub const BROKER_DB: &str = "/var/lib/hyperhive/db/broker.sqlite"; #[must_use] pub fn state_root() -> PathBuf { PathBuf::from(STATE_ROOT) } /// `db/` — sqlite databases (broker, build logs). #[must_use] pub fn db_dir() -> PathBuf { state_root().join("db") } /// `forge/` — hive-c0re's own forge provisioning markers. #[must_use] pub fn forge_dir() -> PathBuf { state_root().join("forge") } /// `forge/core-avatar-set` — marker: core account avatar uploaded. #[must_use] pub fn forge_core_avatar_marker() -> PathBuf { forge_dir().join("core-avatar-set") } /// `forge/agent-configs-avatar-set` — marker: agent-configs org avatar set. #[must_use] pub fn forge_config_org_avatar_marker() -> PathBuf { forge_dir().join("agent-configs-avatar-set") } /// `forge/email-aligned-` — marker: ``'s forge email aligned. #[must_use] pub fn forge_email_aligned_marker(name: &str) -> PathBuf { forge_dir().join(format!("email-aligned-{name}")) } /// `matrix/` — host-side matrix provisioning state (admin token, hive /// Space room id, per-agent password creds). The shared registration /// token is bind-mounted into the tuwunel container via nix and stays /// at its own path (tracked separately). #[must_use] pub fn matrix_dir() -> PathBuf { state_root().join("matrix") } /// `matrix/admin-token` — hive system admin access token. #[must_use] pub fn matrix_admin_token() -> PathBuf { matrix_dir().join("admin-token") } /// `matrix/space-room-id` — persisted hive Space room id. #[must_use] pub fn matrix_space_room_id() -> PathBuf { matrix_dir().join("space-room-id") } /// `matrix/chat-room-id` — persisted default "hive chat" room id (the /// `m.space.child` of the hive Space every agent + the operator can join). #[must_use] pub fn matrix_chat_room_id() -> PathBuf { matrix_dir().join("chat-room-id") } /// `matrix/creds/` — per-agent throwaway matrix passwords (survive /// `destroy --purge`; agents auth by token, this is recovery only). #[must_use] pub fn matrix_creds_dir() -> PathBuf { matrix_dir().join("creds") } /// `run/` — runtime maps hive-c0re regenerates on every meta sync. #[must_use] pub fn run_dir() -> PathBuf { state_root().join("run") } /// `run/agent-ports.json` — name→port map the gateway routing reads. #[must_use] pub fn agent_ports_file() -> PathBuf { run_dir().join("agent-ports.json") } /// `run/agent-sockets.json` — name→socket-path map for UDS upstreams. #[must_use] pub fn agent_sockets_file() -> PathBuf { run_dir().join("agent-sockets.json") } /// Move any host-side state file still at its legacy flat location /// (directly under [`STATE_ROOT`]) into its new subdir. Idempotent and /// rename-based: a move only happens when the old path exists and the /// new one doesn't, so re-runs are no-ops. /// /// Must run **before** the broker db is opened (the broker + build-logs /// dbs are relocated here). Safe because those dbs use rollback-journal /// mode (no `-wal`/`-shm` sidecars after a clean shutdown), and a rename /// within the same filesystem is atomic. pub fn relocate_legacy_state() { let root = state_root(); let moves: [(&str, PathBuf); 8] = [ ("broker.sqlite", db_dir().join("broker.sqlite")), ("build_logs.sqlite", db_dir().join("build_logs.sqlite")), ("forge-core-avatar-set", forge_core_avatar_marker()), ( "forge-agent-configs-avatar-set", forge_config_org_avatar_marker(), ), ("matrix-admin-token", matrix_admin_token()), ("matrix-space-room-id", matrix_space_room_id()), ("matrix-creds", matrix_creds_dir()), ("agent-sockets.json", agent_sockets_file()), ]; for (old_rel, new) in &moves { move_if_legacy(&root.join(old_rel), new); } // agent-ports.json handled here too (kept out of the array only to // keep the fixed-size literal tidy). move_if_legacy(&root.join("agent-ports.json"), &agent_ports_file()); // `forge-email-aligned-` markers: glob the flat root. if let Ok(rd) = std::fs::read_dir(&root) { for ent in rd.flatten() { if let Some(name) = ent .file_name() .to_str() .and_then(|s| s.strip_prefix("forge-email-aligned-")) { move_if_legacy(&ent.path(), &forge_email_aligned_marker(name)); } } } } /// Rename `old` → `new` when `old` exists and `new` doesn't, creating /// `new`'s parent dir first. Logs on success / failure; never panics /// (a failed relocate must not take the daemon down — worst case the /// owning module recreates fresh state at the new path). fn move_if_legacy(old: &Path, new: &Path) { if !old.exists() || new.exists() { return; } if let Some(parent) = new.parent() { let _ = std::fs::create_dir_all(parent); } match std::fs::rename(old, new) { Ok(()) => tracing::info!( from = %old.display(), to = %new.display(), "relocate: moved legacy state into subdir" ), Err(e) => tracing::warn!( from = %old.display(), to = %new.display(), error = ?e, "relocate: rename failed; owning module will recreate at the new path" ), } } #[cfg(test)] mod tests { use super::*; use std::fs; #[test] fn move_if_legacy_moves_then_is_idempotent() { let tmp = tempfile::tempdir().unwrap(); let old = tmp.path().join("flat-file"); let new = tmp.path().join("sub/dir/new-file"); fs::write(&old, b"payload").unwrap(); move_if_legacy(&old, &new); assert!(!old.exists(), "old should be gone after move"); assert_eq!(fs::read(&new).unwrap(), b"payload"); // Re-run with old absent → no-op, new untouched. move_if_legacy(&old, &new); assert_eq!(fs::read(&new).unwrap(), b"payload"); } #[test] fn move_if_legacy_skips_when_new_exists() { let tmp = tempfile::tempdir().unwrap(); let old = tmp.path().join("flat"); let new = tmp.path().join("sub/new"); fs::write(&old, b"OLD").unwrap(); fs::create_dir_all(new.parent().unwrap()).unwrap(); fs::write(&new, b"NEW").unwrap(); // New already present → must NOT overwrite, old left in place. move_if_legacy(&old, &new); assert_eq!(fs::read(&new).unwrap(), b"NEW"); assert!(old.exists(), "old left untouched when new exists"); } }