topology: drop the parent field and the hierarchy it fed
`topology.json` was a map of `name -> parent | null`, and that value fed the whole agent hierarchy: `<parent>` / `<children>` recipient sentinels, the reparenting API (CLI verb, wire verb, dashboard endpoints, DAG node), the dashboard tree, the rebuild depth sort, and an unconditional bind-mount grant giving every agent RW on its direct children's state. Per the operator's ruling the field goes, and with it all of the above. The file survives as what remains once the value is gone: the roster of agent names, which is the set `ManageRootAgent` grants mounts over. It is now a JSON array; `read` still accepts the old map shape and keeps its keys, so a hive that upgrades across this does not blank its roster (and so no capability holder loses its mounts for the length of that window). Two sites kept their behaviour under a different recipient rather than losing it. Both addressed `<parent>`, which the broker already resolved to `operator` for a root agent, and every agent is now what that fallback called a root: - the harness's turn-failure / plugin-failure notification (`Surface::send_to_parent` -> `send_to_operator`), and - the send allow-list's always-permitted escape hatch, so an agent with a restrictive allow-list still has a way to say it is stuck. What is NOT preserved, deliberately: an agent with no capability no longer sees any other agent's dirs. `ManageRootAgent`'s own grant is unchanged -- still every agent in the roster, still state RW + config RO, still no `harness`. The dashboard's reparenting control (the M0V3 picker) is deleted with its CSS. The tree rendering that reads `ContainerView.parent` is left for the frontend owner -- it degrades to a flat list with the field gone.
This commit is contained in:
parent
392f16cbc0
commit
d94bc2188d
28 changed files with 236 additions and 1513 deletions
|
|
@ -60,7 +60,7 @@ pub(super) async fn run_node(
|
|||
kind: &NodeKind,
|
||||
) -> (super::JobBuilder, Result<()>) {
|
||||
// The agent this node targets rides the payload — empty for the agentless
|
||||
// kinds (`MetaLock`, `Reparent`), which never read it.
|
||||
// kinds (`MetaLock`), which never read it.
|
||||
let agent = kind.agent();
|
||||
// Every arm is `Result<()>`; the three that grow work declare into `builder`
|
||||
// *synchronously*, after their own awaits have finished. Borrowing `&builder`
|
||||
|
|
@ -120,7 +120,6 @@ pub(super) async fn run_node(
|
|||
// takes it directly instead of re-matching the kind behind a `bail!`
|
||||
// that could never fire.
|
||||
NodeKind::WritePermFile { payload, .. } => run_write_perm_file(coord, agent, payload).await,
|
||||
NodeKind::Reparent { moves } => run_reparent(coord, moves).await,
|
||||
NodeKind::MergeVerify { approval_id, .. } => {
|
||||
run_merge_verify(coord, *approval_id, id).await
|
||||
}
|
||||
|
|
@ -815,33 +814,6 @@ async fn run_write_perm_file(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply the node's `(child, new_parent)` moves as one `META_LOCK`-fused
|
||||
/// commit (`Coordinator::reparent_bulk_with_notify`, which already handles
|
||||
/// both the single- and bulk-move case, sends the per-agent move
|
||||
/// notifications, and rescans + diff-emits the container tree). Runs under
|
||||
/// the deploy window (it declares `Resource::MetaWindow`), same reasoning as
|
||||
/// `run_write_perm_file`: a topology commit landing inside another node's
|
||||
/// staged deploy window would sweep the staged lock into its commit.
|
||||
async fn run_reparent(
|
||||
coord: &Arc<Coordinator>,
|
||||
moves: &[(hive_types::Ident, Option<hive_types::Ident>)],
|
||||
) -> Result<()> {
|
||||
let refs: Vec<(&str, Option<&str>)> = moves
|
||||
.iter()
|
||||
.map(|(child, parent)| {
|
||||
(
|
||||
child.as_str(),
|
||||
parent.as_ref().map(hive_types::Ident::as_str),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
coord
|
||||
.reparent_bulk_with_notify(&refs)
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deploy phase 1 — drift gate, fetch, eval-verify. Mutates nothing, so a
|
||||
/// failure here cancel-cascades the rest of the subtree with the forge and the
|
||||
/// applied repo exactly as they were.
|
||||
|
|
@ -890,7 +862,9 @@ async fn run_deploy_tail(
|
|||
/// flake affects — the fan-out set for `MetaUpdate` DAGs. Empty
|
||||
/// `inputs` or any input under `hyperhive` → every container;
|
||||
/// otherwise just the agents named by `agent-<name>` inputs.
|
||||
/// Topology-sorted so parents rebuild before their children.
|
||||
/// Sorted by name — #4472 removed the parent field the old depth sort
|
||||
/// keyed on, and with every agent a root that sort already reduced to
|
||||
/// this.
|
||||
///
|
||||
/// `inputs` is the caller-supplied flake-input-name list (the dashboard's
|
||||
/// `POST /api/meta-update` form field, operator-supplied but not
|
||||
|
|
@ -918,8 +892,7 @@ pub async fn meta_update_cascade_agents(inputs: &[String]) -> Vec<String> {
|
|||
} else {
|
||||
touched_agents
|
||||
};
|
||||
let topo = crate::topology::read();
|
||||
crate::auto_update::topology_sort(&mut names, &topo);
|
||||
names.sort();
|
||||
names
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -102,12 +102,6 @@ pub enum NodeKind {
|
|||
WriteDropin { agent: String },
|
||||
/// Commit `tool-groups.json` / `capabilities.json` per its `payload`.
|
||||
WritePermFile { agent: String, payload: PermPayload },
|
||||
/// Topology move(s) — `set-parent` (len 1) or `set-parent-bulk` (len N) —
|
||||
/// as a single queue node. `moves` is typed `(Ident, Option<Ident>)`
|
||||
/// pairs, applied in order under one `META_LOCK` acquisition.
|
||||
Reparent {
|
||||
moves: Vec<(hive_types::Ident, Option<hive_types::Ident>)>,
|
||||
},
|
||||
/// Group root of the approval-deploy (`MergeConfigPr`) subtree — the
|
||||
/// **brace** that owns the deploy window. See _Braces_ and _Approvals_.
|
||||
DeployWindow { agent: String, approval_id: i64 },
|
||||
|
|
@ -203,7 +197,6 @@ impl hive_jobq_wire::WireNode for NodeKind {
|
|||
impl NodeKind {
|
||||
/// The agent this node targets, or `""` for agentless kinds
|
||||
/// ([`NodeKind::MetaLock`] on the `hyperhive` pseudo-agent,
|
||||
/// [`NodeKind::Reparent`] which can span multiple agents, and
|
||||
/// [`NodeKind::ResolveApproval`] which acts on an approval row).
|
||||
#[must_use]
|
||||
pub fn agent(&self) -> &str {
|
||||
|
|
@ -236,7 +229,6 @@ impl NodeKind {
|
|||
| NodeKind::EmitRebuilt { agent, .. }
|
||||
| NodeKind::SetWanted { agent, .. } => agent,
|
||||
NodeKind::MetaLock { .. }
|
||||
| NodeKind::Reparent { .. }
|
||||
| NodeKind::ResolveApproval { .. }
|
||||
| NodeKind::ForgeSweep
|
||||
| NodeKind::MatrixSweep
|
||||
|
|
|
|||
|
|
@ -597,27 +597,6 @@ pub fn meta_update(builder: &JobBuilder, inputs: Vec<String>, approval_id: Optio
|
|||
}
|
||||
}
|
||||
|
||||
/// Topology move(s) as a single-node DAG. `moves` is `(child, new_parent)`
|
||||
/// pairs — len 1 for `set-parent`, len N for `set-parent-bulk`, applied
|
||||
/// uniformly by the one [`NodeKind::Reparent`] node (which holds the global
|
||||
/// meta window for its duration, same precedent as [`NodeKind::WritePermFile`]).
|
||||
/// No rebuild subgraph: `topology.json` is read live by every consumer
|
||||
/// (dashboard tree, `<parent>`/`<children>` sentinel routing, permission
|
||||
/// checks), so a parent move needs no container rebuild to take effect.
|
||||
/// No transient pill either — the node is agentless (no lease to hang one
|
||||
/// off of) and near-instant. No tail node: the write is the whole effect.
|
||||
///
|
||||
/// Returns the single node's guid so a caller can wait on it.
|
||||
pub fn reparent(
|
||||
builder: &JobBuilder,
|
||||
moves: Vec<(hive_types::Ident, Option<hive_types::Ident>)>,
|
||||
) -> hive_jobq::NodeGuid {
|
||||
builder
|
||||
.node(NodeKind::Reparent { moves })
|
||||
.needs(Resource::MetaWindow)
|
||||
.guid()
|
||||
}
|
||||
|
||||
// The boot is assembled inline in `workers/auto_update.rs::submit_boot_tree`
|
||||
// as ONE `Boot` DAG (a sweep `MetaLock` root that grows rebuild subgraphs
|
||||
// in-DAG, plus a `Reconcile` root per drifted agent) — no anchor node and no
|
||||
|
|
|
|||
|
|
@ -49,10 +49,6 @@ fn insert_named(
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn ident(s: &str) -> hive_types::Ident {
|
||||
hive_types::Ident::parse(s).expect("valid test ident")
|
||||
}
|
||||
|
||||
fn rebuild(builder: &JobBuilder, agent: &str) -> Vec<hive_jobq::NodeGuid> {
|
||||
templates::rebuild(builder, agent, true)
|
||||
}
|
||||
|
|
@ -186,14 +182,6 @@ fn node_of(q: &JobQueue, kind: &str) -> hive_jobq::NodeId {
|
|||
found.pop().expect("checked above")
|
||||
}
|
||||
|
||||
/// The payload of the one node of `kind`, for assertions about what a node
|
||||
/// *carries* rather than how it is wired.
|
||||
fn payload_of(q: &JobQueue, kind: &str) -> NodeKind {
|
||||
let id = node_of(q, kind);
|
||||
let sched = q.sched().lock().expect("job_queue mutex poisoned");
|
||||
sched.graph().node(id).expect("node exists").payload.clone()
|
||||
}
|
||||
|
||||
/// Kinds of every node still `Pending` — the nodes that could yet run.
|
||||
/// Stronger than asking the scheduler what is *ready right now*: a node
|
||||
/// blocked on a dep is not ready but is very much still alive.
|
||||
|
|
@ -1785,48 +1773,3 @@ fn perm_change_shape_prefixes_rebuild_chain() {
|
|||
"the perm write prefixes an otherwise ordinary rebuild chain"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reparent_shape_is_a_lone_agentless_meta_window_node() {
|
||||
// Single-move `set-parent` shape: one node, no rebuild subgraph (no
|
||||
// container rebuild needed for a parent move), agentless like
|
||||
// `MetaLock`, and it must declare the meta window — a topology commit
|
||||
// must not land inside another node's staged deploy window.
|
||||
let q = JobQueue::new(1);
|
||||
insert(&q, |builder| {
|
||||
templates::reparent(builder, vec![(ident("alice"), Some(ident("bob")))]);
|
||||
});
|
||||
assert_eq!(
|
||||
declared_shape(&q),
|
||||
vec![row("reparent", None, &[])],
|
||||
"one node, no rebuild subgraph"
|
||||
);
|
||||
let node = node_of(&q, "reparent");
|
||||
assert_eq!(
|
||||
declared_resources(&q, node),
|
||||
vec![Resource::MetaWindow],
|
||||
"a topology commit must declare the same MetaWindow as WritePermFile, \
|
||||
and nothing else — no lease (agentless), no build slot (no nix work)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reparent_bulk_shape_carries_every_move_on_one_node() {
|
||||
// `set-parent-bulk`: still ONE node (one git commit, `moves.len() > 1`),
|
||||
// not one node per move — bulk atomicity across every move in the
|
||||
// request is the reason a single node was chosen in the first place.
|
||||
let moves = vec![(ident("alice"), Some(ident("bob"))), (ident("carol"), None)];
|
||||
let q = JobQueue::new(1);
|
||||
insert(&q, |builder| {
|
||||
templates::reparent(builder, moves.clone());
|
||||
});
|
||||
assert_eq!(
|
||||
declared_shape(&q),
|
||||
vec![row("reparent", None, &[])],
|
||||
"one node for the whole request, not one per move"
|
||||
);
|
||||
let NodeKind::Reparent { moves: got } = payload_of(&q, "reparent") else {
|
||||
panic!("expected a Reparent node");
|
||||
};
|
||||
assert_eq!(got, moves, "every move rides the single node");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue