//! Central host-side state paths under `/var/lib/hyperhive` (and the //! `/run/hyperhive` + `/run/hive-agent` runtime roots). //! //! Historically these were flat string literals scattered across many //! modules (`broker.sqlite`, `matrix-admin-token`, `agent-sockets.json`, //! …). This module is the **single Rust-side source** for every host //! path — the strictly host-side ones grouped into subdirs (`db/`, //! `forge/`, `matrix/`, `run/`), plus the **nix-coupled** roots //! (`agents/`, `applied/`, `meta/`, `shared/`, `gateway/`, `knowledge/`, //! the register/core tokens, the `/run` runtime dirs). //! //! Nix-coupled paths are NOT excluded (the old stance) — they're moved //! here too, each carrying a `// nix: …` comment naming the module / //! bind-mount whose literal must stay in lockstep. Centralising the Rust //! side (one place to grep, one place to change) and documenting the nix //! counterpart is strictly better than scattering the literals to avoid //! drift — the drift risk is the same either way, and the reboot outage //! is the argument for a single source. The nix modules keep their own //! literals (that's their source of truth; out of scope here). //! //! [`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}; // Layout facts shared with the host-side `hivectl` CLI live in // `hive-host-sock` (the crate hivectl links instead of the whole daemon); // re-exported here so this module stays the daemon's single-source facade for // its own callsites. pub use hive_host_sock::{AGENTS_ROOT, GATEWAY_HTPASSWD, HOST_SOCKET, agent_state_dir}; /// Root of all hive-c0re persistent state. // nix: bind-mount source `services.hyperhive.c0re.statePath` (hive-c0re.nix) — must match. pub const STATE_ROOT: &str = "/var/lib/hyperhive"; /// `/run/hyperhive` — hive-c0re's runtime root (host admin socket, the /// per-agent runtime dirs). Regenerated each boot; not persistent state. // nix: `RuntimeDirectory=hyperhive` on the hive-c0re service (hive-c0re.nix) — must match. // priv-sock: `hive_priv_sock::AGENT_RUNTIME_ROOT` is `RUNTIME_ROOT + "/agents"` and must // stay in sync; the privsep boundary prevents importing across the crate. pub const RUNTIME_ROOT: &str = "/run/hyperhive"; /// `/run/hive-agent` — per-agent runtime socket dir root (web + bound /// markers), one subdir per agent. // nix: agent container bind-mount / `RuntimeDirectory` (the harness nix modules) — must match. pub const AGENT_SOCKET_DIR: &str = "/run/hive-agent"; /// 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") } /// `webhook-secret` — hex-encoded 32-byte HMAC secret shared between /// hive-c0re's webhook handlers and the Forgejo webhook registrations. /// Generated on first startup and persisted; Forgejo is re-registered /// whenever the secret changes. #[must_use] pub fn webhook_secret_file() -> PathBuf { state_root().join("webhook-secret") } /// `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}")) } /// `forge/repo-creation-disabled-` — marker: ``'s forge user /// has had `max_repo_creation = 0` applied (blocks direct agent-initiated /// repo creation). One-shot guard so the PATCH runs once per /// agent (including agents provisioned before the change); delete to /// re-apply. #[must_use] pub fn forge_repo_creation_disabled_marker(name: &str) -> PathBuf { forge_dir().join(format!("repo-creation-disabled-{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-sockets.json` — name→socket-path map for UDS upstreams. #[must_use] pub fn agent_sockets_file() -> PathBuf { run_dir().join("agent-sockets.json") } // --------------------------------------------------------------------------- // Nix-coupled roots + runtime dirs. Each carries a `// nix:` note naming the // module / bind-mount whose literal must stay in lockstep with the value here. // The nix modules keep their own literals (their source of truth); this is the // single Rust-side source. // --------------------------------------------------------------------------- /// `agents/` root (`AGENTS_ROOT`) as an owned `PathBuf`. #[must_use] pub fn agents_root() -> PathBuf { PathBuf::from(AGENTS_ROOT) } /// `applied/` — per-agent *applied* (deployed) config repos + rev markers, /// distinct from the proposed configs under `agents//config`. // nix: read by hive-c0re only, but paired with `agents/` in the deploy flow. #[must_use] pub fn applied_root() -> PathBuf { state_root().join("applied") } /// `applied/` — one agent's applied config working tree. #[must_use] pub fn applied_dir(name: &str) -> PathBuf { applied_root().join(name) } /// `applied/..hyperhive-rev` — marker recording the flake rev an /// agent was last successfully rebuilt against (auto-update staleness check). #[must_use] pub fn applied_rev_marker(name: &str) -> PathBuf { applied_root().join(format!(".{name}.hyperhive-rev")) } /// `meta/` — the meta flake working tree (inputs, `flake.lock`, `.git`). // nix: bind-mounted read-only into agent containers as `/meta` (the harness nix modules) — must match. #[must_use] pub fn meta_root() -> PathBuf { state_root().join("meta") } /// `meta/flake.lock` — the meta flake lock (read for input rev display). #[must_use] pub fn meta_flake_lock() -> PathBuf { meta_root().join("flake.lock") } /// `meta/.git/index.lock` — git index lock; checked before meta git ops /// so a stale lock from a crashed process can be cleared. #[must_use] pub fn meta_git_index_lock() -> PathBuf { meta_root().join(".git/index.lock") } /// `shared/` — the cross-agent `/shared` scratch space. A `&str` (the /// dashboard state-file allow-list uses it for prefix checks), so it /// stays a const; [`shared_root`] wraps it. // nix: bind-mounted into every agent container as `/shared` (the harness nix modules) — must match. pub const SHARED_ROOT: &str = "/var/lib/hyperhive/shared"; #[must_use] pub fn shared_root() -> PathBuf { PathBuf::from(SHARED_ROOT) } /// `knowledge/` — local checkout of the `internal/knowledge` repo. A /// `&str` (used as a git `-C` arg / clone target throughout the knowledge /// worker), so it stays a const rather than a `PathBuf` fn. // nix: bind-mounted read-only into agent containers as `/knowledge` (the harness nix modules) — must match. pub const KNOWLEDGE_DIR: &str = "/var/lib/hyperhive/knowledge"; /// `gateway/` — generated nginx include fragments for the gateway vhost. /// The gateway container bind-mounts *this subdir only* (not the whole /// state root) at `/run/hive-state/`, so nginx can read `agents.conf` /// without the rest of `/var/lib/hyperhive/` (forge/matrix tokens, etc.) /// being exposed to the gateway container. // nix: bind-mounted into the gateway container (hive-gateway.nix) — must match. #[must_use] pub fn gateway_dir() -> PathBuf { state_root().join("gateway") } /// `gateway/agents.conf` — per-agent nginx `location` blocks (UDS upstreams). #[must_use] pub fn gateway_agents_conf() -> PathBuf { gateway_dir().join("agents.conf") } /// `forge-core-token` — the hive-c0re forge account API token. A `&str` /// (used in `Path::new` + user-facing `format!` messages), so it stays a /// const rather than a `PathBuf` fn. // nix: bind-mounted into the forge container / read at provisioning (hive-forge.nix) — must match. pub const FORGE_CORE_TOKEN: &str = "/var/lib/hyperhive/forge-core-token"; /// `matrix-register-token` — shared matrix registration token. // nix: bind-mounted into the tuwunel/matrix container (hive-matrix.nix) — must match. #[must_use] pub fn matrix_register_token() -> PathBuf { state_root().join("matrix-register-token") } /// `.meta-migration-done` — one-shot marker: legacy meta layout migrated. #[must_use] pub fn meta_migration_marker() -> PathBuf { state_root().join(".meta-migration-done") } /// `.hroot-rename-done` — one-shot marker: legacy hive-root rename applied. #[must_use] pub fn hroot_rename_marker() -> PathBuf { state_root().join(".hroot-rename-done") } /// `/run/hyperhive` — the runtime root (host admin socket + per-agent dirs). #[must_use] pub fn runtime_root() -> PathBuf { PathBuf::from(RUNTIME_ROOT) } /// `/run/hyperhive/agents` — per-agent runtime dir root (regenerated each boot). #[must_use] pub fn agent_runtime_root() -> PathBuf { runtime_root().join("agents") } /// `/run/hyperhive/agents/` — one agent's runtime dir. #[must_use] pub fn agent_runtime_dir(name: &str) -> PathBuf { agent_runtime_root().join(name) } /// 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); } // `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"); } }