refactor(#2285): repoint all hive-c0re host-path consumers to paths.rs

This commit is contained in:
damocles 2026-07-10 19:52:38 +02:00 committed by mara
commit 187c364feb
16 changed files with 95 additions and 125 deletions

View file

@ -421,10 +421,11 @@ enum MatrixCmd {
}, },
} }
/// Default htpasswd file path — the host-side location of the gateway's // Default htpasswd file path — the host-side location of the gateway's
/// credential store, pre-created by a tmpfiles rule when // credential store, pre-created by a tmpfiles rule when
/// `services.hyperhive.gateway.auth.enable = true`. // `services.hyperhive.gateway.auth.enable = true`. Literal lives in
const DEFAULT_HTPASSWD_FILE: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd"; // `hive_c0re::paths`.
use hive_c0re::paths::GATEWAY_HTPASSWD as DEFAULT_HTPASSWD_FILE;
#[derive(Subcommand)] #[derive(Subcommand)]
enum GatewayCmd { enum GatewayCmd {
@ -530,10 +531,10 @@ enum QuotaCmd {
}, },
} }
/// Default host admin socket path. Must match `hive-c0re`'s default in // Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
/// `main.rs` (`/run/hyperhive/host.sock`) — the daemon binds there and // default via `hive_c0re::paths::HOST_SOCKET` — the daemon binds there
/// `hivectl agents` connects to it. // and `hivectl agents` connects to it.
const DEFAULT_HOST_SOCKET: &str = "/run/hyperhive/host.sock"; use hive_c0re::paths::HOST_SOCKET as DEFAULT_HOST_SOCKET;
#[derive(Subcommand)] #[derive(Subcommand)]
enum AgentsCmd { enum AgentsCmd {
@ -689,10 +690,12 @@ async fn main() -> Result<()> {
/// core (headless / SSH hosts where no browser opener exists); the open /// core (headless / SSH hosts where no browser opener exists); the open
/// is convenience on top, so a missing/failed `xdg-open` is not an error. /// is convenience on top, so a missing/failed `xdg-open` is not an error.
async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> { async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
let urls = query_hive_urls(socket).await.context( let urls = query_hive_urls(socket).await.with_context(|| {
"could not reach the hive-c0re daemon for URLs — is hive-c0re running? \ format!(
(the socket is at /run/hyperhive/host.sock)", "could not reach the hive-c0re daemon for URLs — is hive-c0re running? \
)?; (the socket is at {DEFAULT_HOST_SOCKET})"
)
})?;
let (url, hint) = match target { let (url, hint) = match target {
OpenTarget::Home => ( OpenTarget::Home => (
urls.home, urls.home,
@ -1067,7 +1070,10 @@ fn agent_exists(name: &str) -> Result<bool> {
/// container. /// container.
fn choom(name: &str, resume_session: Option<&str>) -> Result<()> { fn choom(name: &str, resume_session: Option<&str>) -> Result<()> {
if !agent_exists(name)? { if !agent_exists(name)? {
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)"); bail!(
"no such agent: '{name}' (no state dir under {}/)",
hive_c0re::paths::AGENTS_ROOT
);
} }
let container = hive_c0re::lifecycle::container_name(name); let container = hive_c0re::lifecycle::container_name(name);
// Enter as the agent's unix user (== agent name) so claude reads the // Enter as the agent's unix user (== agent name) so claude reads the

View file

@ -244,7 +244,7 @@ pub fn hive_swarm_names() -> (Option<String>, Option<String>) {
/// render the `deployed:<sha12>` chip per container row. /// render the `deployed:<sha12>` chip per container row.
fn read_meta_locked_revs() -> HashMap<String, String> { fn read_meta_locked_revs() -> HashMap<String, String> {
let mut out = HashMap::new(); let mut out = HashMap::new();
let Ok(raw) = std::fs::read_to_string("/var/lib/hyperhive/meta/flake.lock") else { let Ok(raw) = std::fs::read_to_string(crate::paths::meta_flake_lock()) else {
return out; return out;
}; };
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else { let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {

View file

@ -28,16 +28,6 @@ const DASHBOARD_CHANNEL: usize = 256;
/// `Coordinator::set_last_stopped_running`. /// `Coordinator::set_last_stopped_running`.
const LAST_STOPPED_RUNNING_KEY: &str = "last_stopped_running"; const LAST_STOPPED_RUNNING_KEY: &str = "last_stopped_running";
const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents";
/// Manager-editable per-agent config repos. Bind-mounted RW into the manager
/// container as `/agents/<name>/`. Hive-c0re only writes to these on first
/// spawn (initial commit); after that it's manager-only.
const AGENT_STATE_ROOT: &str = "/var/lib/hyperhive/agents";
/// Hive-c0re-only authoritative per-agent config repos. Containers build from
/// these. Manager has no filesystem access; the only way to update is via
/// `request_apply_commit` + user approval.
const APPLIED_STATE_ROOT: &str = "/var/lib/hyperhive/applied";
pub struct Coordinator { pub struct Coordinator {
pub broker: Arc<Broker>, pub broker: Arc<Broker>,
pub approvals: Arc<Approvals>, pub approvals: Arc<Approvals>,
@ -1443,7 +1433,7 @@ impl Coordinator {
} }
pub fn agent_dir(name: &str) -> PathBuf { pub fn agent_dir(name: &str) -> PathBuf {
PathBuf::from(format!("{AGENT_RUNTIME_ROOT}/{name}")) crate::paths::agent_runtime_dir(name)
} }
pub fn socket_path(name: &str) -> PathBuf { pub fn socket_path(name: &str) -> PathBuf {
@ -1454,7 +1444,7 @@ impl Coordinator {
/// ///
/// Per-agent state root (parent of `config/`, future `prompts/`, etc.). /// Per-agent state root (parent of `config/`, future `prompts/`, etc.).
pub fn agent_state_root(name: &str) -> PathBuf { pub fn agent_state_root(name: &str) -> PathBuf {
PathBuf::from(format!("{AGENT_STATE_ROOT}/{name}")) crate::paths::agent_state_dir(name)
} }
/// Manager-editable proposed config repo. Bind-mounted into the manager /// Manager-editable proposed config repo. Bind-mounted into the manager
@ -1490,7 +1480,7 @@ impl Coordinator {
/// Authoritative applied config repo. Hive-c0re-only. /// Authoritative applied config repo. Hive-c0re-only.
pub fn agent_applied_dir(name: &str) -> PathBuf { pub fn agent_applied_dir(name: &str) -> PathBuf {
PathBuf::from(format!("{APPLIED_STATE_ROOT}/{name}")) crate::paths::applied_dir(name)
} }
/// Enumerate names that have a persistent state dir under /// Enumerate names that have a persistent state dir under
@ -1500,7 +1490,7 @@ impl Coordinator {
/// subtracting `lifecycle::list()`. /// subtracting `lifecycle::list()`.
#[must_use] #[must_use]
pub fn kept_state_names() -> Vec<String> { pub fn kept_state_names() -> Vec<String> {
let Ok(rd) = std::fs::read_dir(AGENT_STATE_ROOT) else { let Ok(rd) = std::fs::read_dir(crate::paths::agents_root()) else {
return Vec::new(); return Vec::new();
}; };
let mut out: Vec<String> = rd let mut out: Vec<String> = rd

View file

@ -50,7 +50,7 @@ pub struct MetaInputView {
/// tree — every input shown once, at its shallowest path. /// tree — every input shown once, at its shallowest path.
pub(super) fn read_meta_inputs() -> Vec<MetaInputView> { pub(super) fn read_meta_inputs() -> Vec<MetaInputView> {
let mut out = Vec::new(); let mut out = Vec::new();
let Ok(raw) = std::fs::read_to_string("/var/lib/hyperhive/meta/flake.lock") else { let Ok(raw) = std::fs::read_to_string(crate::paths::meta_flake_lock()) else {
return out; return out;
}; };
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else { let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {

View file

@ -13,6 +13,7 @@ use axum::response::{IntoResponse, Response};
use serde::Deserialize; use serde::Deserialize;
use super::error_response; use super::error_response;
use crate::paths::{AGENTS_ROOT, SHARED_ROOT};
#[derive(Deserialize)] #[derive(Deserialize)]
pub(super) struct StateFileQuery { pub(super) struct StateFileQuery {
@ -27,8 +28,6 @@ fn resolve_state_path(
raw: &str, raw: &str,
) -> std::result::Result<(std::path::PathBuf, std::fs::Metadata), String> { ) -> std::result::Result<(std::path::PathBuf, std::fs::Metadata), String> {
use std::os::unix::fs::PermissionsExt as _; use std::os::unix::fs::PermissionsExt as _;
const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
const SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
let raw = raw.trim(); let raw = raw.trim();
let (mapped, root): (std::path::PathBuf, &str) = let (mapped, root): (std::path::PathBuf, &str) =
if let Some(rest) = raw.strip_prefix("/agents/") { if let Some(rest) = raw.strip_prefix("/agents/") {
@ -136,12 +135,9 @@ fn reject_symlinks_below(
/// broker-message ingest so the dashboard event already carries the /// broker-message ingest so the dashboard event already carries the
/// verified set; security rules stay in sync with the read endpoint. /// verified set; security rules stay in sync with the read endpoint.
pub fn scan_validated_paths(body: &str) -> Vec<String> { pub fn scan_validated_paths(body: &str) -> Vec<String> {
const PREFIXES: [&str; 4] = [ let agents_slash = format!("{AGENTS_ROOT}/");
"/agents/", let shared_slash = format!("{SHARED_ROOT}/");
"/shared/", let prefixes: [&str; 4] = ["/agents/", "/shared/", &agents_slash, &shared_slash];
"/var/lib/hyperhive/agents/",
"/var/lib/hyperhive/shared/",
];
let mut out = Vec::<String>::new(); let mut out = Vec::<String>::new();
for raw in body.split(|c: char| c.is_whitespace()) { for raw in body.split(|c: char| c.is_whitespace()) {
// Trim trailing natural-language punctuation that wouldn't // Trim trailing natural-language punctuation that wouldn't
@ -151,7 +147,7 @@ pub fn scan_validated_paths(body: &str) -> Vec<String> {
if token.is_empty() { if token.is_empty() {
continue; continue;
} }
if !PREFIXES.iter().any(|p| token.starts_with(p)) { if !prefixes.iter().any(|p| token.starts_with(p)) {
continue; continue;
} }
// Cheap dedupe — typical message has 0-3 refs. // Cheap dedupe — typical message has 0-3 refs.

View file

@ -31,8 +31,6 @@ static LAST_FAILED_RELOAD: AtomicU64 = AtomicU64::new(0);
/// Minimum gap between retry attempts after a reload failure (30 s). /// Minimum gap between retry attempts after a reload failure (30 s).
const RELOAD_RETRY_SECS: u64 = 30; const RELOAD_RETRY_SECS: u64 = 30;
const HOST_CONF_PATH: &str = "/var/lib/hyperhive/gateway/agents.conf";
/// Host-side path where c0re writes the generated nginx include file. /// Host-side path where c0re writes the generated nginx include file.
/// The gateway container bind-mounts `/var/lib/hyperhive/gateway/` /// The gateway container bind-mounts `/var/lib/hyperhive/gateway/`
/// (not the whole parent dir) at `/run/hive-state/` so nginx inside /// (not the whole parent dir) at `/run/hive-state/` so nginx inside
@ -41,7 +39,7 @@ const HOST_CONF_PATH: &str = "/var/lib/hyperhive/gateway/agents.conf";
/// forge tokens or other credentials) to the gateway container. /// forge tokens or other credentials) to the gateway container.
#[must_use] #[must_use]
pub fn host_conf_path() -> PathBuf { pub fn host_conf_path() -> PathBuf {
PathBuf::from(HOST_CONF_PATH) crate::paths::gateway_agents_conf()
} }
/// Nginx proxy headers present in every per-agent location block. /// Nginx proxy headers present in every per-agent location block.

View file

@ -61,49 +61,22 @@ pub const CONTAINER_MANAGER_AGENTS_MOUNT: &str = "/agents";
/// inside the container. /// inside the container.
pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied"; pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied";
/// The on-host root that gets bind-mounted to `/agents` inside the manager.
/// Hard-coded to match `AGENT_STATE_ROOT` in coordinator.rs (kept duplicated
/// here so lifecycle stays usable as a leaf module).
pub(super) const HOST_AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
/// On-host applied repo root, mirrored RO into the manager. Matches
/// `APPLIED_STATE_ROOT` in coordinator.rs.
const HOST_APPLIED_ROOT: &str = "/var/lib/hyperhive/applied";
/// On-host meta repo root, mirrored RO into the manager. Matches
/// `meta::meta_dir()` but duplicated here so lifecycle stays a leaf.
const HOST_META_ROOT: &str = "/var/lib/hyperhive/meta";
/// Shared directory accessible to all agents. All agents bind-mount this RW.
const HOST_SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
/// Append bind flags for `child`'s state, harness, and config dirs into /// Append bind flags for `child`'s state, harness, and config dirs into
/// `binds`, all read-write. The RW on `state` is deliberate (recovery), /// `binds`, all read-write. The RW on `state` is deliberate (recovery),
/// not an oversight; see docs/persistence.md ("Parent access to child /// not an oversight; see docs/persistence.md ("Parent access to child
/// state") for the rationale. Creates missing host-side directories so /// state") for the rationale. Creates missing host-side directories so
/// nspawn doesn't refuse to start; missing dirs are non-fatal. /// nspawn doesn't refuse to start; missing dirs are non-fatal.
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) { fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
let state_dir = format!("{HOST_AGENTS_ROOT}/{child}/state"); let child_root = crate::paths::agent_state_dir(child);
let harness_dir = format!("{HOST_AGENTS_ROOT}/{child}/harness"); for sub in ["state", "harness", "config"] {
let config_dir = format!("{HOST_AGENTS_ROOT}/{child}/config"); let host = child_root.join(sub);
for dir in [&state_dir, &harness_dir, &config_dir] { let _ = std::fs::create_dir_all(&host);
let _ = std::fs::create_dir_all(dir); binds.push(BindMount {
host_path: host.to_string_lossy().into_owned(),
container_path: format!("/agents/{child}/{sub}"),
read_only: false,
});
} }
binds.push(BindMount {
host_path: state_dir,
container_path: format!("/agents/{child}/state"),
read_only: false,
});
binds.push(BindMount {
host_path: harness_dir,
container_path: format!("/agents/{child}/harness"),
read_only: false,
});
binds.push(BindMount {
host_path: config_dir,
container_path: format!("/agents/{child}/config"),
read_only: false,
});
} }
/// Hive-wide secrets forwarded into every agent container via nspawn /// Hive-wide secrets forwarded into every agent container via nspawn
@ -154,8 +127,9 @@ async fn set_nspawn_flags(
notes_dir: &Path, notes_dir: &Path,
) -> Result<()> { ) -> Result<()> {
// Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist. // Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist.
std::fs::create_dir_all(HOST_SHARED_ROOT) let shared_root = crate::paths::shared_root();
.with_context(|| format!("create {HOST_SHARED_ROOT}"))?; std::fs::create_dir_all(&shared_root)
.with_context(|| format!("create {}", shared_root.display()))?;
// Make /shared writable by every agent. Containers share host uids (no // Make /shared writable by every agent. Containers share host uids (no
// PrivateUsers), but each agent is a distinct unix user, so a root-owned // PrivateUsers), but each agent is a distinct unix user, so a root-owned
// 0755 dir leaves them unable to write — the documented "read/write for // 0755 dir leaves them unable to write — the documented "read/write for
@ -169,8 +143,8 @@ async fn set_nspawn_flags(
{ {
use std::os::unix::fs::PermissionsExt as _; use std::os::unix::fs::PermissionsExt as _;
let perms = std::fs::Permissions::from_mode(0o1777); let perms = std::fs::Permissions::from_mode(0o1777);
std::fs::set_permissions(HOST_SHARED_ROOT, perms) std::fs::set_permissions(&shared_root, perms)
.with_context(|| format!("chmod 1777 {HOST_SHARED_ROOT}"))?; .with_context(|| format!("chmod 1777 {}", shared_root.display()))?;
} }
// Ensure /knowledge dir exists. It may be empty until forge seeds it; // Ensure /knowledge dir exists. It may be empty until forge seeds it;
// nspawn refuses to start if the bind source is missing entirely. // nspawn refuses to start if the bind source is missing entirely.
@ -204,7 +178,7 @@ async fn set_nspawn_flags(
read_only: false, read_only: false,
}, },
BindMount { BindMount {
host_path: HOST_SHARED_ROOT.to_owned(), host_path: shared_root.to_string_lossy().into_owned(),
container_path: CONTAINER_SHARED_MOUNT.to_owned(), container_path: CONTAINER_SHARED_MOUNT.to_owned(),
read_only: false, read_only: false,
}, },
@ -234,10 +208,11 @@ async fn set_nspawn_flags(
read_only: false, read_only: false,
}); });
} }
let own_config = format!("{HOST_AGENTS_ROOT}/{agent_name}/config"); let own_config = crate::paths::agent_state_dir(agent_name).join("config");
std::fs::create_dir_all(&own_config).with_context(|| format!("create {own_config}"))?; std::fs::create_dir_all(&own_config)
.with_context(|| format!("create {}", own_config.display()))?;
binds.push(BindMount { binds.push(BindMount {
host_path: own_config, host_path: own_config.to_string_lossy().into_owned(),
container_path: format!("/agents/{agent_name}/config"), container_path: format!("/agents/{agent_name}/config"),
read_only: true, read_only: true,
}); });
@ -270,15 +245,16 @@ async fn set_nspawn_flags(
// startup migration, but make sure the directory is there // startup migration, but make sure the directory is there
// before the role holder comes up in case set_nspawn_flags // before the role holder comes up in case set_nspawn_flags
// fires first (e.g. cold start with no agents). // fires first (e.g. cold start with no agents).
std::fs::create_dir_all(HOST_META_ROOT) let meta_root = crate::paths::meta_root();
.with_context(|| format!("create {HOST_META_ROOT}"))?; std::fs::create_dir_all(&meta_root)
.with_context(|| format!("create {}", meta_root.display()))?;
binds.push(BindMount { binds.push(BindMount {
host_path: HOST_APPLIED_ROOT.to_owned(), host_path: crate::paths::applied_root().to_string_lossy().into_owned(),
container_path: CONTAINER_MANAGER_APPLIED_MOUNT.to_owned(), container_path: CONTAINER_MANAGER_APPLIED_MOUNT.to_owned(),
read_only: true, read_only: true,
}); });
binds.push(BindMount { binds.push(BindMount {
host_path: HOST_META_ROOT.to_owned(), host_path: meta_root.to_string_lossy().into_owned(),
container_path: crate::meta::CONTAINER_MANAGER_META_MOUNT.to_owned(), container_path: crate::meta::CONTAINER_MANAGER_META_MOUNT.to_owned(),
read_only: true, read_only: true,
}); });

View file

@ -864,7 +864,7 @@ pub async fn sync_tmpfiles() {
/// # Errors /// # Errors
/// Returns an error if `create_dir_all` fails. /// Returns an error if `create_dir_all` fails.
pub fn ensure_agent_runtime_dir(name: &str) -> Result<()> { pub fn ensure_agent_runtime_dir(name: &str) -> Result<()> {
let dir = std::path::PathBuf::from(format!("/run/hyperhive/agents/{name}")); let dir = crate::paths::agent_runtime_dir(name);
std::fs::create_dir_all(&dir) std::fs::create_dir_all(&dir)
.with_context(|| format!("create agent runtime dir {}", dir.display())) .with_context(|| format!("create agent runtime dir {}", dir.display()))
} }

View file

@ -9,7 +9,6 @@ use anyhow::{Context, Result, bail};
use super::git::{ use super::git::{
git, git_command, git_commit, git_read_tree_reset, git_rev_parse, git_root_commit, git_tag, git, git_command, git_commit, git_read_tree_reset, git_rev_parse, git_root_commit, git_tag,
}; };
use super::host_config::HOST_AGENTS_ROOT;
/// Initialize the manager-editable proposed repo. Seeds two tracked /// Initialize the manager-editable proposed repo. Seeds two tracked
/// files: `agent.nix` (the module the manager edits) and `flake.nix` /// files: `agent.nix` (the module the manager edits) and `flake.nix`
@ -217,7 +216,7 @@ pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
/// brand-new agent on a btrfs host gets a real subvolume. Subvolume creation /// brand-new agent on a btrfs host gets a real subvolume. Subvolume creation
/// is privileged, so it's delegated to hive-priv. /// is privileged, so it's delegated to hive-priv.
pub async fn ensure_agent_state_subvolume(name: &str) -> Result<()> { pub async fn ensure_agent_state_subvolume(name: &str) -> Result<()> {
let root = Path::new(HOST_AGENTS_ROOT).join(name); let root = crate::paths::agent_state_dir(name);
if root.exists() { if root.exists() {
return Ok(()); return Ok(());
} }

View file

@ -21,7 +21,7 @@ use hive_c0re::{
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")] #[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")]
struct Cli { struct Cli {
/// Path to the host admin socket. /// Path to the host admin socket.
#[arg(long, global = true, default_value = "/run/hyperhive/host.sock")] #[arg(long, global = true, default_value = hive_c0re::paths::HOST_SOCKET)]
socket: PathBuf, socket: PathBuf,
#[command(subcommand)] #[command(subcommand)]

View file

@ -7,7 +7,7 @@
//! the full UIAA round-trip, token-file shape, and host/container //! the full UIAA round-trip, token-file shape, and host/container
//! bind-mount layout. //! bind-mount layout.
use std::path::{Path, PathBuf}; use std::path::PathBuf;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use reqwest::StatusCode; use reqwest::StatusCode;
@ -22,11 +22,6 @@ const MATRIX_CONTAINER: &str = "hive-matrix";
/// netns so `localhost:<port>` resolves both from the daemon and from /// netns so `localhost:<port>` resolves both from the daemon and from
/// inside any sub-agent container. /// inside any sub-agent container.
const MATRIX_HTTP: &str = "http://localhost:8008"; const MATRIX_HTTP: &str = "http://localhost:8008";
/// Host path of the matrix registration token. Must match
/// `hyperhive.matrix.registrationTokenFile` in `nix/modules/hive-matrix.nix`
/// (same path is bind-mounted read-only into the tuwunel container so
/// the homeserver can read it via `registration_token_file`).
const REGISTER_TOKEN_PATH: &str = "/var/lib/hyperhive/matrix-register-token";
/// Length (bytes) of the random registration token. 32 raw bytes ⇒ /// Length (bytes) of the random registration token. 32 raw bytes ⇒
/// 64-char hex string; comfortable for a long-lived shared secret. /// 64-char hex string; comfortable for a long-lived shared secret.
const REGISTER_TOKEN_BYTES: usize = 32; const REGISTER_TOKEN_BYTES: usize = 32;
@ -148,8 +143,8 @@ fn random_hex(n: usize) -> Result<String> {
/// against the same secret hive-c0re holds. /// against the same secret hive-c0re holds.
pub fn ensure_register_token() -> Result<String> { pub fn ensure_register_token() -> Result<String> {
use std::os::unix::fs::PermissionsExt; use std::os::unix::fs::PermissionsExt;
let path = Path::new(REGISTER_TOKEN_PATH); let path = crate::paths::matrix_register_token();
if let Ok(existing) = std::fs::read_to_string(path) { if let Ok(existing) = std::fs::read_to_string(&path) {
let trimmed = existing.trim().to_owned(); let trimmed = existing.trim().to_owned();
if !trimmed.is_empty() { if !trimmed.is_empty() {
return Ok(trimmed); return Ok(trimmed);
@ -159,9 +154,9 @@ pub fn ensure_register_token() -> Result<String> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok(); std::fs::create_dir_all(parent).ok();
} }
std::fs::write(path, format!("{token}\n")) std::fs::write(&path, format!("{token}\n"))
.with_context(|| format!("write registration token to {}", path.display()))?; .with_context(|| format!("write registration token to {}", path.display()))?;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)); let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
tracing::info!(path = %path.display(), "matrix: generated registration token"); tracing::info!(path = %path.display(), "matrix: generated registration token");
Ok(token) Ok(token)
} }

View file

@ -13,8 +13,6 @@ use tokio::sync::Mutex;
use crate::coordinator::HiveEnv; use crate::coordinator::HiveEnv;
use crate::lifecycle; use crate::lifecycle;
const META_ROOT: &str = "/var/lib/hyperhive/meta";
const APPLIED_ROOT: &str = "/var/lib/hyperhive/applied";
const GIT_NAME: &str = "c0re"; const GIT_NAME: &str = "c0re";
const GIT_EMAIL: &str = "c0re@hyperhive.local"; const GIT_EMAIL: &str = "c0re@hyperhive.local";
@ -60,7 +58,7 @@ pub struct AgentSpec {
#[must_use] #[must_use]
pub fn meta_dir() -> PathBuf { pub fn meta_dir() -> PathBuf {
PathBuf::from(META_ROOT) crate::paths::meta_root()
} }
/// Idempotently reconcile the meta repo with the current agent set. /// Idempotently reconcile the meta repo with the current agent set.
@ -840,7 +838,7 @@ fn ca_embed_state(dir: &std::path::Path) -> (Vec<(String, String)>, bool) {
/// Returns an empty vec when the lock is missing or unparsable — /// Returns an empty vec when the lock is missing or unparsable —
/// safe degradation, the worst case is no dedup for that agent. /// safe degradation, the worst case is no dedup for that agent.
fn agent_canonical_inputs(name: &str) -> Vec<&'static str> { fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
let path = std::path::PathBuf::from(format!("{APPLIED_ROOT}/{name}/flake.lock")); let path = crate::paths::applied_dir(name).join("flake.lock");
let Ok(raw) = std::fs::read_to_string(&path) else { let Ok(raw) = std::fs::read_to_string(&path) else {
return Vec::new(); return Vec::new();
}; };
@ -931,8 +929,9 @@ where
for spec in agents { for spec in agents {
let _ = writeln!( let _ = writeln!(
out, out,
" agent-{}.url = \"git+file://{APPLIED_ROOT}/{}\";", " agent-{}.url = \"git+file://{}\";",
spec.name, spec.name, spec.name,
crate::paths::applied_dir(&spec.name).display(),
); );
// For each canonical input the agent declares in its own // For each canonical input the agent declares in its own
// `flake.nix` (detected by reading its applied `flake.lock`), // `flake.nix` (detected by reading its applied `flake.lock`),

View file

@ -20,13 +20,13 @@ const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
/// Marker for phase 4. Once present, container repoint is skipped on /// Marker for phase 4. Once present, container repoint is skipped on
/// future restarts. /// future restarts.
fn repoint_marker() -> PathBuf { fn repoint_marker() -> PathBuf {
PathBuf::from("/var/lib/hyperhive/.meta-migration-done") crate::paths::meta_migration_marker()
} }
/// Marker for phase 5. Once present, root→h-root container rename is /// Marker for phase 5. Once present, root→h-root container rename is
/// skipped on future restarts. /// skipped on future restarts.
fn hroot_rename_marker() -> PathBuf { fn hroot_rename_marker() -> PathBuf {
PathBuf::from("/var/lib/hyperhive/.hroot-rename-done") crate::paths::hroot_rename_marker()
} }
/// Substring that identifies the *current* agent flake boilerplate. /// Substring that identifies the *current* agent flake boilerplate.
@ -45,7 +45,7 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
// can leave `.git/index.lock` behind, which blocks every // can leave `.git/index.lock` behind, which blocks every
// subsequent meta op until somebody `rm`s it manually. We just // subsequent meta op until somebody `rm`s it manually. We just
// booted so nothing of ours is holding it; safe to clear. // booted so nothing of ours is holding it; safe to clear.
let meta_lock = std::path::PathBuf::from("/var/lib/hyperhive/meta/.git/index.lock"); let meta_lock = crate::paths::meta_git_index_lock();
if meta_lock.exists() { if meta_lock.exists() {
match std::fs::remove_file(&meta_lock) { match std::fs::remove_file(&meta_lock) {
Ok(()) => tracing::warn!("cleared stale meta/.git/index.lock"), Ok(()) => tracing::warn!("cleared stale meta/.git/index.lock"),

View file

@ -148,11 +148,15 @@ pub fn agent_sockets_file() -> PathBuf {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// `agents/` — per-agent persistent state root (one subdir per agent, /// `agents/` — per-agent persistent state root (one subdir per agent,
/// bind-mounted into each container as `/agents/<name>`). /// 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. // nix: agent container bind-mount source (harness-base.nix / agent-base.nix) — must match.
pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
#[must_use] #[must_use]
pub fn agents_root() -> PathBuf { pub fn agents_root() -> PathBuf {
state_root().join("agents") PathBuf::from(AGENTS_ROOT)
} }
/// `agents/<name>` — one agent's persistent state root. /// `agents/<name>` — one agent's persistent state root.
@ -202,11 +206,15 @@ pub fn meta_git_index_lock() -> PathBuf {
meta_root().join(".git/index.lock") meta_root().join(".git/index.lock")
} }
/// `shared/` — the cross-agent `/shared` scratch space. /// `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. // 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] #[must_use]
pub fn shared_root() -> PathBuf { pub fn shared_root() -> PathBuf {
state_root().join("shared") PathBuf::from(SHARED_ROOT)
} }
/// `knowledge/` — local checkout of the `internal/knowledge` repo. A /// `knowledge/` — local checkout of the `internal/knowledge` repo. A
@ -228,6 +236,12 @@ pub fn gateway_agents_conf() -> PathBuf {
gateway_dir().join("agents.conf") 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` /// `forge-core-token` — the hive-c0re forge account API token. A `&str`
/// (used in `Path::new` + user-facing `format!` messages), so it stays a /// (used in `Path::new` + user-facing `format!` messages), so it stays a
/// const rather than a `PathBuf` fn. /// const rather than a `PathBuf` fn.

View file

@ -17,8 +17,9 @@ use anyhow::{Context, Result};
/// gateway container bind-mounts this whole tree (read-only) so it /// gateway container bind-mounts this whole tree (read-only) so it
/// can `proxy_pass` to any agent. Each agent's container bind-mounts /// can `proxy_pass` to any agent. Each agent's container bind-mounts
/// only its own `<name>/` subdir — agents can only access their own /// only its own `<name>/` subdir — agents can only access their own
/// sockets. /// sockets. The literal lives in [`crate::paths`]; re-exported here
pub const AGENT_SOCKET_DIR: &str = "/run/hive-agent"; /// under the name this module's consumers have always used.
pub use crate::paths::AGENT_SOCKET_DIR;
/// Socket filename inside each per-agent subdir. Fixed so the path /// Socket filename inside each per-agent subdir. Fixed so the path
/// derives entirely from `(AGENT_SOCKET_DIR, name)` — no second /// derives entirely from `(AGENT_SOCKET_DIR, name)` — no second

View file

@ -29,7 +29,7 @@ use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
/// keep it out of the applied repo's git history. Uses a leading dot so a /// keep it out of the applied repo's git history. Uses a leading dot so a
/// glob over `applied/*` doesn't include it. /// glob over `applied/*` doesn't include it.
pub fn rev_marker_path(name: &str) -> PathBuf { pub fn rev_marker_path(name: &str) -> PathBuf {
PathBuf::from(format!("/var/lib/hyperhive/applied/.{name}.hyperhive-rev")) crate::paths::applied_rev_marker(name)
} }
/// Resolve the current rev of `hyperhive_flake`. For a path on disk we /// Resolve the current rev of `hyperhive_flake`. For a path on disk we
@ -58,13 +58,9 @@ pub fn current_flake_rev(hyperhive_flake: &str) -> Option<String> {
/// nix-build disk saturation that's long enough that concurrent sweeps /// nix-build disk saturation that's long enough that concurrent sweeps
/// starved the runtime and stalled the per-agent sockets. /// starved the runtime and stalled the per-agent sockets.
pub async fn agent_config_pending(name: &str, deployed_sha: Option<&str>) -> bool { pub async fn agent_config_pending(name: &str, deployed_sha: Option<&str>) -> bool {
let applied = crate::paths::applied_dir(name);
let applied_head = tokio::process::Command::new("git") let applied_head = tokio::process::Command::new("git")
.args([ .args(["-C", &applied.to_string_lossy(), "rev-parse", "HEAD"])
"-C",
&format!("/var/lib/hyperhive/applied/{name}"),
"rev-parse",
"HEAD",
])
.output() .output()
.await .await
.ok() .ok()