Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
556a213320 | ||
|
|
cfb84b420a | ||
|
|
187c364feb | ||
|
|
4162f557c9 | ||
|
|
1cefaf2c16 |
31 changed files with 335 additions and 254 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {})",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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:<port>` 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<String> {
|
|||
/// against the same secret hive-c0re holds.
|
||||
pub fn ensure_register_token() -> Result<String> {
|
||||
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<String> {
|
|||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/<n>`'s annotated `failed/<id>` 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("<root>"))
|
||||
|
|
@ -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`),
|
||||
|
|
|
|||
|
|
@ -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<Coordinator>) -> 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<Coordinator>) -> 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<Coordinator>) -> 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<Coordinator>) {
|
||||
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<Coordinator>) {
|
|||
}
|
||||
|
||||
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<String> {
|
|||
}
|
||||
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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/<name>`). A `&str` (the
|
||||
/// dashboard state-file allow-list uses it for `strip_prefix` /
|
||||
/// `starts_with` checks), so it stays a const; [`agents_root`] wraps it.
|
||||
// nix: agent container bind-mount source (harness-base.nix / agent-base.nix) — must match.
|
||||
pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
||||
|
||||
#[must_use]
|
||||
pub fn agents_root() -> PathBuf {
|
||||
PathBuf::from(AGENTS_ROOT)
|
||||
}
|
||||
|
||||
/// `agents/<name>` — 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/<name>/config`.
|
||||
// nix: read by hive-c0re only, but paired with `agents/` in the deploy flow.
|
||||
#[must_use]
|
||||
pub fn applied_root() -> PathBuf {
|
||||
state_root().join("applied")
|
||||
}
|
||||
|
||||
/// `applied/<name>` — one agent's applied config working tree.
|
||||
#[must_use]
|
||||
pub fn applied_dir(name: &str) -> PathBuf {
|
||||
applied_root().join(name)
|
||||
}
|
||||
|
||||
/// `applied/.<name>.hyperhive-rev` — marker recording the flake rev an
|
||||
/// agent was last successfully rebuilt against (auto-update staleness check).
|
||||
#[must_use]
|
||||
pub fn applied_rev_marker(name: &str) -> PathBuf {
|
||||
applied_root().join(format!(".{name}.hyperhive-rev"))
|
||||
}
|
||||
|
||||
/// `meta/` — the meta flake working tree (inputs, `flake.lock`, `.git`).
|
||||
// nix: bind-mounted read-only into agent containers as `/meta` (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/<name>` — 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
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
|||
/// registration and notifying the manager on failure.
|
||||
async fn handle_spawn(coord: &Arc<Coordinator>, name: &str) -> Result<HostResponse> {
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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 {})",
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ pub fn start(agent: &str, socket_path: &Path, coord: Arc<Coordinator>) -> Result
|
|||
/// standard per-agent runtime dir + socket, with no dedicated helpers.
|
||||
pub fn start_manager(coord: Arc<Coordinator>) -> 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!(
|
||||
|
|
|
|||
|
|
@ -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 `<name>/` 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
|
||||
|
|
|
|||
|
|
@ -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/<name>/` (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<String> {
|
|||
/// 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<Coordinator>) -> 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<Coordinator>) -> 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<Coordinator>) -> 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<Coordinator>) -> 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 {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue