hyperhive/hive-c0re/src/paths.rs
atlas dcd559e7c7 fix(#2368): AGENT_RUNTIME_ROOT → priv_proto; fix stale priv comment; cross-ref lockstep
- Add `priv_proto::AGENT_RUNTIME_ROOT` to hive-sh4re as the shared
  single source for the per-agent runtime root path.  hive-priv now
  imports it instead of carrying a local const with a stale comment
  that still pointed at `coordinator::AGENT_RUNTIME_ROOT` (removed in
  #2285/#2367 — moved to `paths::agent_runtime_root()`).

- Add 'must stay in sync' cross-ref comments on both sides of the
  privsep boundary:
    · priv_proto::META_DIR ↔ paths::meta_root()
    · priv_proto::AGENT_STATE_ROOT ↔ paths::AGENTS_ROOT
    · priv_proto::AGENT_RUNTIME_ROOT ↔ paths::RUNTIME_ROOT + agent_runtime_root()
    · paths::AGENTS_ROOT ↔ priv_proto::AGENT_STATE_ROOT
    · paths::RUNTIME_ROOT ↔ priv_proto::AGENT_RUNTIME_ROOT

  The dep graph prevents a shared import (hive-sh4re is a leaf; both
  hive-c0re and hive-priv depend on it but not each other), so the
  lockstep comments are the enforced contract.
2026-07-11 01:22:50 +02:00

404 lines
15 KiB
Rust

//! 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};
/// 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.
// sh4re: `hive_sh4re::priv_proto::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";
/// Default host admin socket (`/run/hyperhive/host.sock`). Exposed as a
/// `&str` for the `--socket` / `--host-socket` clap `default_value` in
/// `main.rs` (hive-c0re) and `bin/hivectl.rs`.
pub const HOST_SOCKET: &str = "/run/hyperhive/host.sock";
/// `/run/hive-agent` — per-agent runtime socket dir root (web + bound
/// markers), one subdir per agent.
// nix: agent container bind-mount / `RuntimeDirectory` (harness-base.nix) — 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")
}
/// `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-<name>` — marker: `<name>`'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-<name>` — marker: `<name>`'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/` — per-agent persistent state root (one subdir per agent,
/// bind-mounted into each container as `/agents/<name>`). A `&str` (the
/// dashboard state-file allow-list uses it for `strip_prefix` /
/// `starts_with` checks), so it stays a const; [`agents_root`] wraps it.
// nix: agent container bind-mount source (harness-base.nix / agent-base.nix) — must match.
// sh4re: `hive_sh4re::priv_proto::AGENT_STATE_ROOT` is the same value and must stay in sync;
// the privsep boundary prevents importing across the crate.
pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
#[must_use]
pub fn agents_root() -> PathBuf {
PathBuf::from(AGENTS_ROOT)
}
/// `agents/<name>` — one agent's persistent state root.
#[must_use]
pub fn agent_state_dir(name: &str) -> PathBuf {
agents_root().join(name)
}
/// `applied/` — per-agent *applied* (deployed) config repos + rev markers,
/// distinct from the proposed configs under `agents/<name>/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/<name>` — one agent's applied config working tree.
#[must_use]
pub fn applied_dir(name: &str) -> PathBuf {
applied_root().join(name)
}
/// `applied/.<name>.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` (harness-base.nix) — 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` (harness-base.nix) — 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` (harness-base.nix) — 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")
}
/// `gateway/gateway.htpasswd` — nginx basic-auth credential store for the
/// operator dashboard vhost. A `&str` (used as a `hivectl` clap
/// `default_value`), so it stays a const rather than a `PathBuf` fn.
// nix: read by the gateway container's nginx (hive-gateway.nix) — must match.
pub const GATEWAY_HTPASSWD: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd";
/// `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/<name>` — one agent's runtime dir.
#[must_use]
pub fn agent_runtime_dir(name: &str) -> PathBuf {
agent_runtime_root().join(name)
}
/// `/run/hive-agent` — per-agent socket dir root (web + bound markers).
#[must_use]
pub fn agent_socket_dir() -> PathBuf {
PathBuf::from(AGENT_SOCKET_DIR)
}
/// 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-<name>` 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");
}
}