Watch
0
0
Fork
You've already forked hyperhive
0

Compare commits

...
31 changed files with 335 additions and 254 deletions

View file

@ -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<Coordinator>, 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<Coordinator>, 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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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<bool> {
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<bool> {
/// 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

View file

@ -244,7 +244,7 @@ pub fn hive_swarm_names() -> (Option<String>, Option<String>) {
/// render the `deployed:<sha12>` chip per container row.
fn read_meta_locked_revs() -> HashMap<String, String> {
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::<serde_json::Value>(&raw) else {

View file

@ -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/<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 broker: Arc<Broker>,
pub approvals: Arc<Approvals>,
@ -547,7 +537,7 @@ impl Coordinator {
/// Assemble the per-agent filesystem paths for `name`. `agent_dir`
/// is the runtime directory (`/run/hyperhive/agents/<name>`), 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/<name>/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<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();
};
let mut out: Vec<String> = 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()
}

View file

@ -110,7 +110,7 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
/// dashboard event (instead of forcing the dashboard to wait a
/// `/api/state` cycle to see the diff for newly-queued approvals).
pub(crate) async fn approval_diff(agent: &str, approval_id: i64) -> 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 {})",

View file

@ -50,7 +50,7 @@ pub struct MetaInputView {
/// tree — every input shown once, at its shallowest path.
pub(super) fn read_meta_inputs() -> Vec<MetaInputView> {
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::<serde_json::Value>(&raw) else {

View file

@ -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<String> {
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::<String>::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<String> {
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.

View file

@ -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)

View file

@ -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()

View file

@ -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(());
}

View file

@ -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-<name>`) live
// in `crate::paths` — one-shot guards: the upload/align runs once, the

View file

@ -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(());
}

View file

@ -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<Coordinator>, 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<Coordinator>, 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<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Res
/// build+create — no prebuild needed).
async fn run_create(coord: &Arc<Coordinator>, claim: &Claim, ctx: &Ctx<'_>) -> Result<NodeOutput> {
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<Coordinator>, claim: &Claim) -> Result<Nod
// write_dropins only needs the path value to build AgentPaths; the
// dir doesn't need to exist at this point (created by ensure_agent_runtime_dir
// on the upstream Prebuild/Start node).
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::write_dropins(name, &hive, &paths).await?;

View file

@ -49,7 +49,7 @@ pub fn restart(coord: &Arc<Coordinator>, agent: &str, source: Source, reason: St
/// — the old fast-lane `run_start` upgrade, moved to submit time.
pub fn start(coord: &Arc<Coordinator>, 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 {

View file

@ -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<BindMount>) {
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,
});

View file

@ -635,7 +635,7 @@ pub async fn rebuild_no_meta(
on_build_log_id: &(dyn Fn(i64) + Send + Sync),
) -> Result<bool> {
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()))
}

View file

@ -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(());
}

Some files were not shown because too many files have changed in this diff Show more