diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 55a65eae..6aba31fe 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -161,8 +161,8 @@ pub async fn run_approval_apply_commit( let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::ApplyCommit)?; // Runtime dir creation is handled inside lifecycle::rebuild_no_meta's // spawn path (first-spawn) or is already present for rebuilds. - let agent_dir = Coordinator::agent_dir(&approval.agent); - let applied_dir = Coordinator::agent_applied_dir(&approval.agent); + let agent_dir = crate::paths::agent_runtime_dir(&approval.agent); + let applied_dir = crate::paths::applied_dir(&approval.agent); coord.set_queue_step(queue_entry_id, "apply commit"); let (result, terminal_tag, is_first_spawn) = run_apply_commit(coord, &approval, &agent_dir, &applied_dir, queue_entry_id).await; @@ -194,8 +194,8 @@ pub async fn run_approval_merge_config_pr( approval_id: i64, ) -> Result<()> { let approval = fetch_approval_for_worker(coord, approval_id, ApprovalKind::MergeConfigPr)?; - let agent_dir = Coordinator::agent_dir(&approval.agent); - let applied_dir = Coordinator::agent_applied_dir(&approval.agent); + let agent_dir = crate::paths::agent_runtime_dir(&approval.agent); + let applied_dir = crate::paths::applied_dir(&approval.agent); coord.set_queue_step(queue_entry_id, "merge config pr"); let (result, terminal_tag) = run_merge_config_pr(coord, &approval, &agent_dir, &applied_dir, queue_entry_id).await; @@ -911,7 +911,7 @@ pub async fn destroy(coord: &Arc, name: &str, purge: bool) -> Resul let guard = coord.transient_guard(name, TransientKind::Destroying); lifecycle::destroy(name).await?; coord.unregister_agent(name); - let runtime = Coordinator::agent_dir(name); + let runtime = crate::paths::agent_runtime_dir(name); if runtime.exists() { let _ = std::fs::remove_dir_all(&runtime); } @@ -924,8 +924,8 @@ pub async fn destroy(coord: &Arc, name: &str, purge: bool) -> Resul tracing::warn!(error = ?e, %name, "purge: delete state subvolume failed"); } for dir in [ - Coordinator::agent_state_root(name), - Coordinator::agent_applied_dir(name), + crate::paths::agent_state_dir(name), + crate::paths::applied_dir(name), ] { if dir.exists() && let Err(e) = std::fs::remove_dir_all(&dir) @@ -997,7 +997,7 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<() // the annotated body) the reason. Spawn approvals have no // commit to tag, so they fall through unannotated. if matches!(a.kind, ApprovalKind::ApplyCommit) { - let applied_dir = Coordinator::agent_applied_dir(&a.agent); + let applied_dir = crate::paths::applied_dir(&a.agent); let proposal_ref = format!("refs/tags/proposal/{id}"); if lifecycle::git_rev_parse(&applied_dir, &proposal_ref) .await diff --git a/hive-c0re/src/agent_config/capabilities.rs b/hive-c0re/src/agent_config/capabilities.rs index 396e9305..dd8e4b33 100644 --- a/hive-c0re/src/agent_config/capabilities.rs +++ b/hive-c0re/src/agent_config/capabilities.rs @@ -28,7 +28,7 @@ const CAPABILITIES_FILE: &str = "capabilities.json"; #[must_use] pub fn capabilities_path() -> PathBuf { - crate::meta::meta_dir().join(CAPABILITIES_FILE) + crate::paths::meta_root().join(CAPABILITIES_FILE) } /// Read the per-agent capability map. Returns an empty map when the diff --git a/hive-c0re/src/agent_config/tool_groups.rs b/hive-c0re/src/agent_config/tool_groups.rs index 528f94a5..4bb1fe08 100644 --- a/hive-c0re/src/agent_config/tool_groups.rs +++ b/hive-c0re/src/agent_config/tool_groups.rs @@ -30,7 +30,7 @@ const TOOL_GROUPS_FILE: &str = "tool-groups.json"; #[must_use] pub fn tool_groups_path() -> PathBuf { - crate::meta::meta_dir().join(TOOL_GROUPS_FILE) + crate::paths::meta_root().join(TOOL_GROUPS_FILE) } /// Read the per-agent tool-group map. Returns an empty map when the diff --git a/hive-c0re/src/agent_config/topology.rs b/hive-c0re/src/agent_config/topology.rs index 08acd088..cdbdb510 100644 --- a/hive-c0re/src/agent_config/topology.rs +++ b/hive-c0re/src/agent_config/topology.rs @@ -34,7 +34,7 @@ const TOPOLOGY_FILE: &str = "topology.json"; #[must_use] pub fn topology_path() -> PathBuf { - crate::meta::meta_dir().join(TOPOLOGY_FILE) + crate::paths::meta_root().join(TOPOLOGY_FILE) } /// Snapshot of the topology map. Read on every `container_view::build_all` @@ -451,7 +451,7 @@ const ROLES_FILE: &str = "roles.json"; #[must_use] pub fn roles_path() -> std::path::PathBuf { - crate::meta::meta_dir().join(ROLES_FILE) + crate::paths::meta_root().join(ROLES_FILE) } /// Read the roles map from disk. Returns an empty map when absent or diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index bde9f405..eef8d729 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -421,10 +421,11 @@ enum MatrixCmd { }, } -/// Default htpasswd file path — the host-side location of the gateway's -/// credential store, pre-created by a tmpfiles rule when -/// `services.hyperhive.gateway.auth.enable = true`. -const DEFAULT_HTPASSWD_FILE: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd"; +// Default htpasswd file path — the host-side location of the gateway's +// credential store, pre-created by a tmpfiles rule when +// `services.hyperhive.gateway.auth.enable = true`. Literal lives in +// `hive_c0re::paths`. +use hive_c0re::paths::GATEWAY_HTPASSWD as DEFAULT_HTPASSWD_FILE; #[derive(Subcommand)] enum GatewayCmd { @@ -530,10 +531,10 @@ enum QuotaCmd { }, } -/// Default host admin socket path. Must match `hive-c0re`'s default in -/// `main.rs` (`/run/hyperhive/host.sock`) — the daemon binds there and -/// `hivectl agents` connects to it. -const DEFAULT_HOST_SOCKET: &str = "/run/hyperhive/host.sock"; +// Default host admin socket path. Shared with `hive-c0re`'s `main.rs` +// default via `hive_c0re::paths::HOST_SOCKET` — the daemon binds there +// and `hivectl agents` connects to it. +use hive_c0re::paths::HOST_SOCKET as DEFAULT_HOST_SOCKET; #[derive(Subcommand)] enum AgentsCmd { @@ -689,10 +690,12 @@ async fn main() -> Result<()> { /// 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. async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> { - let urls = query_hive_urls(socket).await.context( - "could not reach the hive-c0re daemon for URLs — is hive-c0re running? \ - (the socket is at /run/hyperhive/host.sock)", - )?; + let urls = query_hive_urls(socket).await.with_context(|| { + format!( + "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 { OpenTarget::Home => ( urls.home, @@ -1045,7 +1048,7 @@ fn human_bytes(n: u64) -> String { /// "needs root" error fixes that first-run footgun, where running a /// privileged verb without sudo reported as a missing agent. fn agent_exists(name: &str) -> Result { - let root = Coordinator::agent_state_root(name); + let root = hive_c0re::paths::agent_state_dir(name); match root.try_exists() { Ok(found) => Ok(found), Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => bail!( @@ -1067,7 +1070,10 @@ fn agent_exists(name: &str) -> Result { /// container. fn choom(name: &str, resume_session: Option<&str>) -> Result<()> { 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); // Enter as the agent's unix user (== agent name) so claude reads the diff --git a/hive-c0re/src/container_view.rs b/hive-c0re/src/container_view.rs index 9cdda653..28b10f19 100644 --- a/hive-c0re/src/container_view.rs +++ b/hive-c0re/src/container_view.rs @@ -244,7 +244,7 @@ pub fn hive_swarm_names() -> (Option, Option) { /// render the `deployed:` chip per container row. fn read_meta_locked_revs() -> HashMap { 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; }; let Ok(json) = serde_json::from_str::(&raw) else { diff --git a/hive-c0re/src/coordinator.rs b/hive-c0re/src/coordinator.rs index 6312c71e..ab62c459 100644 --- a/hive-c0re/src/coordinator.rs +++ b/hive-c0re/src/coordinator.rs @@ -28,16 +28,6 @@ const DASHBOARD_CHANNEL: usize = 256; /// `Coordinator::set_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//`. 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 broker: Arc, pub approvals: Arc, @@ -547,7 +537,7 @@ impl Coordinator { /// Assemble the per-agent filesystem paths for `name`. `agent_dir` /// is the runtime directory (`/run/hyperhive/agents/`), obtained - /// from `Coordinator::agent_dir(name)` (pure path) or from + /// from `crate::paths::agent_runtime_dir(name)` (pure path) or from /// `lifecycle::ensure_agent_runtime_dir(name)` when the dir must be /// created. All other paths are derived statically from `name`. #[must_use] @@ -555,7 +545,7 @@ impl Coordinator { AgentPaths { agent_dir, proposed_dir: Self::agent_proposed_dir(name), - applied_dir: Self::agent_applied_dir(name), + applied_dir: crate::paths::applied_dir(name), claude_dir: Self::agent_claude_dir(name), notes_dir: Self::agent_notes_dir(name), } @@ -1134,7 +1124,7 @@ impl Coordinator { // Idempotent: drop any existing listener so re-registration (e.g. on rebuild, // or after a hive-c0re restart cleared /run/hyperhive) gets a fresh socket. self.unregister_agent(name); - let agent_dir = Self::agent_dir(name); + let agent_dir = crate::paths::agent_runtime_dir(name); std::fs::create_dir_all(&agent_dir) .with_context(|| format!("create agent dir {}", agent_dir.display()))?; let socket_path = Self::socket_path(name); @@ -1442,25 +1432,14 @@ impl Coordinator { errors } - pub fn agent_dir(name: &str) -> PathBuf { - PathBuf::from(format!("{AGENT_RUNTIME_ROOT}/{name}")) - } - pub fn socket_path(name: &str) -> PathBuf { - Self::agent_dir(name).join("mcp.sock") - } - - /// Ensure a runtime dir + (for sub-agents) per-agent socket exists. - /// - /// Per-agent state root (parent of `config/`, future `prompts/`, etc.). - pub fn agent_state_root(name: &str) -> PathBuf { - PathBuf::from(format!("{AGENT_STATE_ROOT}/{name}")) + crate::paths::agent_runtime_dir(name).join("mcp.sock") } /// Manager-editable proposed config repo. Bind-mounted into the manager /// container as `/agents//config/`. pub fn agent_proposed_dir(name: &str) -> PathBuf { - Self::agent_state_root(name).join("config") + crate::paths::agent_state_dir(name).join("config") } /// Per-agent Claude credentials dir. Bind-mounted RW into the agent @@ -1468,14 +1447,14 @@ impl Coordinator { /// destroy/recreate. Each agent owns its own token lineage — sharing /// would break on the first refresh-token rotation. pub fn agent_claude_dir(name: &str) -> PathBuf { - Self::agent_state_root(name).join("claude") + crate::paths::agent_state_dir(name).join("claude") } /// Per-agent durable knowledge dir. Bind-mounted RW into the agent /// container at `/agents/{name}/state`. Survives destroy/recreate. /// Agent-visible — claude is told to write long-lived notes here. pub fn agent_notes_dir(name: &str) -> PathBuf { - Self::agent_state_root(name).join("state") + crate::paths::agent_state_dir(name).join("state") } /// Per-agent harness-internal state dir. Bind-mounted RW into the @@ -1485,12 +1464,7 @@ impl Coordinator { /// from the agent-visible `state/` so claude's "my notes" view is /// uncluttered and the host vacuum has a clean sweep root. pub fn agent_harness_dir(name: &str) -> PathBuf { - Self::agent_state_root(name).join("harness") - } - - /// Authoritative applied config repo. Hive-c0re-only. - pub fn agent_applied_dir(name: &str) -> PathBuf { - PathBuf::from(format!("{APPLIED_STATE_ROOT}/{name}")) + crate::paths::agent_state_dir(name).join("harness") } /// Enumerate names that have a persistent state dir under @@ -1500,7 +1474,7 @@ impl Coordinator { /// subtracting `lifecycle::list()`. #[must_use] pub fn kept_state_names() -> Vec { - 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(); }; let mut out: Vec = rd @@ -1524,7 +1498,7 @@ impl Coordinator { .into_iter() .filter(|n| { Self::agent_proposed_dir(n).join(".git").exists() - && !Self::agent_applied_dir(n).join(".git").exists() + && !crate::paths::applied_dir(n).join(".git").exists() }) .collect() } diff --git a/hive-c0re/src/dashboard/approvals.rs b/hive-c0re/src/dashboard/approvals.rs index 60f7be0f..9581b9a2 100644 --- a/hive-c0re/src/dashboard/approvals.rs +++ b/hive-c0re/src/dashboard/approvals.rs @@ -110,7 +110,7 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec) -> Vec String { - let applied = Coordinator::agent_applied_dir(agent); + let applied = crate::paths::applied_dir(agent); if !applied.join(".git").exists() { return format!("(no applied git repo at {})", applied.display()); } @@ -190,7 +190,7 @@ pub(super) async fn get_approval_diff( if !matches!(approval.kind, hive_sh4re::ApprovalKind::ApplyCommit) { return Err(error_problem("spawn approvals carry no commit to diff")); } - let applied = Coordinator::agent_applied_dir(&approval.agent); + let applied = crate::paths::applied_dir(&approval.agent); if !applied.join(".git").exists() { return Ok(plain_text(format!( "(no applied git repo at {})", diff --git a/hive-c0re/src/dashboard/meta_inputs.rs b/hive-c0re/src/dashboard/meta_inputs.rs index b02fc82a..aa8e0fae 100644 --- a/hive-c0re/src/dashboard/meta_inputs.rs +++ b/hive-c0re/src/dashboard/meta_inputs.rs @@ -50,7 +50,7 @@ pub struct MetaInputView { /// tree — every input shown once, at its shallowest path. pub(super) fn read_meta_inputs() -> Vec { 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; }; let Ok(json) = serde_json::from_str::(&raw) else { diff --git a/hive-c0re/src/dashboard/state_files.rs b/hive-c0re/src/dashboard/state_files.rs index 002abde7..08afc1c8 100644 --- a/hive-c0re/src/dashboard/state_files.rs +++ b/hive-c0re/src/dashboard/state_files.rs @@ -13,6 +13,7 @@ use axum::response::{IntoResponse, Response}; use serde::Deserialize; use super::error_response; +use crate::paths::{AGENTS_ROOT, SHARED_ROOT}; #[derive(Deserialize)] pub(super) struct StateFileQuery { @@ -27,8 +28,6 @@ fn resolve_state_path( raw: &str, ) -> std::result::Result<(std::path::PathBuf, std::fs::Metadata), String> { 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 (mapped, root): (std::path::PathBuf, &str) = 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 /// verified set; security rules stay in sync with the read endpoint. pub fn scan_validated_paths(body: &str) -> Vec { - const PREFIXES: [&str; 4] = [ - "/agents/", - "/shared/", - "/var/lib/hyperhive/agents/", - "/var/lib/hyperhive/shared/", - ]; + let agents_slash = format!("{AGENTS_ROOT}/"); + let shared_slash = format!("{SHARED_ROOT}/"); + let prefixes: [&str; 4] = ["/agents/", "/shared/", &agents_slash, &shared_slash]; let mut out = Vec::::new(); for raw in body.split(|c: char| c.is_whitespace()) { // Trim trailing natural-language punctuation that wouldn't @@ -151,7 +147,7 @@ pub fn scan_validated_paths(body: &str) -> Vec { if token.is_empty() { continue; } - if !PREFIXES.iter().any(|p| token.starts_with(p)) { + if !prefixes.iter().any(|p| token.starts_with(p)) { continue; } // Cheap dedupe — typical message has 0-3 refs. diff --git a/hive-c0re/src/dashboard/tombstones.rs b/hive-c0re/src/dashboard/tombstones.rs index 8cd6c408..885f9676 100644 --- a/hive-c0re/src/dashboard/tombstones.rs +++ b/hive-c0re/src/dashboard/tombstones.rs @@ -47,7 +47,7 @@ pub(super) fn build_tombstone_views( .into_iter() .filter(|name| !live.contains(name.as_str())) .map(|name| { - let root = Coordinator::agent_state_root(&name); + let root = crate::paths::agent_state_dir(&name); let state_bytes = dir_size_bytes(&root); let last_seen = std::fs::metadata(&root) .and_then(|m| m.modified()) @@ -133,8 +133,8 @@ pub(super) async fn post_purge_tombstone( } let mut errors = Vec::new(); for dir in [ - Coordinator::agent_state_root(&name), - Coordinator::agent_applied_dir(&name), + crate::paths::agent_state_dir(&name), + crate::paths::applied_dir(&name), ] { if dir.exists() && let Err(e) = std::fs::remove_dir_all(&dir) diff --git a/hive-c0re/src/forge/pr_merge.rs b/hive-c0re/src/forge/pr_merge.rs index 74398dec..ca268140 100644 --- a/hive-c0re/src/forge/pr_merge.rs +++ b/hive-c0re/src/forge/pr_merge.rs @@ -7,8 +7,6 @@ use anyhow::Context; use forgejo_api::ForgejoError; use forgejo_api::structs::{MergePullRequestOption, MergePullRequestOptionDo}; -use crate::coordinator::Coordinator; - use super::{CONFIG_ORG, api, core_token, forge_git_url}; // --------------------------------------------------------------------------- @@ -133,7 +131,7 @@ pub async fn fetch_pr_head_into_applied(repo: &str, pr: u64) -> Result<(), Forge let token = core_token() .ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?; let url = forge_git_url(&token, repo); - let applied = Coordinator::agent_applied_dir(repo_agent_name(repo)); + let applied = crate::paths::applied_dir(repo_agent_name(repo)); let refspec = format!("refs/pull/{pr}/head"); let out = crate::lifecycle::git_command() .current_dir(&applied) @@ -165,7 +163,7 @@ pub async fn ff_push_to_main(repo: &str, sha: &str) -> Result<(), ForgeMergeErro let token = core_token() .ok_or_else(|| ForgeMergeError::Other(anyhow::anyhow!("forge core token absent")))?; let url = forge_git_url(&token, repo); - let applied = Coordinator::agent_applied_dir(repo_agent_name(repo)); + let applied = crate::paths::applied_dir(repo_agent_name(repo)); // Current `main` on the forge repo. let ls = crate::lifecycle::git_command() diff --git a/hive-c0re/src/forge/repos.rs b/hive-c0re/src/forge/repos.rs index f8aa5703..2bc5c186 100644 --- a/hive-c0re/src/forge/repos.rs +++ b/hive-c0re/src/forge/repos.rs @@ -360,7 +360,7 @@ pub async fn push_config(name: &str) -> Result<()> { let Some(token) = core_token() else { return Ok(()); }; - let dir = Coordinator::agent_applied_dir(name); + let dir = crate::paths::applied_dir(name); if !dir.join(".git").exists() { return Ok(()); } diff --git a/hive-c0re/src/forge/users.rs b/hive-c0re/src/forge/users.rs index 4d6c20f2..085f9a1f 100644 --- a/hive-c0re/src/forge/users.rs +++ b/hive-c0re/src/forge/users.rs @@ -15,10 +15,10 @@ use reqwest::StatusCode; use super::{CONFIG_ORG, api, forge_admin, is_present}; const TOKEN_NAME_PREFIX: &str = "hyperhive"; -/// Where the host-side `core` admin token lives. Used by hive-c0re -/// itself to push the meta repo + drive admin API calls (org -/// creation, future webhook setup, etc.). Root-only. -const CORE_TOKEN_PATH: &str = "/var/lib/hyperhive/forge-core-token"; +// Where the host-side `core` admin token lives. Used by hive-c0re itself +// to push the meta repo + drive admin API calls. Root-only. Literal lives +// in `crate::paths`; aliased here under the long-standing name. +use crate::paths::FORGE_CORE_TOKEN as CORE_TOKEN_PATH; // Forge provisioning markers (`forge/core-avatar-set`, // `forge/agent-configs-avatar-set`, `forge/email-aligned-`) live // in `crate::paths` — one-shot guards: the upload/align runs once, the diff --git a/hive-c0re/src/gateway_nginx.rs b/hive-c0re/src/gateway_nginx.rs index 673d70a2..53c96fcc 100644 --- a/hive-c0re/src/gateway_nginx.rs +++ b/hive-c0re/src/gateway_nginx.rs @@ -7,7 +7,6 @@ use anyhow::{Context, Result}; use std::fmt::Write as _; -use std::path::PathBuf; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -31,19 +30,6 @@ static LAST_FAILED_RELOAD: AtomicU64 = AtomicU64::new(0); /// Minimum gap between retry attempts after a reload failure (30 s). 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. -/// The gateway container bind-mounts `/var/lib/hyperhive/gateway/` -/// (not the whole parent dir) at `/run/hive-state/` so nginx inside -/// can read it at `/run/hive-state/agents.conf`. Subdirectory scoping -/// avoids exposing the rest of `/var/lib/hyperhive/` (which may contain -/// forge tokens or other credentials) to the gateway container. -#[must_use] -pub fn host_conf_path() -> PathBuf { - PathBuf::from(HOST_CONF_PATH) -} - /// Nginx proxy headers present in every per-agent location block. /// `$connection_upgrade` is defined in the http context by the NixOS /// nginx module when `recommendedProxySettings = true` (which the @@ -164,7 +150,7 @@ fn render(names: &[String], frontend_dir: Option<&str>) -> String { } /// Atomically write the nginx include file for `names` to -/// [`host_conf_path()`]. Skips the write + reload when the rendered +/// [`crate::paths::gateway_agents_conf()`]. Skips the write + reload when the rendered /// body matches what's already on disk (idempotent; avoids spurious /// gateway reloads on a quiet tick). /// @@ -186,7 +172,7 @@ pub async fn write(names: &[String]) -> Result<()> { .ok() .filter(|s| !s.is_empty()); let body = render(names, frontend_dir.as_deref()); - let path = host_conf_path(); + let path = crate::paths::gateway_agents_conf(); if std::fs::read_to_string(&path).ok().as_deref() == Some(&body) { return Ok(()); } diff --git a/hive-c0re/src/job_queue/exec.rs b/hive-c0re/src/job_queue/exec.rs index 51b35358..66adbe41 100644 --- a/hive-c0re/src/job_queue/exec.rs +++ b/hive-c0re/src/job_queue/exec.rs @@ -100,7 +100,7 @@ async fn run_prebuild( // Prebuild runs while the agent is still up — the runtime dir and // MCP listener already exist. Use the pure path accessor; no need // to re-register the listener (event-driven: registered at start/create). - let agent_dir = Coordinator::agent_dir(name); + let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); crate::lifecycle::prepare_rebuild_dirs(name, &paths).await?; @@ -120,7 +120,7 @@ async fn run_prebuild( } } ctx.step("nix build"); - let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); + let flake_ref = format!("{}#{name}", crate::paths::meta_root().display()); crate::lifecycle::prebuild_toplevel(name, &flake_ref, &|log_id| ctx.build_log(log_id)).await?; Ok(NodeOutput::default()) } @@ -134,7 +134,7 @@ async fn run_swap(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> Res let name = &claim.agent; // Swap runs on an already-existing (stopped) container — runtime dir // and listener were created earlier. Pure path accessor suffices. - let agent_dir = Coordinator::agent_dir(name); + let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); let result = @@ -145,7 +145,7 @@ async fn run_swap(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> Res match &result { Ok(()) => { if let Some(rev) = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) - && let Err(e) = std::fs::write(crate::auto_update::rev_marker_path(name), rev) + && let Err(e) = std::fs::write(crate::paths::applied_rev_marker(name), rev) { tracing::warn!(%name, error = ?e, "write rev marker failed"); } @@ -181,7 +181,7 @@ async fn run_swap(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> Res /// build+create — no prebuild needed). async fn run_create(coord: &Arc, claim: &Claim, ctx: &Ctx<'_>) -> Result { let name = &claim.agent; - let agent_dir = Coordinator::agent_dir(name); + let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); ctx.step("nixos-container create"); @@ -255,7 +255,7 @@ async fn run_reconcile( // exists and writes the nspawn/resource-limits drop-ins. // The returned StartableAgent token is the only way to call // start_with_fallback — omitting this becomes a compile error. - let agent_dir = Coordinator::agent_dir(name); + let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); let token = crate::lifecycle::converge_start_preamble(name, &hive, &paths).await?; @@ -346,7 +346,7 @@ async fn run_write_dropin(coord: &Arc, claim: &Claim) -> Result, agent: &str, source: Source, reason: St /// — the old fast-lane `run_start` upgrade, moved to submit time. pub fn start(coord: &Arc, agent: &str, source: Source, reason: String) -> u64 { set_wanted(coord, agent, Wanted::Up); - let stored = std::fs::read_to_string(crate::auto_update::rev_marker_path(agent)).ok(); + let stored = std::fs::read_to_string(crate::paths::applied_rev_marker(agent)).ok(); let stale = crate::auto_update::current_flake_rev(&coord.hyperhive_flake) .is_some_and(|rev| stored.as_deref() != Some(rev.as_str())); if stale { diff --git a/hive-c0re/src/lifecycle/host_config.rs b/hive-c0re/src/lifecycle/host_config.rs index f7ae69b9..73210e53 100644 --- a/hive-c0re/src/lifecycle/host_config.rs +++ b/hive-c0re/src/lifecycle/host_config.rs @@ -61,49 +61,22 @@ pub const CONTAINER_MANAGER_AGENTS_MOUNT: &str = "/agents"; /// inside the container. 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 /// `binds`, all read-write. The RW on `state` is deliberate (recovery), /// not an oversight; see docs/persistence.md ("Parent access to child /// state") for the rationale. Creates missing host-side directories so /// nspawn doesn't refuse to start; missing dirs are non-fatal. fn bind_child_agent_dirs(child: &str, binds: &mut Vec) { - let state_dir = format!("{HOST_AGENTS_ROOT}/{child}/state"); - let harness_dir = format!("{HOST_AGENTS_ROOT}/{child}/harness"); - let config_dir = format!("{HOST_AGENTS_ROOT}/{child}/config"); - for dir in [&state_dir, &harness_dir, &config_dir] { - let _ = std::fs::create_dir_all(dir); + let child_root = crate::paths::agent_state_dir(child); + for sub in ["state", "harness", "config"] { + let host = child_root.join(sub); + let _ = std::fs::create_dir_all(&host); + 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 @@ -154,8 +127,9 @@ async fn set_nspawn_flags( notes_dir: &Path, ) -> Result<()> { // Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist. - std::fs::create_dir_all(HOST_SHARED_ROOT) - .with_context(|| format!("create {HOST_SHARED_ROOT}"))?; + let shared_root = crate::paths::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 // 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 @@ -169,8 +143,8 @@ async fn set_nspawn_flags( { use std::os::unix::fs::PermissionsExt as _; let perms = std::fs::Permissions::from_mode(0o1777); - std::fs::set_permissions(HOST_SHARED_ROOT, perms) - .with_context(|| format!("chmod 1777 {HOST_SHARED_ROOT}"))?; + std::fs::set_permissions(&shared_root, perms) + .with_context(|| format!("chmod 1777 {}", shared_root.display()))?; } // Ensure /knowledge dir exists. It may be empty until forge seeds it; // nspawn refuses to start if the bind source is missing entirely. @@ -204,7 +178,7 @@ async fn set_nspawn_flags( read_only: false, }, BindMount { - host_path: HOST_SHARED_ROOT.to_owned(), + host_path: shared_root.to_string_lossy().into_owned(), container_path: CONTAINER_SHARED_MOUNT.to_owned(), read_only: false, }, @@ -234,10 +208,11 @@ async fn set_nspawn_flags( read_only: false, }); } - let own_config = format!("{HOST_AGENTS_ROOT}/{agent_name}/config"); - std::fs::create_dir_all(&own_config).with_context(|| format!("create {own_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.display()))?; binds.push(BindMount { - host_path: own_config, + host_path: own_config.to_string_lossy().into_owned(), container_path: format!("/agents/{agent_name}/config"), read_only: true, }); @@ -270,15 +245,16 @@ async fn set_nspawn_flags( // startup migration, but make sure the directory is there // before the role holder comes up in case set_nspawn_flags // fires first (e.g. cold start with no agents). - std::fs::create_dir_all(HOST_META_ROOT) - .with_context(|| format!("create {HOST_META_ROOT}"))?; + let meta_root = crate::paths::meta_root(); + std::fs::create_dir_all(&meta_root) + .with_context(|| format!("create {}", meta_root.display()))?; 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(), read_only: true, }); 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(), read_only: true, }); diff --git a/hive-c0re/src/lifecycle/mod.rs b/hive-c0re/src/lifecycle/mod.rs index 44f109f1..9e2d8936 100644 --- a/hive-c0re/src/lifecycle/mod.rs +++ b/hive-c0re/src/lifecycle/mod.rs @@ -635,7 +635,7 @@ pub async fn rebuild_no_meta( on_build_log_id: &(dyn Fn(i64) + Send + Sync), ) -> Result { prepare_rebuild_dirs(name, paths).await?; - let flake_ref = format!("{}#{name}", crate::meta::meta_dir().display()); + let flake_ref = format!("{}#{name}", crate::paths::meta_root().display()); if container_exists(name).await { // Rebuild strategy: stop-before-update + pre-build. // See `docs/coordinator.md::Container lifecycle`. @@ -864,7 +864,7 @@ pub async fn sync_tmpfiles() { /// # Errors /// Returns an error if `create_dir_all` fails. 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) .with_context(|| format!("create agent runtime dir {}", dir.display())) } diff --git a/hive-c0re/src/lifecycle/setup.rs b/hive-c0re/src/lifecycle/setup.rs index a97898ba..d1f0aaf0 100644 --- a/hive-c0re/src/lifecycle/setup.rs +++ b/hive-c0re/src/lifecycle/setup.rs @@ -9,7 +9,6 @@ use anyhow::{Context, Result, bail}; use super::git::{ 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 /// 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 /// is privileged, so it's delegated to hive-priv. 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() { return Ok(()); } diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 12cbac7a..8efa4fcc 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -21,7 +21,7 @@ use hive_c0re::{ #[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")] struct Cli { /// 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, #[command(subcommand)] diff --git a/hive-c0re/src/matrix.rs b/hive-c0re/src/matrix.rs index bc611661..cbbc7123 100644 --- a/hive-c0re/src/matrix.rs +++ b/hive-c0re/src/matrix.rs @@ -7,7 +7,7 @@ //! the full UIAA round-trip, token-file shape, and host/container //! bind-mount layout. -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use anyhow::{Context, Result}; use reqwest::StatusCode; @@ -22,11 +22,6 @@ const MATRIX_CONTAINER: &str = "hive-matrix"; /// netns so `localhost:` resolves both from the daemon and from /// inside any sub-agent container. 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 ⇒ /// 64-char hex string; comfortable for a long-lived shared secret. const REGISTER_TOKEN_BYTES: usize = 32; @@ -148,8 +143,8 @@ fn random_hex(n: usize) -> Result { /// against the same secret hive-c0re holds. pub fn ensure_register_token() -> Result { use std::os::unix::fs::PermissionsExt; - let path = Path::new(REGISTER_TOKEN_PATH); - if let Ok(existing) = std::fs::read_to_string(path) { + let path = crate::paths::matrix_register_token(); + if let Ok(existing) = std::fs::read_to_string(&path) { let trimmed = existing.trim().to_owned(); if !trimmed.is_empty() { return Ok(trimmed); @@ -159,9 +154,9 @@ pub fn ensure_register_token() -> Result { if let Some(parent) = path.parent() { 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()))?; - 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"); Ok(token) } diff --git a/hive-c0re/src/meta.rs b/hive-c0re/src/meta.rs index ad9034bf..a7d833f3 100644 --- a/hive-c0re/src/meta.rs +++ b/hive-c0re/src/meta.rs @@ -4,7 +4,7 @@ //! `finalize_deploy` / `abort_deploy`, `lock_update_hyperhive`): //! `docs/approvals.md::Meta flake`. -use std::path::{Path, PathBuf}; +use std::path::Path; use anyhow::{Context, Result, bail}; use tokio::process::Command; @@ -13,8 +13,6 @@ use tokio::sync::Mutex; use crate::coordinator::HiveEnv; 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_EMAIL: &str = "c0re@hyperhive.local"; @@ -58,11 +56,6 @@ pub struct AgentSpec { pub port: u16, } -#[must_use] -pub fn meta_dir() -> PathBuf { - PathBuf::from(META_ROOT) -} - /// Idempotently reconcile the meta repo with the current agent set. /// First call inits the git repo, runs `nix flake lock`, and lands a /// seed commit. Subsequent calls only touch `flake.nix` when the @@ -70,7 +63,7 @@ pub fn meta_dir() -> PathBuf { /// no-op. pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); std::fs::create_dir_all(&dir).with_context(|| format!("create {}", dir.display()))?; let new_flake = render_flake( @@ -243,7 +236,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> { /// meta history only carries successful deploys. pub async fn prepare_deploy(name: &str) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let input = format!("agent-{name}"); nix(&dir, &["flake", "update", &input]).await?; // Stage the new lock — git+file://'s dirty-tree fetcher reads @@ -257,7 +250,7 @@ pub async fn prepare_deploy(name: &str) -> Result<()> { /// place (nothing staged → nothing to commit). pub async fn finalize_deploy(name: &str, sha: &str, tag: &str) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); if !paths_dirty(&dir, &["flake.lock"]).await? { return Ok(()); } @@ -275,7 +268,7 @@ pub async fn finalize_deploy(name: &str, sha: &str, tag: &str) -> Result<()> { /// captured in `applied/`'s annotated `failed/` tag. pub async fn abort_deploy() -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); git(&dir, &["restore", "--staged", "flake.lock"]).await?; git(&dir, &["restore", "flake.lock"]).await } @@ -286,7 +279,7 @@ pub async fn abort_deploy() -> Result<()> { /// semantics — it always wants the latest main. pub async fn lock_update_for_rebuild(name: &str) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let input = format!("agent-{name}"); nix(&dir, &["flake", "update", &input]).await?; if !paths_dirty(&dir, &["flake.lock"]).await? { @@ -327,7 +320,7 @@ fn agent_input_override(applied_dir: &Path, sha: &str) -> String { /// actual apply, so this is the right "would this apply" gate. pub async fn verify_commit(name: &str, applied_dir: &Path, sha: &str) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let input = format!("agent-{name}"); let over = agent_input_override(applied_dir, sha); let attr = format!(".#nixosConfigurations.{name}.config.system.build.toplevel.drvPath"); @@ -357,7 +350,7 @@ pub async fn verify_commit(name: &str, applied_dir: &Path, sha: &str) -> Result< /// file) when targeting specific inputs. pub async fn lock_update(inputs: &[String]) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let mut args: Vec<&str> = vec!["flake", "update"]; for i in inputs { args.push(i.as_str()); @@ -382,7 +375,7 @@ pub async fn lock_update(inputs: &[String]) -> Result<()> { /// because the per-agent inputs aren't touched. pub async fn lock_update_hyperhive() -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); nix(&dir, &["flake", "update", "hyperhive"]).await?; if !paths_dirty(&dir, &["flake.lock"]).await? { return Ok(()); @@ -398,7 +391,7 @@ pub async fn lock_update_hyperhive() -> Result<()> { pub async fn commit_tool_groups(agent: &str, groups: &[String]) -> Result<()> { let _guard = META_LOCK.lock().await; crate::tool_groups::set_groups(agent, groups)?; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); if crate::tool_groups::tool_groups_path().exists() { git(&dir, &["add", "tool-groups.json"]).await?; } @@ -419,7 +412,7 @@ pub async fn commit_capabilities(agent: &str, caps: &[String]) -> Result<()> { let _guard = META_LOCK.lock().await; crate::capabilities::set_caps(agent, caps) .map_err(|e| anyhow::anyhow!("set capabilities for {agent}: {e}"))?; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); if crate::capabilities::capabilities_path().exists() { git(&dir, &["add", "capabilities.json"]).await?; } @@ -452,7 +445,7 @@ pub async fn commit_perms( caps: Option<&[String]>, ) -> Result<()> { let _guard = META_LOCK.lock().await; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let mut parts: Vec<&str> = Vec::new(); if let Some(groups) = groups { crate::tool_groups::set_groups(agent, groups)?; @@ -494,7 +487,7 @@ pub async fn commit_topology( ) -> std::result::Result<(), String> { let _guard = META_LOCK.lock().await; crate::topology::set_parent(child, new_parent)?; - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let stage = async { git(&dir, &["add", "topology.json"]).await?; if paths_dirty(&dir, &["topology.json"]).await? { @@ -556,7 +549,7 @@ pub async fn bulk_commit_topology( crate::topology::write(&next).map_err(|e| format!("{e:#}"))?; } // Commit the whole batch as one git operation. - let dir = meta_dir(); + let dir = crate::paths::meta_root(); let commit_msg = if moves.len() == 1 { let (child, new_parent) = moves[0]; format!("topology: {} → {}", child, new_parent.unwrap_or("")) @@ -840,7 +833,7 @@ fn ca_embed_state(dir: &std::path::Path) -> (Vec<(String, String)>, bool) { /// Returns an empty vec when the lock is missing or unparsable — /// safe degradation, the worst case is no dedup for that agent. 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 { return Vec::new(); }; @@ -931,8 +924,9 @@ where for spec in agents { let _ = writeln!( out, - " agent-{}.url = \"git+file://{APPLIED_ROOT}/{}\";", - spec.name, spec.name, + " agent-{}.url = \"git+file://{}\";", + spec.name, + crate::paths::applied_dir(&spec.name).display(), ); // For each canonical input the agent declares in its own // `flake.nix` (detected by reading its applied `flake.lock`), diff --git a/hive-c0re/src/migrate.rs b/hive-c0re/src/migrate.rs index e9b92861..5ed34f51 100644 --- a/hive-c0re/src/migrate.rs +++ b/hive-c0re/src/migrate.rs @@ -4,7 +4,7 @@ //! Kill-switch: `HIVE_SKIP_META_MIGRATION=1`. Full migration sequence //! and phase details: `docs/approvals.md::Migration from the pre-tag`. -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use anyhow::{Context, Result}; @@ -17,18 +17,6 @@ use crate::tool_groups; const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION"; -/// Marker for phase 4. Once present, container repoint is skipped on -/// future restarts. -fn repoint_marker() -> PathBuf { - PathBuf::from("/var/lib/hyperhive/.meta-migration-done") -} - -/// Marker for phase 5. Once present, root→h-root container rename is -/// skipped on future restarts. -fn hroot_rename_marker() -> PathBuf { - PathBuf::from("/var/lib/hyperhive/.hroot-rename-done") -} - /// Substring that identifies the *current* agent flake boilerplate. /// Bumped whenever the template changes so the startup migration /// re-renders existing agents onto the new shape. Today the marker @@ -45,7 +33,7 @@ pub async fn run(coord: &Arc) -> Result<()> { // can leave `.git/index.lock` behind, which blocks every // subsequent meta op until somebody `rm`s it manually. We just // 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() { match std::fs::remove_file(&meta_lock) { Ok(()) => tracing::warn!("cleared stale meta/.git/index.lock"), @@ -83,7 +71,7 @@ pub async fn run(coord: &Arc) -> Result<()> { } // Phase 4: container repoint, guarded by marker. - if repoint_marker().exists() { + if crate::paths::meta_migration_marker().exists() { tracing::debug!("migration: phase 4 marker present, skipping repoint"); return Ok(()); } @@ -104,7 +92,7 @@ pub async fn run(coord: &Arc) -> Result<()> { } if all_ok && !names.is_empty() - && let Err(e) = std::fs::write(repoint_marker(), b"done\n") + && let Err(e) = std::fs::write(crate::paths::meta_migration_marker(), b"done\n") { tracing::warn!(error = ?e, "migration: write repoint marker failed"); } @@ -169,20 +157,20 @@ fn migrate_harness_files(name: &str) { /// conf files present; on the next hive-c0re start the marker is /// absent so the phase retries. async fn rename_manager_container(coord: &Arc) { - if hroot_rename_marker().exists() { + if crate::paths::hroot_rename_marker().exists() { return; } let old_conf = std::path::PathBuf::from("/etc/nixos-containers/root.conf"); let new_conf = std::path::PathBuf::from("/etc/nixos-containers/h-root.conf"); if !old_conf.exists() { // Fresh install — root container was never created under the old name. - let _ = std::fs::write(hroot_rename_marker(), b"done\n"); + let _ = std::fs::write(crate::paths::hroot_rename_marker(), b"done\n"); return; } if new_conf.exists() { // Already renamed (but marker was lost — write it and return). tracing::info!("migration phase 5: h-root.conf already present, marking done"); - let _ = std::fs::write(hroot_rename_marker(), b"done\n"); + let _ = std::fs::write(crate::paths::hroot_rename_marker(), b"done\n"); return; } tracing::info!("migration phase 5: renaming root container to h-root"); @@ -243,7 +231,7 @@ async fn rename_manager_container(coord: &Arc) { } tracing::info!("migration phase 5: root container renamed to h-root"); - let _ = std::fs::write(hroot_rename_marker(), b"done\n"); + let _ = std::fs::write(crate::paths::hroot_rename_marker(), b"done\n"); // Clean up the old conf file so `nixos-container list` doesn't show // a stale stopped `root` entry. Best-effort; a failure here is // harmless — h-root is already running and the marker is written. @@ -267,7 +255,7 @@ async fn enumerate_agents() -> Vec { } async fn migrate_applied_repo(name: &str) -> Result<()> { - let dir = Coordinator::agent_applied_dir(name); + let dir = crate::paths::applied_dir(name); if !dir.join(".git").exists() { return Ok(()); } @@ -312,7 +300,7 @@ async fn migrate_applied_repo(name: &str) -> Result<()> { async fn repoint_container(name: &str) -> Result<()> { let container = lifecycle::container_name(name); - let flake_ref = format!("{}#{name}", meta::meta_dir().display()); + let flake_ref = format!("{}#{name}", crate::paths::meta_root().display()); let out = Command::new("nixos-container") .args(["update", &container, "--flake", &flake_ref]) .output() diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index 70be660a..ee3bb0b0 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -1,15 +1,22 @@ -//! Central host-side state paths under `/var/lib/hyperhive`. +//! 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`, -//! …) 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/`. +//! …). 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 (`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. +//! 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 @@ -18,8 +25,24 @@ 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. +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). @@ -117,6 +140,161 @@ 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/`). 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. +pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents"; + +#[must_use] +pub fn agents_root() -> PathBuf { + PathBuf::from(AGENTS_ROOT) +} + +/// `agents/` — 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//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` (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/` — 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 diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index bf0b3779..f43da1db 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -202,7 +202,7 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { /// registration and notifying the manager on failure. async fn handle_spawn(coord: &Arc, name: &str) -> Result { tracing::info!(%name, "spawn"); - let agent_dir = Coordinator::agent_dir(name); + let agent_dir = crate::paths::agent_runtime_dir(name); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(name, agent_dir); // lifecycle::spawn creates the runtime dir internally before start. diff --git a/hive-c0re/src/socket_server/config_approvals.rs b/hive-c0re/src/socket_server/config_approvals.rs index cabfc994..492322b9 100644 --- a/hive-c0re/src/socket_server/config_approvals.rs +++ b/hive-c0re/src/socket_server/config_approvals.rs @@ -196,7 +196,7 @@ pub(crate) async fn submit_apply_commit( ) -> anyhow::Result<(i64, String)> { validate_commit_ref(commit_ref)?; let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(agent); - let applied_dir = crate::coordinator::Coordinator::agent_applied_dir(agent); + let applied_dir = crate::paths::applied_dir(agent); if !proposed_dir.exists() { anyhow::bail!( "proposed repo missing for agent '{agent}' (expected at {})", diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index af5e6254..e88e6de8 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -99,7 +99,7 @@ pub fn start(agent: &str, socket_path: &Path, coord: Arc) -> Result /// standard per-agent runtime dir + socket, with no dedicated helpers. pub fn start_manager(coord: Arc) -> Result<()> { use std::os::unix::fs::PermissionsExt as _; - let dir = Coordinator::agent_dir(crate::lifecycle::MANAGER_NAME); + let dir = crate::paths::agent_runtime_dir(crate::lifecycle::MANAGER_NAME); std::fs::create_dir_all(&dir) .with_context(|| format!("create manager dir {}", dir.display()))?; let socket = Coordinator::socket_path(crate::lifecycle::MANAGER_NAME); @@ -1026,7 +1026,7 @@ pub(crate) fn handle_send( }; } if resolved != hive_sh4re::OPERATOR_RECIPIENT { - let state_root = crate::coordinator::Coordinator::agent_state_root(&resolved); + let state_root = crate::paths::agent_state_dir(&resolved); if !state_root.exists() { return AgentResponse::Err { message: format!( diff --git a/hive-c0re/src/workers/agent_sockets.rs b/hive-c0re/src/workers/agent_sockets.rs index 6e0dbfda..21910aeb 100644 --- a/hive-c0re/src/workers/agent_sockets.rs +++ b/hive-c0re/src/workers/agent_sockets.rs @@ -17,8 +17,9 @@ use anyhow::{Context, Result}; /// gateway container bind-mounts this whole tree (read-only) so it /// can `proxy_pass` to any agent. Each agent's container bind-mounts /// only its own `/` subdir — agents can only access their own -/// sockets. -pub const AGENT_SOCKET_DIR: &str = "/run/hive-agent"; +/// sockets. The literal lives in [`crate::paths`]; re-exported here +/// 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 /// derives entirely from `(AGENT_SOCKET_DIR, name)` — no second diff --git a/hive-c0re/src/workers/auto_update.rs b/hive-c0re/src/workers/auto_update.rs index 1c7a909c..3c6109ce 100644 --- a/hive-c0re/src/workers/auto_update.rs +++ b/hive-c0re/src/workers/auto_update.rs @@ -16,7 +16,7 @@ //! Booting with no config change performs no meta commit — only //! reconciles. See `docs/coordinator.md::Boot reconcile`. -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use anyhow::Result; @@ -24,14 +24,6 @@ use anyhow::Result; use crate::coordinator::Coordinator; use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME}; -/// Marker file recording the hyperhive rev a sub-agent's container was last -/// built against. Sibling of `applied//` (rather than inside it) to -/// keep it out of the applied repo's git history. Uses a leading dot so a -/// glob over `applied/*` doesn't include it. -pub fn rev_marker_path(name: &str) -> PathBuf { - PathBuf::from(format!("/var/lib/hyperhive/applied/.{name}.hyperhive-rev")) -} - /// Resolve the current rev of `hyperhive_flake`. For a path on disk we /// canonicalize (following symlinks) so a /etc/hyperhive → /nix/store/... /// update yields a different string. For anything else we return None. @@ -58,13 +50,9 @@ pub fn current_flake_rev(hyperhive_flake: &str) -> Option { /// nix-build disk saturation that's long enough that concurrent sweeps /// starved the runtime and stalled the per-agent sockets. 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") - .args([ - "-C", - &format!("/var/lib/hyperhive/applied/{name}"), - "rev-parse", - "HEAD", - ]) + .args(["-C", &applied.to_string_lossy(), "rev-parse", "HEAD"]) .output() .await .ok() @@ -115,7 +103,7 @@ pub async fn ensure_root_agent(coord: &Arc) -> Result<()> { // (no applied flake on disk) we must rebuild — otherwise it's // running whatever the host-declarative config was at create // time, with a wrong systemd unit and port. - let applied_flake = Coordinator::agent_applied_dir(MANAGER_NAME).join("flake.nix"); + let applied_flake = crate::paths::applied_dir(MANAGER_NAME).join("flake.nix"); if !applied_flake.exists() && current_rev.is_some() { tracing::warn!( "manager container exists but no applied flake — forcing rebuild to migrate" @@ -155,7 +143,7 @@ pub async fn ensure_root_agent(coord: &Arc) -> Result<()> { tracing::info!("manager container missing — spawning"); // lifecycle::spawn creates the runtime dir internally; no manual // ensure_agent_runtime_dir needed here. - let runtime = Coordinator::agent_dir(MANAGER_NAME); + let runtime = crate::paths::agent_runtime_dir(MANAGER_NAME); let hive = coord.hive_env(); let paths = Coordinator::agent_paths(MANAGER_NAME, runtime); lifecycle::spawn(MANAGER_NAME, &hive, &paths).await?; @@ -163,7 +151,7 @@ pub async fn ensure_root_agent(coord: &Arc) -> Result<()> { tracing::warn!(error = ?e, "agent_power: set manager wanted=up failed"); } if let Some(rev) = current_rev { - let _ = std::fs::write(rev_marker_path(MANAGER_NAME), &rev); + let _ = std::fs::write(crate::paths::applied_rev_marker(MANAGER_NAME), &rev); } Ok(()) } @@ -252,7 +240,7 @@ pub async fn run(coord: Arc) -> Result<()> { } }; let fresh = current_rev.as_ref().is_some_and(|rev| { - std::fs::read_to_string(rev_marker_path(name)) + std::fs::read_to_string(crate::paths::applied_rev_marker(name)) .is_ok_and(|stored| stored == rev.as_str()) }); if fresh { diff --git a/hive-c0re/src/workers/knowledge.rs b/hive-c0re/src/workers/knowledge.rs index c3647b42..8e535298 100644 --- a/hive-c0re/src/workers/knowledge.rs +++ b/hive-c0re/src/workers/knowledge.rs @@ -23,7 +23,9 @@ pub const REPO: &str = "knowledge"; /// Host-side path for the local clone. Also referenced from /// `lifecycle.rs` (bind-mount source) and the dashboard webhook handler. -pub const LOCAL_DIR: &str = "/var/lib/hyperhive/knowledge"; +/// The literal lives in [`crate::paths`]; re-exported here under the name +/// this module and its consumers have always used. +pub use crate::paths::KNOWLEDGE_DIR as LOCAL_DIR; /// In-container mount point for the knowledge repo. Bind-mounted /// read-only from [`LOCAL_DIR`] into every agent container.