refactor(#2285): repoint all hive-c0re host-path consumers to paths.rs
This commit is contained in:
parent
4162f557c9
commit
187c364feb
16 changed files with 95 additions and 125 deletions
|
|
@ -421,10 +421,11 @@ enum MatrixCmd {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default htpasswd file path — the host-side location of the gateway's
|
// Default htpasswd file path — the host-side location of the gateway's
|
||||||
/// credential store, pre-created by a tmpfiles rule when
|
// credential store, pre-created by a tmpfiles rule when
|
||||||
/// `services.hyperhive.gateway.auth.enable = true`.
|
// `services.hyperhive.gateway.auth.enable = true`. Literal lives in
|
||||||
const DEFAULT_HTPASSWD_FILE: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd";
|
// `hive_c0re::paths`.
|
||||||
|
use hive_c0re::paths::GATEWAY_HTPASSWD as DEFAULT_HTPASSWD_FILE;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum GatewayCmd {
|
enum GatewayCmd {
|
||||||
|
|
@ -530,10 +531,10 @@ enum QuotaCmd {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default host admin socket path. Must match `hive-c0re`'s default in
|
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
|
||||||
/// `main.rs` (`/run/hyperhive/host.sock`) — the daemon binds there and
|
// default via `hive_c0re::paths::HOST_SOCKET` — the daemon binds there
|
||||||
/// `hivectl agents` connects to it.
|
// and `hivectl agents` connects to it.
|
||||||
const DEFAULT_HOST_SOCKET: &str = "/run/hyperhive/host.sock";
|
use hive_c0re::paths::HOST_SOCKET as DEFAULT_HOST_SOCKET;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum AgentsCmd {
|
enum AgentsCmd {
|
||||||
|
|
@ -689,10 +690,12 @@ async fn main() -> Result<()> {
|
||||||
/// core (headless / SSH hosts where no browser opener exists); the open
|
/// core (headless / SSH hosts where no browser opener exists); the open
|
||||||
/// is convenience on top, so a missing/failed `xdg-open` is not an error.
|
/// is convenience on top, so a missing/failed `xdg-open` is not an error.
|
||||||
async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
|
async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> {
|
||||||
let urls = query_hive_urls(socket).await.context(
|
let urls = query_hive_urls(socket).await.with_context(|| {
|
||||||
"could not reach the hive-c0re daemon for URLs — is hive-c0re running? \
|
format!(
|
||||||
(the socket is at /run/hyperhive/host.sock)",
|
"could not reach the hive-c0re daemon for URLs — is hive-c0re running? \
|
||||||
)?;
|
(the socket is at {DEFAULT_HOST_SOCKET})"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let (url, hint) = match target {
|
let (url, hint) = match target {
|
||||||
OpenTarget::Home => (
|
OpenTarget::Home => (
|
||||||
urls.home,
|
urls.home,
|
||||||
|
|
@ -1067,7 +1070,10 @@ fn agent_exists(name: &str) -> Result<bool> {
|
||||||
/// container.
|
/// container.
|
||||||
fn choom(name: &str, resume_session: Option<&str>) -> Result<()> {
|
fn choom(name: &str, resume_session: Option<&str>) -> Result<()> {
|
||||||
if !agent_exists(name)? {
|
if !agent_exists(name)? {
|
||||||
bail!("no such agent: '{name}' (no state dir under /var/lib/hyperhive/agents/)");
|
bail!(
|
||||||
|
"no such agent: '{name}' (no state dir under {}/)",
|
||||||
|
hive_c0re::paths::AGENTS_ROOT
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let container = hive_c0re::lifecycle::container_name(name);
|
let container = hive_c0re::lifecycle::container_name(name);
|
||||||
// Enter as the agent's unix user (== agent name) so claude reads the
|
// Enter as the agent's unix user (== agent name) so claude reads the
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,7 @@ pub fn hive_swarm_names() -> (Option<String>, Option<String>) {
|
||||||
/// render the `deployed:<sha12>` chip per container row.
|
/// render the `deployed:<sha12>` chip per container row.
|
||||||
fn read_meta_locked_revs() -> HashMap<String, String> {
|
fn read_meta_locked_revs() -> HashMap<String, String> {
|
||||||
let mut out = HashMap::new();
|
let mut out = HashMap::new();
|
||||||
let Ok(raw) = std::fs::read_to_string("/var/lib/hyperhive/meta/flake.lock") else {
|
let Ok(raw) = std::fs::read_to_string(crate::paths::meta_flake_lock()) else {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,6 @@ const DASHBOARD_CHANNEL: usize = 256;
|
||||||
/// `Coordinator::set_last_stopped_running`.
|
/// `Coordinator::set_last_stopped_running`.
|
||||||
const LAST_STOPPED_RUNNING_KEY: &str = "last_stopped_running";
|
const LAST_STOPPED_RUNNING_KEY: &str = "last_stopped_running";
|
||||||
|
|
||||||
const AGENT_RUNTIME_ROOT: &str = "/run/hyperhive/agents";
|
|
||||||
/// Manager-editable per-agent config repos. Bind-mounted RW into the manager
|
|
||||||
/// container as `/agents/<name>/`. Hive-c0re only writes to these on first
|
|
||||||
/// spawn (initial commit); after that it's manager-only.
|
|
||||||
const AGENT_STATE_ROOT: &str = "/var/lib/hyperhive/agents";
|
|
||||||
/// Hive-c0re-only authoritative per-agent config repos. Containers build from
|
|
||||||
/// these. Manager has no filesystem access; the only way to update is via
|
|
||||||
/// `request_apply_commit` + user approval.
|
|
||||||
const APPLIED_STATE_ROOT: &str = "/var/lib/hyperhive/applied";
|
|
||||||
|
|
||||||
pub struct Coordinator {
|
pub struct Coordinator {
|
||||||
pub broker: Arc<Broker>,
|
pub broker: Arc<Broker>,
|
||||||
pub approvals: Arc<Approvals>,
|
pub approvals: Arc<Approvals>,
|
||||||
|
|
@ -1443,7 +1433,7 @@ impl Coordinator {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn agent_dir(name: &str) -> PathBuf {
|
pub fn agent_dir(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("{AGENT_RUNTIME_ROOT}/{name}"))
|
crate::paths::agent_runtime_dir(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn socket_path(name: &str) -> PathBuf {
|
pub fn socket_path(name: &str) -> PathBuf {
|
||||||
|
|
@ -1454,7 +1444,7 @@ impl Coordinator {
|
||||||
///
|
///
|
||||||
/// Per-agent state root (parent of `config/`, future `prompts/`, etc.).
|
/// Per-agent state root (parent of `config/`, future `prompts/`, etc.).
|
||||||
pub fn agent_state_root(name: &str) -> PathBuf {
|
pub fn agent_state_root(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("{AGENT_STATE_ROOT}/{name}"))
|
crate::paths::agent_state_dir(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manager-editable proposed config repo. Bind-mounted into the manager
|
/// Manager-editable proposed config repo. Bind-mounted into the manager
|
||||||
|
|
@ -1490,7 +1480,7 @@ impl Coordinator {
|
||||||
|
|
||||||
/// Authoritative applied config repo. Hive-c0re-only.
|
/// Authoritative applied config repo. Hive-c0re-only.
|
||||||
pub fn agent_applied_dir(name: &str) -> PathBuf {
|
pub fn agent_applied_dir(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("{APPLIED_STATE_ROOT}/{name}"))
|
crate::paths::applied_dir(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enumerate names that have a persistent state dir under
|
/// Enumerate names that have a persistent state dir under
|
||||||
|
|
@ -1500,7 +1490,7 @@ impl Coordinator {
|
||||||
/// subtracting `lifecycle::list()`.
|
/// subtracting `lifecycle::list()`.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn kept_state_names() -> Vec<String> {
|
pub fn kept_state_names() -> Vec<String> {
|
||||||
let Ok(rd) = std::fs::read_dir(AGENT_STATE_ROOT) else {
|
let Ok(rd) = std::fs::read_dir(crate::paths::agents_root()) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
let mut out: Vec<String> = rd
|
let mut out: Vec<String> = rd
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,7 @@ pub struct MetaInputView {
|
||||||
/// tree — every input shown once, at its shallowest path.
|
/// tree — every input shown once, at its shallowest path.
|
||||||
pub(super) fn read_meta_inputs() -> Vec<MetaInputView> {
|
pub(super) fn read_meta_inputs() -> Vec<MetaInputView> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
let Ok(raw) = std::fs::read_to_string("/var/lib/hyperhive/meta/flake.lock") else {
|
let Ok(raw) = std::fs::read_to_string(crate::paths::meta_flake_lock()) else {
|
||||||
return out;
|
return out;
|
||||||
};
|
};
|
||||||
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ use axum::response::{IntoResponse, Response};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::error_response;
|
use super::error_response;
|
||||||
|
use crate::paths::{AGENTS_ROOT, SHARED_ROOT};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(super) struct StateFileQuery {
|
pub(super) struct StateFileQuery {
|
||||||
|
|
@ -27,8 +28,6 @@ fn resolve_state_path(
|
||||||
raw: &str,
|
raw: &str,
|
||||||
) -> std::result::Result<(std::path::PathBuf, std::fs::Metadata), String> {
|
) -> std::result::Result<(std::path::PathBuf, std::fs::Metadata), String> {
|
||||||
use std::os::unix::fs::PermissionsExt as _;
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
|
||||||
const SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
|
|
||||||
let raw = raw.trim();
|
let raw = raw.trim();
|
||||||
let (mapped, root): (std::path::PathBuf, &str) =
|
let (mapped, root): (std::path::PathBuf, &str) =
|
||||||
if let Some(rest) = raw.strip_prefix("/agents/") {
|
if let Some(rest) = raw.strip_prefix("/agents/") {
|
||||||
|
|
@ -136,12 +135,9 @@ fn reject_symlinks_below(
|
||||||
/// broker-message ingest so the dashboard event already carries the
|
/// broker-message ingest so the dashboard event already carries the
|
||||||
/// verified set; security rules stay in sync with the read endpoint.
|
/// verified set; security rules stay in sync with the read endpoint.
|
||||||
pub fn scan_validated_paths(body: &str) -> Vec<String> {
|
pub fn scan_validated_paths(body: &str) -> Vec<String> {
|
||||||
const PREFIXES: [&str; 4] = [
|
let agents_slash = format!("{AGENTS_ROOT}/");
|
||||||
"/agents/",
|
let shared_slash = format!("{SHARED_ROOT}/");
|
||||||
"/shared/",
|
let prefixes: [&str; 4] = ["/agents/", "/shared/", &agents_slash, &shared_slash];
|
||||||
"/var/lib/hyperhive/agents/",
|
|
||||||
"/var/lib/hyperhive/shared/",
|
|
||||||
];
|
|
||||||
let mut out = Vec::<String>::new();
|
let mut out = Vec::<String>::new();
|
||||||
for raw in body.split(|c: char| c.is_whitespace()) {
|
for raw in body.split(|c: char| c.is_whitespace()) {
|
||||||
// Trim trailing natural-language punctuation that wouldn't
|
// Trim trailing natural-language punctuation that wouldn't
|
||||||
|
|
@ -151,7 +147,7 @@ pub fn scan_validated_paths(body: &str) -> Vec<String> {
|
||||||
if token.is_empty() {
|
if token.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if !PREFIXES.iter().any(|p| token.starts_with(p)) {
|
if !prefixes.iter().any(|p| token.starts_with(p)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Cheap dedupe — typical message has 0-3 refs.
|
// Cheap dedupe — typical message has 0-3 refs.
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,6 @@ static LAST_FAILED_RELOAD: AtomicU64 = AtomicU64::new(0);
|
||||||
/// Minimum gap between retry attempts after a reload failure (30 s).
|
/// Minimum gap between retry attempts after a reload failure (30 s).
|
||||||
const RELOAD_RETRY_SECS: u64 = 30;
|
const RELOAD_RETRY_SECS: u64 = 30;
|
||||||
|
|
||||||
const HOST_CONF_PATH: &str = "/var/lib/hyperhive/gateway/agents.conf";
|
|
||||||
|
|
||||||
/// Host-side path where c0re writes the generated nginx include file.
|
/// Host-side path where c0re writes the generated nginx include file.
|
||||||
/// The gateway container bind-mounts `/var/lib/hyperhive/gateway/`
|
/// The gateway container bind-mounts `/var/lib/hyperhive/gateway/`
|
||||||
/// (not the whole parent dir) at `/run/hive-state/` so nginx inside
|
/// (not the whole parent dir) at `/run/hive-state/` so nginx inside
|
||||||
|
|
@ -41,7 +39,7 @@ const HOST_CONF_PATH: &str = "/var/lib/hyperhive/gateway/agents.conf";
|
||||||
/// forge tokens or other credentials) to the gateway container.
|
/// forge tokens or other credentials) to the gateway container.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn host_conf_path() -> PathBuf {
|
pub fn host_conf_path() -> PathBuf {
|
||||||
PathBuf::from(HOST_CONF_PATH)
|
crate::paths::gateway_agents_conf()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Nginx proxy headers present in every per-agent location block.
|
/// Nginx proxy headers present in every per-agent location block.
|
||||||
|
|
|
||||||
|
|
@ -61,49 +61,22 @@ pub const CONTAINER_MANAGER_AGENTS_MOUNT: &str = "/agents";
|
||||||
/// inside the container.
|
/// inside the container.
|
||||||
pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied";
|
pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied";
|
||||||
|
|
||||||
/// The on-host root that gets bind-mounted to `/agents` inside the manager.
|
|
||||||
/// Hard-coded to match `AGENT_STATE_ROOT` in coordinator.rs (kept duplicated
|
|
||||||
/// here so lifecycle stays usable as a leaf module).
|
|
||||||
pub(super) const HOST_AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
|
||||||
|
|
||||||
/// On-host applied repo root, mirrored RO into the manager. Matches
|
|
||||||
/// `APPLIED_STATE_ROOT` in coordinator.rs.
|
|
||||||
const HOST_APPLIED_ROOT: &str = "/var/lib/hyperhive/applied";
|
|
||||||
|
|
||||||
/// On-host meta repo root, mirrored RO into the manager. Matches
|
|
||||||
/// `meta::meta_dir()` but duplicated here so lifecycle stays a leaf.
|
|
||||||
const HOST_META_ROOT: &str = "/var/lib/hyperhive/meta";
|
|
||||||
|
|
||||||
/// Shared directory accessible to all agents. All agents bind-mount this RW.
|
|
||||||
const HOST_SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
|
|
||||||
|
|
||||||
/// Append bind flags for `child`'s state, harness, and config dirs into
|
/// Append bind flags for `child`'s state, harness, and config dirs into
|
||||||
/// `binds`, all read-write. The RW on `state` is deliberate (recovery),
|
/// `binds`, all read-write. The RW on `state` is deliberate (recovery),
|
||||||
/// not an oversight; see docs/persistence.md ("Parent access to child
|
/// not an oversight; see docs/persistence.md ("Parent access to child
|
||||||
/// state") for the rationale. Creates missing host-side directories so
|
/// state") for the rationale. Creates missing host-side directories so
|
||||||
/// nspawn doesn't refuse to start; missing dirs are non-fatal.
|
/// nspawn doesn't refuse to start; missing dirs are non-fatal.
|
||||||
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
|
||||||
let state_dir = format!("{HOST_AGENTS_ROOT}/{child}/state");
|
let child_root = crate::paths::agent_state_dir(child);
|
||||||
let harness_dir = format!("{HOST_AGENTS_ROOT}/{child}/harness");
|
for sub in ["state", "harness", "config"] {
|
||||||
let config_dir = format!("{HOST_AGENTS_ROOT}/{child}/config");
|
let host = child_root.join(sub);
|
||||||
for dir in [&state_dir, &harness_dir, &config_dir] {
|
let _ = std::fs::create_dir_all(&host);
|
||||||
let _ = std::fs::create_dir_all(dir);
|
binds.push(BindMount {
|
||||||
|
host_path: host.to_string_lossy().into_owned(),
|
||||||
|
container_path: format!("/agents/{child}/{sub}"),
|
||||||
|
read_only: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
binds.push(BindMount {
|
|
||||||
host_path: state_dir,
|
|
||||||
container_path: format!("/agents/{child}/state"),
|
|
||||||
read_only: false,
|
|
||||||
});
|
|
||||||
binds.push(BindMount {
|
|
||||||
host_path: harness_dir,
|
|
||||||
container_path: format!("/agents/{child}/harness"),
|
|
||||||
read_only: false,
|
|
||||||
});
|
|
||||||
binds.push(BindMount {
|
|
||||||
host_path: config_dir,
|
|
||||||
container_path: format!("/agents/{child}/config"),
|
|
||||||
read_only: false,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hive-wide secrets forwarded into every agent container via nspawn
|
/// Hive-wide secrets forwarded into every agent container via nspawn
|
||||||
|
|
@ -154,8 +127,9 @@ async fn set_nspawn_flags(
|
||||||
notes_dir: &Path,
|
notes_dir: &Path,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
// Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist.
|
// Ensure /shared directory exists before binding. systemd-nspawn requires the bind source to exist.
|
||||||
std::fs::create_dir_all(HOST_SHARED_ROOT)
|
let shared_root = crate::paths::shared_root();
|
||||||
.with_context(|| format!("create {HOST_SHARED_ROOT}"))?;
|
std::fs::create_dir_all(&shared_root)
|
||||||
|
.with_context(|| format!("create {}", shared_root.display()))?;
|
||||||
// Make /shared writable by every agent. Containers share host uids (no
|
// Make /shared writable by every agent. Containers share host uids (no
|
||||||
// PrivateUsers), but each agent is a distinct unix user, so a root-owned
|
// PrivateUsers), but each agent is a distinct unix user, so a root-owned
|
||||||
// 0755 dir leaves them unable to write — the documented "read/write for
|
// 0755 dir leaves them unable to write — the documented "read/write for
|
||||||
|
|
@ -169,8 +143,8 @@ async fn set_nspawn_flags(
|
||||||
{
|
{
|
||||||
use std::os::unix::fs::PermissionsExt as _;
|
use std::os::unix::fs::PermissionsExt as _;
|
||||||
let perms = std::fs::Permissions::from_mode(0o1777);
|
let perms = std::fs::Permissions::from_mode(0o1777);
|
||||||
std::fs::set_permissions(HOST_SHARED_ROOT, perms)
|
std::fs::set_permissions(&shared_root, perms)
|
||||||
.with_context(|| format!("chmod 1777 {HOST_SHARED_ROOT}"))?;
|
.with_context(|| format!("chmod 1777 {}", shared_root.display()))?;
|
||||||
}
|
}
|
||||||
// Ensure /knowledge dir exists. It may be empty until forge seeds it;
|
// Ensure /knowledge dir exists. It may be empty until forge seeds it;
|
||||||
// nspawn refuses to start if the bind source is missing entirely.
|
// nspawn refuses to start if the bind source is missing entirely.
|
||||||
|
|
@ -204,7 +178,7 @@ async fn set_nspawn_flags(
|
||||||
read_only: false,
|
read_only: false,
|
||||||
},
|
},
|
||||||
BindMount {
|
BindMount {
|
||||||
host_path: HOST_SHARED_ROOT.to_owned(),
|
host_path: shared_root.to_string_lossy().into_owned(),
|
||||||
container_path: CONTAINER_SHARED_MOUNT.to_owned(),
|
container_path: CONTAINER_SHARED_MOUNT.to_owned(),
|
||||||
read_only: false,
|
read_only: false,
|
||||||
},
|
},
|
||||||
|
|
@ -234,10 +208,11 @@ async fn set_nspawn_flags(
|
||||||
read_only: false,
|
read_only: false,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
let own_config = format!("{HOST_AGENTS_ROOT}/{agent_name}/config");
|
let own_config = crate::paths::agent_state_dir(agent_name).join("config");
|
||||||
std::fs::create_dir_all(&own_config).with_context(|| format!("create {own_config}"))?;
|
std::fs::create_dir_all(&own_config)
|
||||||
|
.with_context(|| format!("create {}", own_config.display()))?;
|
||||||
binds.push(BindMount {
|
binds.push(BindMount {
|
||||||
host_path: own_config,
|
host_path: own_config.to_string_lossy().into_owned(),
|
||||||
container_path: format!("/agents/{agent_name}/config"),
|
container_path: format!("/agents/{agent_name}/config"),
|
||||||
read_only: true,
|
read_only: true,
|
||||||
});
|
});
|
||||||
|
|
@ -270,15 +245,16 @@ async fn set_nspawn_flags(
|
||||||
// startup migration, but make sure the directory is there
|
// startup migration, but make sure the directory is there
|
||||||
// before the role holder comes up in case set_nspawn_flags
|
// before the role holder comes up in case set_nspawn_flags
|
||||||
// fires first (e.g. cold start with no agents).
|
// fires first (e.g. cold start with no agents).
|
||||||
std::fs::create_dir_all(HOST_META_ROOT)
|
let meta_root = crate::paths::meta_root();
|
||||||
.with_context(|| format!("create {HOST_META_ROOT}"))?;
|
std::fs::create_dir_all(&meta_root)
|
||||||
|
.with_context(|| format!("create {}", meta_root.display()))?;
|
||||||
binds.push(BindMount {
|
binds.push(BindMount {
|
||||||
host_path: HOST_APPLIED_ROOT.to_owned(),
|
host_path: crate::paths::applied_root().to_string_lossy().into_owned(),
|
||||||
container_path: CONTAINER_MANAGER_APPLIED_MOUNT.to_owned(),
|
container_path: CONTAINER_MANAGER_APPLIED_MOUNT.to_owned(),
|
||||||
read_only: true,
|
read_only: true,
|
||||||
});
|
});
|
||||||
binds.push(BindMount {
|
binds.push(BindMount {
|
||||||
host_path: HOST_META_ROOT.to_owned(),
|
host_path: meta_root.to_string_lossy().into_owned(),
|
||||||
container_path: crate::meta::CONTAINER_MANAGER_META_MOUNT.to_owned(),
|
container_path: crate::meta::CONTAINER_MANAGER_META_MOUNT.to_owned(),
|
||||||
read_only: true,
|
read_only: true,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -864,7 +864,7 @@ pub async fn sync_tmpfiles() {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// Returns an error if `create_dir_all` fails.
|
/// Returns an error if `create_dir_all` fails.
|
||||||
pub fn ensure_agent_runtime_dir(name: &str) -> Result<()> {
|
pub fn ensure_agent_runtime_dir(name: &str) -> Result<()> {
|
||||||
let dir = std::path::PathBuf::from(format!("/run/hyperhive/agents/{name}"));
|
let dir = crate::paths::agent_runtime_dir(name);
|
||||||
std::fs::create_dir_all(&dir)
|
std::fs::create_dir_all(&dir)
|
||||||
.with_context(|| format!("create agent runtime dir {}", dir.display()))
|
.with_context(|| format!("create agent runtime dir {}", dir.display()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ use anyhow::{Context, Result, bail};
|
||||||
use super::git::{
|
use super::git::{
|
||||||
git, git_command, git_commit, git_read_tree_reset, git_rev_parse, git_root_commit, git_tag,
|
git, git_command, git_commit, git_read_tree_reset, git_rev_parse, git_root_commit, git_tag,
|
||||||
};
|
};
|
||||||
use super::host_config::HOST_AGENTS_ROOT;
|
|
||||||
|
|
||||||
/// Initialize the manager-editable proposed repo. Seeds two tracked
|
/// Initialize the manager-editable proposed repo. Seeds two tracked
|
||||||
/// files: `agent.nix` (the module the manager edits) and `flake.nix`
|
/// files: `agent.nix` (the module the manager edits) and `flake.nix`
|
||||||
|
|
@ -217,7 +216,7 @@ pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
|
||||||
/// brand-new agent on a btrfs host gets a real subvolume. Subvolume creation
|
/// brand-new agent on a btrfs host gets a real subvolume. Subvolume creation
|
||||||
/// is privileged, so it's delegated to hive-priv.
|
/// is privileged, so it's delegated to hive-priv.
|
||||||
pub async fn ensure_agent_state_subvolume(name: &str) -> Result<()> {
|
pub async fn ensure_agent_state_subvolume(name: &str) -> Result<()> {
|
||||||
let root = Path::new(HOST_AGENTS_ROOT).join(name);
|
let root = crate::paths::agent_state_dir(name);
|
||||||
if root.exists() {
|
if root.exists() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ use hive_c0re::{
|
||||||
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")]
|
#[command(name = "hive-c0re", about = "hyperhive coordinator daemon and CLI")]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
/// Path to the host admin socket.
|
/// Path to the host admin socket.
|
||||||
#[arg(long, global = true, default_value = "/run/hyperhive/host.sock")]
|
#[arg(long, global = true, default_value = hive_c0re::paths::HOST_SOCKET)]
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
//! the full UIAA round-trip, token-file shape, and host/container
|
//! the full UIAA round-trip, token-file shape, and host/container
|
||||||
//! bind-mount layout.
|
//! bind-mount layout.
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
@ -22,11 +22,6 @@ const MATRIX_CONTAINER: &str = "hive-matrix";
|
||||||
/// netns so `localhost:<port>` resolves both from the daemon and from
|
/// netns so `localhost:<port>` resolves both from the daemon and from
|
||||||
/// inside any sub-agent container.
|
/// inside any sub-agent container.
|
||||||
const MATRIX_HTTP: &str = "http://localhost:8008";
|
const MATRIX_HTTP: &str = "http://localhost:8008";
|
||||||
/// Host path of the matrix registration token. Must match
|
|
||||||
/// `hyperhive.matrix.registrationTokenFile` in `nix/modules/hive-matrix.nix`
|
|
||||||
/// (same path is bind-mounted read-only into the tuwunel container so
|
|
||||||
/// the homeserver can read it via `registration_token_file`).
|
|
||||||
const REGISTER_TOKEN_PATH: &str = "/var/lib/hyperhive/matrix-register-token";
|
|
||||||
/// Length (bytes) of the random registration token. 32 raw bytes ⇒
|
/// Length (bytes) of the random registration token. 32 raw bytes ⇒
|
||||||
/// 64-char hex string; comfortable for a long-lived shared secret.
|
/// 64-char hex string; comfortable for a long-lived shared secret.
|
||||||
const REGISTER_TOKEN_BYTES: usize = 32;
|
const REGISTER_TOKEN_BYTES: usize = 32;
|
||||||
|
|
@ -148,8 +143,8 @@ fn random_hex(n: usize) -> Result<String> {
|
||||||
/// against the same secret hive-c0re holds.
|
/// against the same secret hive-c0re holds.
|
||||||
pub fn ensure_register_token() -> Result<String> {
|
pub fn ensure_register_token() -> Result<String> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let path = Path::new(REGISTER_TOKEN_PATH);
|
let path = crate::paths::matrix_register_token();
|
||||||
if let Ok(existing) = std::fs::read_to_string(path) {
|
if let Ok(existing) = std::fs::read_to_string(&path) {
|
||||||
let trimmed = existing.trim().to_owned();
|
let trimmed = existing.trim().to_owned();
|
||||||
if !trimmed.is_empty() {
|
if !trimmed.is_empty() {
|
||||||
return Ok(trimmed);
|
return Ok(trimmed);
|
||||||
|
|
@ -159,9 +154,9 @@ pub fn ensure_register_token() -> Result<String> {
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).ok();
|
std::fs::create_dir_all(parent).ok();
|
||||||
}
|
}
|
||||||
std::fs::write(path, format!("{token}\n"))
|
std::fs::write(&path, format!("{token}\n"))
|
||||||
.with_context(|| format!("write registration token to {}", path.display()))?;
|
.with_context(|| format!("write registration token to {}", path.display()))?;
|
||||||
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
|
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
||||||
tracing::info!(path = %path.display(), "matrix: generated registration token");
|
tracing::info!(path = %path.display(), "matrix: generated registration token");
|
||||||
Ok(token)
|
Ok(token)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,6 @@ use tokio::sync::Mutex;
|
||||||
use crate::coordinator::HiveEnv;
|
use crate::coordinator::HiveEnv;
|
||||||
use crate::lifecycle;
|
use crate::lifecycle;
|
||||||
|
|
||||||
const META_ROOT: &str = "/var/lib/hyperhive/meta";
|
|
||||||
const APPLIED_ROOT: &str = "/var/lib/hyperhive/applied";
|
|
||||||
const GIT_NAME: &str = "c0re";
|
const GIT_NAME: &str = "c0re";
|
||||||
const GIT_EMAIL: &str = "c0re@hyperhive.local";
|
const GIT_EMAIL: &str = "c0re@hyperhive.local";
|
||||||
|
|
||||||
|
|
@ -60,7 +58,7 @@ pub struct AgentSpec {
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn meta_dir() -> PathBuf {
|
pub fn meta_dir() -> PathBuf {
|
||||||
PathBuf::from(META_ROOT)
|
crate::paths::meta_root()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Idempotently reconcile the meta repo with the current agent set.
|
/// Idempotently reconcile the meta repo with the current agent set.
|
||||||
|
|
@ -840,7 +838,7 @@ fn ca_embed_state(dir: &std::path::Path) -> (Vec<(String, String)>, bool) {
|
||||||
/// Returns an empty vec when the lock is missing or unparsable —
|
/// Returns an empty vec when the lock is missing or unparsable —
|
||||||
/// safe degradation, the worst case is no dedup for that agent.
|
/// safe degradation, the worst case is no dedup for that agent.
|
||||||
fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
||||||
let path = std::path::PathBuf::from(format!("{APPLIED_ROOT}/{name}/flake.lock"));
|
let path = crate::paths::applied_dir(name).join("flake.lock");
|
||||||
let Ok(raw) = std::fs::read_to_string(&path) else {
|
let Ok(raw) = std::fs::read_to_string(&path) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|
@ -931,8 +929,9 @@ where
|
||||||
for spec in agents {
|
for spec in agents {
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
" agent-{}.url = \"git+file://{APPLIED_ROOT}/{}\";",
|
" agent-{}.url = \"git+file://{}\";",
|
||||||
spec.name, spec.name,
|
spec.name,
|
||||||
|
crate::paths::applied_dir(&spec.name).display(),
|
||||||
);
|
);
|
||||||
// For each canonical input the agent declares in its own
|
// For each canonical input the agent declares in its own
|
||||||
// `flake.nix` (detected by reading its applied `flake.lock`),
|
// `flake.nix` (detected by reading its applied `flake.lock`),
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,13 @@ const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
|
||||||
/// Marker for phase 4. Once present, container repoint is skipped on
|
/// Marker for phase 4. Once present, container repoint is skipped on
|
||||||
/// future restarts.
|
/// future restarts.
|
||||||
fn repoint_marker() -> PathBuf {
|
fn repoint_marker() -> PathBuf {
|
||||||
PathBuf::from("/var/lib/hyperhive/.meta-migration-done")
|
crate::paths::meta_migration_marker()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker for phase 5. Once present, root→h-root container rename is
|
/// Marker for phase 5. Once present, root→h-root container rename is
|
||||||
/// skipped on future restarts.
|
/// skipped on future restarts.
|
||||||
fn hroot_rename_marker() -> PathBuf {
|
fn hroot_rename_marker() -> PathBuf {
|
||||||
PathBuf::from("/var/lib/hyperhive/.hroot-rename-done")
|
crate::paths::hroot_rename_marker()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Substring that identifies the *current* agent flake boilerplate.
|
/// Substring that identifies the *current* agent flake boilerplate.
|
||||||
|
|
@ -45,7 +45,7 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
|
||||||
// can leave `.git/index.lock` behind, which blocks every
|
// can leave `.git/index.lock` behind, which blocks every
|
||||||
// subsequent meta op until somebody `rm`s it manually. We just
|
// subsequent meta op until somebody `rm`s it manually. We just
|
||||||
// booted so nothing of ours is holding it; safe to clear.
|
// booted so nothing of ours is holding it; safe to clear.
|
||||||
let meta_lock = std::path::PathBuf::from("/var/lib/hyperhive/meta/.git/index.lock");
|
let meta_lock = crate::paths::meta_git_index_lock();
|
||||||
if meta_lock.exists() {
|
if meta_lock.exists() {
|
||||||
match std::fs::remove_file(&meta_lock) {
|
match std::fs::remove_file(&meta_lock) {
|
||||||
Ok(()) => tracing::warn!("cleared stale meta/.git/index.lock"),
|
Ok(()) => tracing::warn!("cleared stale meta/.git/index.lock"),
|
||||||
|
|
|
||||||
|
|
@ -148,11 +148,15 @@ pub fn agent_sockets_file() -> PathBuf {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// `agents/` — per-agent persistent state root (one subdir per agent,
|
/// `agents/` — per-agent persistent state root (one subdir per agent,
|
||||||
/// bind-mounted into each container as `/agents/<name>`).
|
/// bind-mounted into each container as `/agents/<name>`). A `&str` (the
|
||||||
|
/// dashboard state-file allow-list uses it for `strip_prefix` /
|
||||||
|
/// `starts_with` checks), so it stays a const; [`agents_root`] wraps it.
|
||||||
// nix: agent container bind-mount source (harness-base.nix / agent-base.nix) — must match.
|
// nix: agent container bind-mount source (harness-base.nix / agent-base.nix) — must match.
|
||||||
|
pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn agents_root() -> PathBuf {
|
pub fn agents_root() -> PathBuf {
|
||||||
state_root().join("agents")
|
PathBuf::from(AGENTS_ROOT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `agents/<name>` — one agent's persistent state root.
|
/// `agents/<name>` — one agent's persistent state root.
|
||||||
|
|
@ -202,11 +206,15 @@ pub fn meta_git_index_lock() -> PathBuf {
|
||||||
meta_root().join(".git/index.lock")
|
meta_root().join(".git/index.lock")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `shared/` — the cross-agent `/shared` scratch space.
|
/// `shared/` — the cross-agent `/shared` scratch space. A `&str` (the
|
||||||
|
/// dashboard state-file allow-list uses it for prefix checks), so it
|
||||||
|
/// stays a const; [`shared_root`] wraps it.
|
||||||
// nix: bind-mounted into every agent container as `/shared` (harness-base.nix) — must match.
|
// nix: bind-mounted into every agent container as `/shared` (harness-base.nix) — must match.
|
||||||
|
pub const SHARED_ROOT: &str = "/var/lib/hyperhive/shared";
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn shared_root() -> PathBuf {
|
pub fn shared_root() -> PathBuf {
|
||||||
state_root().join("shared")
|
PathBuf::from(SHARED_ROOT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `knowledge/` — local checkout of the `internal/knowledge` repo. A
|
/// `knowledge/` — local checkout of the `internal/knowledge` repo. A
|
||||||
|
|
@ -228,6 +236,12 @@ pub fn gateway_agents_conf() -> PathBuf {
|
||||||
gateway_dir().join("agents.conf")
|
gateway_dir().join("agents.conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `gateway/gateway.htpasswd` — nginx basic-auth credential store for the
|
||||||
|
/// operator dashboard vhost. A `&str` (used as a `hivectl` clap
|
||||||
|
/// `default_value`), so it stays a const rather than a `PathBuf` fn.
|
||||||
|
// nix: read by the gateway container's nginx (hive-gateway.nix) — must match.
|
||||||
|
pub const GATEWAY_HTPASSWD: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd";
|
||||||
|
|
||||||
/// `forge-core-token` — the hive-c0re forge account API token. A `&str`
|
/// `forge-core-token` — the hive-c0re forge account API token. A `&str`
|
||||||
/// (used in `Path::new` + user-facing `format!` messages), so it stays a
|
/// (used in `Path::new` + user-facing `format!` messages), so it stays a
|
||||||
/// const rather than a `PathBuf` fn.
|
/// const rather than a `PathBuf` fn.
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ use anyhow::{Context, Result};
|
||||||
/// gateway container bind-mounts this whole tree (read-only) so it
|
/// gateway container bind-mounts this whole tree (read-only) so it
|
||||||
/// can `proxy_pass` to any agent. Each agent's container bind-mounts
|
/// can `proxy_pass` to any agent. Each agent's container bind-mounts
|
||||||
/// only its own `<name>/` subdir — agents can only access their own
|
/// only its own `<name>/` subdir — agents can only access their own
|
||||||
/// sockets.
|
/// sockets. The literal lives in [`crate::paths`]; re-exported here
|
||||||
pub const AGENT_SOCKET_DIR: &str = "/run/hive-agent";
|
/// under the name this module's consumers have always used.
|
||||||
|
pub use crate::paths::AGENT_SOCKET_DIR;
|
||||||
|
|
||||||
/// Socket filename inside each per-agent subdir. Fixed so the path
|
/// Socket filename inside each per-agent subdir. Fixed so the path
|
||||||
/// derives entirely from `(AGENT_SOCKET_DIR, name)` — no second
|
/// derives entirely from `(AGENT_SOCKET_DIR, name)` — no second
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_NAME};
|
||||||
/// keep it out of the applied repo's git history. Uses a leading dot so a
|
/// keep it out of the applied repo's git history. Uses a leading dot so a
|
||||||
/// glob over `applied/*` doesn't include it.
|
/// glob over `applied/*` doesn't include it.
|
||||||
pub fn rev_marker_path(name: &str) -> PathBuf {
|
pub fn rev_marker_path(name: &str) -> PathBuf {
|
||||||
PathBuf::from(format!("/var/lib/hyperhive/applied/.{name}.hyperhive-rev"))
|
crate::paths::applied_rev_marker(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the current rev of `hyperhive_flake`. For a path on disk we
|
/// Resolve the current rev of `hyperhive_flake`. For a path on disk we
|
||||||
|
|
@ -58,13 +58,9 @@ pub fn current_flake_rev(hyperhive_flake: &str) -> Option<String> {
|
||||||
/// nix-build disk saturation that's long enough that concurrent sweeps
|
/// nix-build disk saturation that's long enough that concurrent sweeps
|
||||||
/// starved the runtime and stalled the per-agent sockets.
|
/// starved the runtime and stalled the per-agent sockets.
|
||||||
pub async fn agent_config_pending(name: &str, deployed_sha: Option<&str>) -> bool {
|
pub async fn agent_config_pending(name: &str, deployed_sha: Option<&str>) -> bool {
|
||||||
|
let applied = crate::paths::applied_dir(name);
|
||||||
let applied_head = tokio::process::Command::new("git")
|
let applied_head = tokio::process::Command::new("git")
|
||||||
.args([
|
.args(["-C", &applied.to_string_lossy(), "rev-parse", "HEAD"])
|
||||||
"-C",
|
|
||||||
&format!("/var/lib/hyperhive/applied/{name}"),
|
|
||||||
"rev-parse",
|
|
||||||
"HEAD",
|
|
||||||
])
|
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue