meta inputs panel: walk transitive inputs, slash-path names
read_meta_inputs() previously only included direct inputs of meta's root node — so a manager-added 'inputs.mcp-matrix' in agent-dmatrix's flake.nix never surfaced in the dashboard panel even though it's a real fetched input that nix can update. now: BFS the flake.lock graph from root to depth 2. emits one MetaInputView per fetched (non-follows) node, names are slash-paths from root — 'hyperhive', 'agent-coder', 'agent-dmatrix/mcp-matrix', 'hyperhive/nixpkgs', etc. that's the same syntax 'nix flake update' accepts for transitive inputs, so the existing POST /meta-update path needs no nix-side change. depth limit of 2 keeps the panel readable — deeper transitives (nixpkgs's own deps etc.) would explode it; bumping a level-2 entry re-fetches its sub-inputs anyway. POST /meta-update's 'which agents to rebuild' derivation updated for the slash names: anything under hyperhive/ fans out to all agents (shared base); 'agent-<n>/...' picks out the agent name from before the first slash. read_meta_locked_revs (used by the deployed:<sha> chip per container) split out into its own straight root-input lookup since the chip only cares about the agent's own input.
This commit is contained in:
parent
67e4242b9f
commit
78aa830430
1 changed files with 126 additions and 49 deletions
|
|
@ -380,15 +380,47 @@ async fn build_container_views(
|
|||
(out, any_stale)
|
||||
}
|
||||
|
||||
/// Parse `/var/lib/hyperhive/meta/flake.lock` into a map of node name
|
||||
/// (`agent-<n>`, `hyperhive`) → locked sha. Missing / unparsable lock
|
||||
/// yields an empty map so the dashboard degrades gracefully when the
|
||||
/// meta repo hasn't been seeded yet.
|
||||
/// Map of node name → locked sha for nodes the **root** of meta
|
||||
/// directly depends on (`hyperhive`, `agent-<n>`). Used by the
|
||||
/// container row to render its `deployed:<sha12>` chip per agent.
|
||||
/// Distinct from `read_meta_inputs()` which walks deeper for the
|
||||
/// flake-input update form.
|
||||
fn read_meta_locked_revs() -> std::collections::HashMap<String, String> {
|
||||
read_meta_inputs()
|
||||
.into_iter()
|
||||
.map(|i| (i.name, i.rev))
|
||||
.collect()
|
||||
let mut out = std::collections::HashMap::new();
|
||||
let Ok(raw) = std::fs::read_to_string("/var/lib/hyperhive/meta/flake.lock") else {
|
||||
return out;
|
||||
};
|
||||
let Ok(json) = serde_json::from_str::<serde_json::Value>(&raw) else {
|
||||
return out;
|
||||
};
|
||||
let Some(nodes) = json.get("nodes").and_then(|v| v.as_object()) else {
|
||||
return out;
|
||||
};
|
||||
let Some(root_name) = json.get("root").and_then(|v| v.as_str()) else {
|
||||
return out;
|
||||
};
|
||||
let Some(root_inputs) = nodes
|
||||
.get(root_name)
|
||||
.and_then(|n| n.get("inputs"))
|
||||
.and_then(|v| v.as_object())
|
||||
else {
|
||||
return out;
|
||||
};
|
||||
for alias in root_inputs.keys() {
|
||||
let target_name = match root_inputs.get(alias) {
|
||||
Some(serde_json::Value::String(s)) => s.clone(),
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(rev) = nodes
|
||||
.get(&target_name)
|
||||
.and_then(|n| n.get("locked"))
|
||||
.and_then(|v| v.get("rev"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
out.insert(alias.clone(), rev.to_owned());
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
|
|
@ -406,10 +438,20 @@ struct MetaInputView {
|
|||
url: Option<String>,
|
||||
}
|
||||
|
||||
/// Walk `flake.lock`'s `nodes` map → `Vec<MetaInputView>`. Only
|
||||
/// includes nodes the root depends on (i.e. real inputs), skipping
|
||||
/// the synthetic `root` entry. Sorted with `hyperhive` first then
|
||||
/// alphabetically so the UI's top entry is the swarm-wide base.
|
||||
/// Walk `flake.lock`'s `nodes` graph from `root` and emit one
|
||||
/// `MetaInputView` per fetched input, up to two levels deep. That
|
||||
/// surfaces the direct meta inputs (`hyperhive`, `agent-<n>`) AND
|
||||
/// the agent flakes' own inputs (`agent-dmatrix/mcp-matrix`,
|
||||
/// `hyperhive/nixpkgs`, etc.) so the operator can bump them
|
||||
/// individually from the UI. Deeper transitive nodes aren't shown
|
||||
/// to keep the panel readable — bumping the level-2 entry will
|
||||
/// re-fetch its own sub-inputs anyway. Names are slash-separated
|
||||
/// paths from root, which is the syntax `nix flake update` accepts
|
||||
/// for transitive inputs.
|
||||
///
|
||||
/// Inputs that resolve via a `follows` chain (lock value is an
|
||||
/// Array of strings) are skipped — they're aliases, not their own
|
||||
/// fetched derivation, and updating them does nothing.
|
||||
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 {
|
||||
|
|
@ -424,40 +466,9 @@ fn read_meta_inputs() -> Vec<MetaInputView> {
|
|||
let Some(root_name) = json.get("root").and_then(|v| v.as_str()) else {
|
||||
return out;
|
||||
};
|
||||
let root_inputs: std::collections::BTreeSet<String> = nodes
|
||||
.get(root_name)
|
||||
.and_then(|n| n.get("inputs"))
|
||||
.and_then(|v| v.as_object())
|
||||
.map(|m| m.keys().cloned().collect())
|
||||
.unwrap_or_default();
|
||||
for (name, node) in nodes {
|
||||
if !root_inputs.contains(name) {
|
||||
continue;
|
||||
}
|
||||
let locked = node.get("locked");
|
||||
let Some(rev) = locked
|
||||
.and_then(|v| v.get("rev"))
|
||||
.and_then(|v| v.as_str())
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let last_modified = locked
|
||||
.and_then(|v| v.get("lastModified"))
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let url = node
|
||||
.get("original")
|
||||
.and_then(|v| v.get("url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_owned);
|
||||
out.push(MetaInputView {
|
||||
name: name.clone(),
|
||||
rev: rev.to_owned(),
|
||||
last_modified,
|
||||
url,
|
||||
});
|
||||
}
|
||||
// hyperhive first, then alphabetical.
|
||||
walk_meta_inputs(nodes, root_name, "", 0, 2, &mut out);
|
||||
// hyperhive first, then alphabetical (sub-paths sort under their
|
||||
// parent, which gives a tidy 'agent-foo, agent-foo/bar' grouping).
|
||||
out.sort_by(|a, b| match (a.name.as_str(), b.name.as_str()) {
|
||||
("hyperhive", _) => std::cmp::Ordering::Less,
|
||||
(_, "hyperhive") => std::cmp::Ordering::Greater,
|
||||
|
|
@ -466,6 +477,65 @@ fn read_meta_inputs() -> Vec<MetaInputView> {
|
|||
out
|
||||
}
|
||||
|
||||
fn walk_meta_inputs(
|
||||
nodes: &serde_json::Map<String, serde_json::Value>,
|
||||
node_name: &str,
|
||||
prefix: &str,
|
||||
depth: u32,
|
||||
max_depth: u32,
|
||||
out: &mut Vec<MetaInputView>,
|
||||
) {
|
||||
if depth >= max_depth {
|
||||
return;
|
||||
}
|
||||
let Some(node) = nodes.get(node_name) else {
|
||||
return;
|
||||
};
|
||||
let Some(inputs_map) = node.get("inputs").and_then(|v| v.as_object()) else {
|
||||
return;
|
||||
};
|
||||
for (alias, target) in inputs_map {
|
||||
// Inputs map value is either a string (node name) or an
|
||||
// array (a `follows` chain). The latter just aliases another
|
||||
// node — we can't `nix flake update` it directly, so skip.
|
||||
let target_name = match target {
|
||||
serde_json::Value::String(s) => s.clone(),
|
||||
_ => continue,
|
||||
};
|
||||
let Some(target_node) = nodes.get(&target_name) else {
|
||||
continue;
|
||||
};
|
||||
let path = if prefix.is_empty() {
|
||||
alias.clone()
|
||||
} else {
|
||||
format!("{prefix}/{alias}")
|
||||
};
|
||||
if let Some(rev) = target_node
|
||||
.get("locked")
|
||||
.and_then(|v| v.get("rev"))
|
||||
.and_then(|v| v.as_str())
|
||||
{
|
||||
let last_modified = target_node
|
||||
.get("locked")
|
||||
.and_then(|v| v.get("lastModified"))
|
||||
.and_then(serde_json::Value::as_i64)
|
||||
.unwrap_or(0);
|
||||
let url = target_node
|
||||
.get("original")
|
||||
.and_then(|v| v.get("url"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(str::to_owned);
|
||||
out.push(MetaInputView {
|
||||
name: path.clone(),
|
||||
rev: rev.to_owned(),
|
||||
last_modified,
|
||||
url,
|
||||
});
|
||||
}
|
||||
walk_meta_inputs(nodes, &target_name, &path, depth + 1, max_depth, out);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transient state for agents whose container does NOT yet exist
|
||||
/// (`Spawning`). Lifecycle ops on existing containers surface as
|
||||
/// `ContainerView.pending` inline; this list only catches pre-creation.
|
||||
|
|
@ -917,11 +987,18 @@ async fn run_meta_update(coord: &Arc<crate::coordinator::Coordinator>, inputs: &
|
|||
return;
|
||||
}
|
||||
|
||||
// Decide which agents to rebuild.
|
||||
let touched_hyperhive = inputs.iter().any(|i| i == "hyperhive");
|
||||
// Decide which agents to rebuild. Inputs are slash-paths from
|
||||
// the meta root — `hyperhive`, `hyperhive/nixpkgs`,
|
||||
// `agent-coder`, `agent-coder/mcp-matrix`, etc. Anything in the
|
||||
// hyperhive subtree affects every agent (shared base); anything
|
||||
// in `agent-<n>/...` only the named agent.
|
||||
let touched_hyperhive = inputs
|
||||
.iter()
|
||||
.any(|i| i == "hyperhive" || i.starts_with("hyperhive/"));
|
||||
let touched_agents: Vec<String> = inputs
|
||||
.iter()
|
||||
.filter_map(|i| i.strip_prefix("agent-").map(str::to_owned))
|
||||
.filter_map(|i| i.strip_prefix("agent-"))
|
||||
.map(|rest| rest.split('/').next().unwrap_or(rest).to_owned())
|
||||
.collect();
|
||||
let agents_to_rebuild: Vec<String> = if touched_hyperhive {
|
||||
crate::lifecycle::list()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue