Watch
0
0
Fork
You've already forked hyperhive
0

build agents only from internal/meta; delete the hive's local meta flake

The hive no longer renders, locks or commits a meta flake. A rebuild's
MetaSync node picks the internal/meta commit and records it with the
hyperhive and config pins it holds; Prebuild, Swap and Create build from it,
and a head that does not pin the agent's current config fails the build
naming the agent. hive-priv's meta_rev is required.

Removed: meta.rs sync/lock/render, the manual-rebuild relock, the boot
sweep's hyperhive bump (MetaLock), the update-meta-inputs form and its
panel, MetaWindow, the hive's hyperhive_rev setting.

Kept: agent_set::refresh holds what sync_agents also did (topology.json, the
gateway's agent-socket map and nginx include), called from spawn, destroy,
rebuild and the migration.

The boot reconcile and the stale-start upgrade compare the agent's marker
with the hyperhive pin of internal/meta; the dashboard's deployed chip reads
the config marker a successful build writes.
This commit is contained in:
atlas 2026-10-25 13:33:15 +01:00
commit 4aa11b3a94
4 changed files with 1042 additions and 3793 deletions

View file

@ -187,44 +187,6 @@ pub async fn git_rev_parse(dir: &Path, refname: &str) -> Result<String> {
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
}
/// The contents of `path` at commit `rev` in `dir`, `None` when `rev` has no
/// such file. Reads the object database, not the working tree.
pub async fn git_show_file(dir: &Path, rev: &str, path: &str) -> Result<Option<String>> {
let spec = format!("{rev}:{path}");
let listed = run_with_deadline(
git_command()
.current_dir(dir)
.args(["ls-tree", "--name-only", rev, "--", path]),
GIT_LOCAL_DEADLINE,
&format!("git ls-tree {spec} in {}", dir.display()),
)
.await?;
if !listed.status.success() {
bail!(
"git ls-tree {spec} failed ({}): {}",
listed.status,
String::from_utf8_lossy(&listed.stderr).trim()
);
}
if listed.stdout.iter().all(u8::is_ascii_whitespace) {
return Ok(None);
}
let shown = run_with_deadline(
git_command().current_dir(dir).args(["show", &spec]),
GIT_LOCAL_DEADLINE,
&format!("git show {spec} in {}", dir.display()),
)
.await?;
if !shown.status.success() {
bail!(
"git show {spec} failed ({}): {}",
shown.status,
String::from_utf8_lossy(&shown.stderr).trim()
);
}
Ok(Some(String::from_utf8_lossy(&shown.stdout).into_owned()))
}
/// Plant a lightweight tag at `target`. Errors if the tag already
/// exists — we want loud failures on id reuse, not silent
/// overwrites.
@ -241,9 +203,10 @@ pub async fn git_read_tree_reset(dir: &Path, target: &str) -> Result<()> {
git(dir, &["read-tree", "--reset", "-u", target]).await
}
/// Hard-set a ref to `target`. Used to fast-forward `refs/heads/main`
/// to the just-deployed proposal commit. Uses `update-ref`, not
/// `branch -f`, so it works regardless of where HEAD currently sits.
/// Hard-set a ref to `target` with `update-ref`, so it works regardless of
/// where HEAD currently sits. Test fixtures only: production moves refs with
/// [`git_update_ref_cas`].
#[cfg(test)]
pub async fn git_update_ref(dir: &Path, refname: &str, target: &str) -> Result<()> {
git(dir, &["update-ref", refname, target]).await
}
@ -253,7 +216,7 @@ pub async fn git_update_ref(dir: &Path, refname: &str, target: &str) -> Result<(
/// untouched — when the current value differs. That refusal is the whole
/// difference between "advance this branch" and "overwrite whatever is there".
///
/// Prefer this over [`git_update_ref`] whenever the caller already knows the
/// Prefer this over a plain `update-ref` whenever the caller already knows the
/// value it believes the ref holds: a plain `update-ref` that raced another
/// writer discards the other writer's commits without a word.
///

View file

@ -529,8 +529,8 @@ async fn set_nspawn_flags(
}
}
// systemd-nspawn refuses to start a container whose bind
// source doesn't exist. The meta repo is created by the
// startup migration, but make sure the directory is there
// source doesn't exist. The directory is created by the
// startup migration, but make sure it is there
// before the role holder comes up in case set_nspawn_flags
// fires first (e.g. cold start with no agents).
let meta_root = crate::paths::meta_root();

View file

@ -7,10 +7,12 @@ mod setup;
#[cfg(test)]
mod tests;
#[cfg(test)]
pub use git::git_update_ref;
pub use git::{
FETCH_DEADLINE, GIT_LOCAL_DEADLINE, git, git_authed, git_command, git_command_authed,
git_is_ancestor, git_read_tree_reset, git_rev_parse, git_show_file, git_tag, git_update_ref,
git_update_ref_cas, run_with_deadline,
git_is_ancestor, git_read_tree_reset, git_rev_parse, git_tag, git_update_ref_cas,
run_with_deadline,
};
pub use host_config::write_dropins;
pub use setup::{
@ -207,10 +209,10 @@ async fn port_collision(self_name: &str) -> Result<Option<String>> {
/// First-spawn pre-create provisioning — the job queue's `Provision`
/// node. Fail fast on a port collision, set up the proposed/applied
/// repos + state subvolume + claude/notes dirs, then register the new
/// agent in the meta flake (`sync_agents`) so the later
/// `nixos-container create --flake meta#<name>` ref resolves. Does NOT
/// agent in the host-side files that follow the agent set
/// ([`crate::agent_set::refresh`]). Does NOT
/// create the container — that's `create_only` / the `Create` node.
pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
pub async fn provision_container(name: &str, paths: &AgentPaths) -> Result<()> {
validate(name)?;
if let Some(other) = port_collision(name).await? {
bail!(
@ -219,7 +221,7 @@ pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths)
);
}
// Read before anything is seeded: an archetype agent gets no config
// repo, and the meta render below builds it from the archetype.
// repo, and the build takes it from the archetype.
crate::archetypes::refresh(name).await?;
if crate::archetypes::of(name)?.is_none() {
setup_proposed(&paths.proposed, name).await?;
@ -228,19 +230,15 @@ pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths)
ensure_agent_state_subvolume(name).await?;
ensure_claude_dir(&paths.claude)?;
ensure_state_dir(&paths.notes)?;
// Meta flake gets the new agent's input + nixosConfiguration
// before `nixos-container create` so the `--flake meta#<name>`
// ref resolves.
let agents = agents_after_spawn(name).await?;
crate::meta::sync_agents(hive, &agents).await
crate::agent_set::refresh(&agents).await
}
/// The `nixos-container create` proper — the job queue's `Create` node.
/// Assumes `provision_container` already registered the agent in meta.
/// `meta_rev` is the `internal/meta` commit to build from; none builds from the
/// hive's own meta flake (`crate::meta_rev`).
pub async fn create_only(name: &str, meta_rev: Option<&str>) -> Result<()> {
priv_run_inner("create", name, meta_rev, None).await
/// Assumes `provision_container` already registered the agent.
/// `meta_rev` is the `internal/meta` commit to build from (`crate::meta_rev`).
pub async fn create_only(name: &str, meta_rev: &str) -> Result<()> {
priv_run_inner("create", name, Some(meta_rev), None).await
}
/// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on
@ -298,15 +296,15 @@ pub async fn swap_update(
name: &str,
hive: &HiveEnv,
paths: &AgentPaths,
meta_rev: Option<&str>,
meta_rev: &str,
node_id: Option<u64>,
) -> Result<()> {
write_dropins(name, hive, paths).await?;
priv_run_inner("update", name, meta_rev, node_id).await
priv_run_inner("update", name, Some(meta_rev), node_id).await
}
/// Build the `AgentSpec` list for the meta flake from `nixos-container
/// list` + a hypothetical extra name not yet in the list (for spawn
/// Build the `AgentSpec` list for [`crate::agent_set::refresh`] from
/// `nixos-container list` + a hypothetical extra name not yet in the list (for spawn
/// where the new agent's container doesn't exist yet). Pass empty
/// `name_to_add` from rebuild paths where the agent is already in the
/// container list.
@ -314,17 +312,17 @@ pub async fn swap_update(
/// Propagates errors from `list()` rather than swallowing them.
/// Using `.unwrap_or_default()` here would silently produce an empty
/// agent list when `nixos-container list` fails (priv helper down, race),
/// which `sync_agents` would then commit to meta — dropping every agent
/// from `flake.nix`. Callers that can tolerate failures (e.g. migration)
/// which would drop every agent from the topology and the gateway. Callers
/// that can tolerate failures (e.g. migration)
/// handle the `Err` themselves with `.unwrap_or_default()`.
async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::AgentSpec>> {
async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<hive_meta_flake::AgentSpec>> {
let containers = list().await?;
let archetypes = crate::archetypes::read_at(&crate::archetypes::path())?;
let mut out: Vec<crate::meta::AgentSpec> = containers
let mut out: Vec<hive_meta_flake::AgentSpec> = containers
.into_iter()
.filter_map(|c| {
let name = c.strip_prefix(AGENT_PREFIX)?.to_owned();
Some(crate::meta::AgentSpec {
Some(hive_meta_flake::AgentSpec {
port: agent_web_port(&name),
archetype: archetypes.get(&name).cloned(),
pin: None,
@ -335,7 +333,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::A
if let Some(extra) = name_to_add
&& !out.iter().any(|a| a.name == extra)
{
out.push(crate::meta::AgentSpec {
out.push(hive_meta_flake::AgentSpec {
port: agent_web_port(extra),
archetype: archetypes.get(extra).cloned(),
pin: None,
@ -346,7 +344,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::A
Ok(out)
}
async fn agents_after_spawn(name: &str) -> Result<Vec<crate::meta::AgentSpec>> {
async fn agents_after_spawn(name: &str) -> Result<Vec<hive_meta_flake::AgentSpec>> {
agents_for_meta(Some(name)).await
}
@ -354,7 +352,7 @@ async fn agents_after_spawn(name: &str) -> Result<Vec<crate::meta::AgentSpec>> {
/// `nixos-container list` says), sorted, no extras. For callers
/// outside this module that need to reseed meta after lifecycle
/// changes — destroy, startup reconciliation, etc.
pub async fn agents_for_meta_listing() -> Result<Vec<crate::meta::AgentSpec>> {
pub async fn agents_for_meta_listing() -> Result<Vec<hive_meta_flake::AgentSpec>> {
agents_for_meta(None).await
}
@ -970,6 +968,13 @@ fn make_log_callback(
}
}
/// A `create` or `update` asked for without an `internal/meta` commit. The
/// public entry points take the commit, so this guards the string dispatch
/// below only.
fn missing_meta_rev(kind: &str, name: &str) -> anyhow::Error {
anyhow::anyhow!("{name}: {kind} needs an internal/meta commit to build from")
}
/// Execute a container operation via hive-priv and integrate with
/// `build_logs.sqlite`. hive-priv runs as root and forwards output lines
/// to hive-c0re in real time via the streaming priv protocol. Each line
@ -1007,22 +1012,28 @@ async fn priv_run_inner(
// For long-running ops use the streaming protocol so build_logs
// receives lines in real time rather than as a batch at completion.
let result: Result<()> = match kind {
"create" => {
crate::priv_client::create_container_streaming(
name,
meta_rev,
make_log_callback(logs.clone(), log_id, cmdline.clone()),
)
.await
}
"update" => {
crate::priv_client::update_container_streaming(
name,
meta_rev,
make_log_callback(logs.clone(), log_id, cmdline.clone()),
)
.await
}
"create" => match meta_rev {
Some(rev) => {
crate::priv_client::create_container_streaming(
name,
rev,
make_log_callback(logs.clone(), log_id, cmdline.clone()),
)
.await
}
None => Err(missing_meta_rev(kind, name)),
},
"update" => match meta_rev {
Some(rev) => {
crate::priv_client::update_container_streaming(
name,
rev,
make_log_callback(logs.clone(), log_id, cmdline.clone()),
)
.await
}
None => Err(missing_meta_rev(kind, name)),
},
"start" => crate::priv_client::start_container(name).await,
"stop" => crate::priv_client::stop_container(name).await,
"kill" => crate::priv_client::kill_container(name).await,

View file

@ -260,8 +260,8 @@ fn initial_agent_nix(name: &str) -> String {
}
/// Module-only flake exposed by every agent's repo. Consumed by the
/// hive-c0re-owned meta flake at `/var/lib/hyperhive/meta/` as a flake
/// input. The wrapper is intentionally permissive:
/// swarm's `internal/meta` flake as a flake input. The wrapper is
/// intentionally permissive:
///
/// - Manager edits `inputs.* = …` to add other flakes (e.g. an MCP
/// server's own flake) — the lock for those lands in the agent's