Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/hive-c0re/src/paths.rs
atlas 92559af8e7 gateway: serve the hive dashboard on <hive>.hives.<swarm> behind authelia, drop htpasswd
The dashboard, its API, the swagger UI and the agent routes move to their
own vhost, `swarm.dashboardDomain` = `<hive>.hives.<swarm.domain>`, under
the swarm domain so authelia's session cookie reaches it. `/`, `/api/` and
`/api/docs/` ask authelia through `auth_request`, and authelia's
`operatorsOnly` rule for each hive's dashboard name lets only the admins
group in. The hive domain keeps matrix discovery and redirects every other
path to the new name.

The name rides as a SAN on the hive's own gateway leaf: the per-hive bao
role allows it, and hive-tls asks for it and checks coverage.

HTTP Basic auth is gone: the htpasswd writer and its test, the
`hivectl gateway` verbs and host-socket requests, the `gateway.auth`
options (now a removed-option error), the unauthorized page and the bcrypt
dependency. tmpfiles deletes a leftover gateway.htpasswd, which held
bcrypt hashes at 0644.

c0re hands out dashboard and agent URLs on the new name
(HYPERHIVE_DASHBOARD_DOMAIN), and the swarm controller's hive roster
carries each hive's dashboard name for the swarm UI's hive list.

Closes #3469
2026-10-15 21:44:27 +02:00

357 lines
14 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-sender-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, HOST_SOCKET, agent_state_dir};
/// Name of the per-agent working dir under `agents/<name>/`, a sibling of
/// `state/`. Bound RW into the container at `/agents/<name>/work`.
pub const WORK_SUBDIR: &str = "work";
/// Root of all hive-c0re persistent state.
// nix: the same literal is written in `nix/host-modules/hive-c0re/default.nix` — must match.
// Not option-driven: there is no `statePath` option, the path is hardcoded both sides.
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
// (`nix/host-modules/hive-c0re/default.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";
/// 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")
}
/// `agent-identity/` — one subdir per agent holding the store identity this
/// hive collected for it. Staging only: the files exist so `systemd-nspawn`
/// has something to `--load-credential` from, and nothing on the host ever
/// reads them back.
///
/// Under the state root rather than `/run` deliberately. The forward happens
/// when a container's nspawn config is (re)written, and a container can be
/// restarted long after that; material that vanished with `/run` would leave
/// a boot where the agent's identity is simply absent and the only symptom is
/// a unit inside the container refusing to start.
#[must_use]
pub fn agent_identity_root() -> PathBuf {
state_root().join("agent-identity")
}
/// `agent-identity/<name>` — one agent's staged store identity.
///
/// ⚠️ Deliberately **not** under `agents/<name>`: that tree is bind-mounted
/// into the container (and a parent's, for a child), so a private key placed
/// there would be readable by the agent as a plain file, bypassing the
/// credential mechanism that exists to control exactly that.
#[must_use]
pub fn agent_identity_dir(name: &str) -> PathBuf {
agent_identity_root().join(name)
}
/// `matrix/` — host-side matrix provisioning state (the appservice sender token, hive
/// Space room id, the hive sender account's 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/access-token` — the matrix appservice sender account's access token.
#[must_use]
pub fn matrix_sender_token() -> PathBuf {
matrix_dir().join("access-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")
}
/// `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/<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` (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";
/// `/var/lib/hive-gateway/conf` — generated nginx include fragments,
/// deliberately **outside** `STATE_ROOT`.
///
/// nginx runs on the host as an unprivileged user and reads these
/// directly. Keeping them here rather than under `/var/lib/hyperhive`
/// means nginx needs no access to c0re's state dir at all — no shared
/// parent to traverse, so no group membership handing it everything else
/// that lives there (the broker db above all). The separation IS the
/// confinement; nothing else is doing that job.
///
/// Sibling of the gateway's `tls/`, which already lived here.
// nix: named by the gateway's nginx config (hive-gateway/vhosts.nix) and created by
// its tmpfiles rules (hive-gateway/default.nix) — must match.
pub const GATEWAY_CONF_DIR: &str = "/var/lib/hive-gateway/conf";
#[must_use]
pub fn gateway_dir() -> PathBuf {
PathBuf::from(GATEWAY_CONF_DIR)
}
/// `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: read at provisioning; the same literal is in
// `nix/host-modules/hive-c0re/default.nix` — must match.
pub const FORGE_CORE_TOKEN: &str = "/var/lib/hyperhive/forge-core-token";
/// `/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)
}
/// 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); 6] = [
("broker.sqlite", db_dir().join("broker.sqlite")),
("build_logs.sqlite", db_dir().join("build_logs.sqlite")),
("forge-core-avatar-set", forge_core_avatar_marker()),
("matrix-sender-token", matrix_sender_token()),
("matrix-space-room-id", matrix_space_room_id()),
("agent-sockets.json", agent_sockets_file()),
];
for (old_rel, new) in &moves {
move_if_legacy(&root.join(old_rel), new);
}
}
/// 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 Err(e) = std::fs::create_dir_all(parent)
{
tracing::warn!(
dir = %parent.display(),
error = ?e,
"relocate: create parent dir failed"
);
}
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");
}
}