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

View file

@ -51,7 +51,7 @@ fn insert_named(
}
fn rebuild(builder: &JobBuilder, agent: &str) -> Vec<hive_jobq::NodeGuid> {
templates::rebuild(builder, agent, true)
templates::rebuild(builder, agent)
}
/// Restart shape with every agent treated as **running** — the online
@ -468,7 +468,7 @@ fn rebuild_chain_is_declared_serial() {
fn graceful_rebuild_chain_drains_before_stopping() {
let q = JobQueue::new(1);
insert(&q, |builder| {
templates::graceful_rebuild_nodes(builder, "agent-a", true, None);
templates::graceful_rebuild_nodes(builder, "agent-a", None);
});
// Asserted as full rows, not just kinds: the kind list is identical whether
// the quiesce chain runs beside the build or nested under it, so a
@ -515,8 +515,8 @@ fn graceful_rebuild_chain_drains_before_stopping() {
}
/// The non-graceful shape is the default everywhere except the boot sweep:
/// a manual rebuild, a meta-update cascade child and a deploy must NOT spend a
/// drain window, so `StopForUpdate` waits only on `Prebuild`.
/// a manual rebuild and a deploy must NOT spend a drain window, so
/// `StopForUpdate` waits only on `Prebuild`.
#[test]
fn non_graceful_rebuild_has_no_signal_or_drain() {
// Read the shape off the queue rather than out of a node list: a declared
@ -524,7 +524,7 @@ fn non_graceful_rebuild_has_no_signal_or_drain() {
// observable where it matters — in what the scheduler runs.
let q = JobQueue::new(1);
insert(&q, |builder| {
templates::rebuild_nodes(builder, "agent-a", true, None);
templates::rebuild_nodes(builder, "agent-a", None);
});
assert_eq!(
declared_shape(&q)
@ -576,10 +576,10 @@ fn rebuild_chain_declares_its_resources_on_the_brace() {
res("reconcile"),
],
[
// The meta preamble takes the global window and *nothing else* —
// no slot (it does no nix work) and no lease. It stays a sibling
// root so that hive-global window is not held across any build.
vec![Resource::MetaWindow],
// The preamble declares nothing — no slot (it does no nix work) and
// no lease. It stays a sibling root so nothing it holds is held
// across a build.
vec![],
// The brace takes both, atomically, and holds them for its whole
// subtree. Hoisting the slot here is not a widening: it already
// spanned the entire rebuild when `Prebuild` held it, because a unit
@ -782,44 +782,43 @@ fn boot_sweep_nodes_declare_their_own_resources() {
// nothing in this module covered it. When resource derivation moved to the
// construction sites, this path was missed and both kinds silently declared
// nothing — dropping the agent lease a boot `Reconcile` needs to not race
// another DAG's container ops, and letting the sweep `MetaLock` land its
// meta commit inside another node's staged deploy window. Nothing failed to
// compile; only an exhaustive caller list would have caught it.
// another DAG's container ops. Nothing failed to compile; only an
// exhaustive caller list would have caught it.
let q = JobQueue::new(4);
insert(&q, |builder| {
crate::workers::auto_update::boot_nodes(
builder,
true,
vec!["stale-agent".to_owned()],
&["stale-agent".to_owned()],
vec!["drifted-agent".to_owned()],
);
});
let mut lock = declared_resources(&q, node_of(&q, "meta_lock"));
lock.sort_by_key(|r| format!("{r:?}"));
let mut window = declared_resources(&q, node_of(&q, "agent_window"));
window.sort_by_key(|r| format!("{r:?}"));
assert_eq!(
lock,
vec![Resource::BuildSlot, Resource::MetaWindow],
"the sweep MetaLock runs a nix lock bump and commits to meta"
window,
vec![
Resource::Agent("stale-agent".to_owned()),
Resource::BuildSlot
],
"a swept rebuild holds the agent's lease and a build slot"
);
// Two `reconcile` nodes exist: the swept agent's recovery tail and the
// drifted agent's own.
assert_eq!(
declared_resources(&q, node_of(&q, "reconcile")),
vec![Resource::Agent("drifted-agent".to_owned())],
declared_resources_of_kind(&q, "reconcile"),
vec![
vec![Resource::Agent("drifted-agent".to_owned())],
vec![Resource::Agent("stale-agent".to_owned())],
],
"a boot Reconcile touches the container, so it holds that agent's lease"
);
}
/// Crash-watch suppression for a cascade rebuild, which the deleted half of
/// `meta_update_grows_cascade_in_dag` used to assert via a DAG-level
/// `transient` field on the submit-time spec (both the field and the spec type
/// are gone).
///
/// The property is unchanged — a container going down under a rebuild must not
/// read as a crash — but it is no longer a DAG-level declaration: each node
/// answers for itself, so the assertion moves to the nodes a cascade actually
/// runs. Kept as its own test rather than dropped, because it is the *property*
/// that mattered, not the field that used to carry it.
/// Crash-watch suppression for a rebuild: a container going down under a
/// rebuild must not read as a crash. Each node answers for itself, so the
/// assertion is on the nodes a rebuild actually runs.
#[test]
fn rebuild_chain_nodes_suppress_crash_watch() {
for kind in [
@ -990,26 +989,15 @@ fn a_fanned_out_mechanical_node_declares_its_agent_lease() {
);
}
/// A running `MetaLock` grows one rebuild subgraph per agent into **its own
/// DAG**, rooted on itself — not as child DAGs. That is what keeps a boot sweep
/// (or a meta-update cascade) one unit of work, with every rebuild building
/// against the lock the emitter just bumped.
///
/// Replaces `grown_subgraph_roots_on_emitter_and_rebases_local_deps` and
/// `meta_update_grows_cascade_in_dag`, which differed only in which rebuild
/// flavour they grew and each minted a builder by hand to simulate the graft.
/// What they were checking is the `grown_*_rebuilds` templates, so this calls
/// one — the boot sweep's, since that is the caller that grows a graceful one.
///
/// That the grafted work lands under the emitter, and that the emitter parks in
/// `Finishing` until it settles, is `hive_jobq`'s
/// (`a_completing_node_grows_the_work_it_declared`).
/// The boot sweep declares one rebuild subgraph per stale agent into **one
/// DAG**, each an independent root — not as child DAGs. That keeps a boot sweep
/// one unit of work the operator can watch and cancel.
#[test]
fn a_meta_lock_grows_one_rebuild_subgraph_per_agent() {
fn the_boot_sweep_declares_one_rebuild_subgraph_per_stale_agent() {
let q = JobQueue::new(4);
let agents = vec!["alice".to_owned(), "bob".to_owned()];
insert(&q, |builder| {
templates::grown_graceful_rebuilds(builder, &agents, true);
crate::workers::auto_update::boot_nodes(builder, &agents, Vec::new());
});
// One chain per agent, each an independent group root — so the two rebuild