222 lines
7.7 KiB
Rust
222 lines
7.7 KiB
Rust
//! 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-sockets.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-<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")
|
|
}
|
|
|
|
/// 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");
|
|
}
|
|
}
|