manager: add update_meta_inputs tool to update flake.lock on demand (closes #235)

This commit is contained in:
damocles 2026-05-22 01:31:26 +02:00 committed by Mara
parent 15e44955a8
commit 597e4ba03a
5 changed files with 76 additions and 8 deletions

View file

@ -10,6 +10,7 @@ Tools (hyperhive surface):
- `mcp__hyperhive__start(name)` — start a stopped sub-agent. No approval required.
- `mcp__hyperhive__restart(name)` — stop + start a sub-agent. No approval required.
- `mcp__hyperhive__update(name)` — rebuild a sub-agent (re-applies the current hyperhive flake + agent.nix, restarts the container). No approval required — idempotent. Use when you receive a `needs_update` system event.
- `mcp__hyperhive__update_meta_inputs(inputs?)` — run `nix flake update [inputs...]` on the meta flake and commit the lock changes. Pass specific input names (e.g. `["bitburner-agent"]`) to update just those, or omit / pass `[]` to update everything. Blocks until complete. Does NOT trigger rebuilds — call `update(name)` on affected agents afterward.
- `mcp__hyperhive__get_logs(agent, lines?)` — fetch recent journal lines for a sub-agent container. Use to diagnose MCP-server registration failures, startup crashes, or harness issues you can't see from inside. Pass the plain logical agent name; `lines` defaults to 50 (capped at 500).
- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)` — submit a config change for any agent (`hm1nd` for self) for operator approval. Pass an optional `description` and it appears on the dashboard approval card so the operator knows what changed without opening the diff. At submit time hive-c0re fetches your commit into the agent's applied repo and pins it as `proposal/<id>`; from that moment your proposed-side commit can be amended or force-pushed freely without changing what the operator will build.
- `mcp__hyperhive__ask(question, options?, multi?, ttl_seconds?, to?)` — surface a structured question to the operator (default, or `to: "operator"`) OR a sub-agent (`to: "<agent-name>"`). Returns immediately with a question id; the answer arrives later as a system `question_answered { id, question, answer, answerer }` event in your inbox. Options are advisory: the dashboard always lets the operator type a free-text answer in addition. Set `multi: true` to render options as checkboxes (operator can pick multiple); the answer comes back as `, `-separated. Set `ttl_seconds` to auto-cancel after a deadline (capped at 6h server-side) — on expiry the answer is `[expired]` and `answerer` is `"ttl-watchdog"`. Do not poll inside the same turn — finish the current work and react when the event lands.

View file

@ -807,6 +807,14 @@ pub struct RequestApplyCommitArgs {
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct UpdateMetaInputsArgs {
/// Flake input names to update (e.g. `["bitburner-agent", "nixpkgs"]`).
/// Pass an empty list to update ALL inputs.
#[serde(default)]
pub inputs: Vec<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GetLogsArgs {
/// Logical agent name to fetch logs for (e.g. `gui`, `hm1nd`).
@ -1022,6 +1030,38 @@ impl ManagerServer {
.await
}
#[tool(
description = "Run `nix flake update` on the meta flake and commit the resulting \
`flake.lock` changes. Pass specific input names to update only those inputs \
(e.g. `[\"bitburner-agent\"]`), or pass an empty list to update ALL inputs. \
Blocks until the lock step completes (~30-120s depending on download size). \
Does NOT trigger container rebuilds call `update` on each affected agent \
separately after the lock settles."
)]
async fn update_meta_inputs(
&self,
Parameters(args): Parameters<UpdateMetaInputsArgs>,
) -> String {
let log = format!("{args:?}");
run_tool_envelope("update_meta_inputs", log, async move {
let label = if args.inputs.is_empty() {
"all inputs".to_string()
} else {
args.inputs.join(", ")
};
let (resp, retries) = self
.dispatch(hive_sh4re::ManagerRequest::UpdateMetaInputs {
inputs: args.inputs,
})
.await;
annotate_retries(
format_ack(resp, "update_meta_inputs", format!("lock updated: {label}")),
retries,
)
})
.await
}
#[tool(
description = "Surface a structured question to either the operator OR a sub-agent. \
Returns immediately with a question id do NOT wait inline. When the recipient \

View file

@ -311,6 +311,22 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc<Coordinator>) -> ManagerResp
},
}
}
ManagerRequest::UpdateMetaInputs { inputs } => {
let label = if inputs.is_empty() {
"all inputs".to_string()
} else {
inputs.join(", ")
};
tracing::info!(%label, "manager: update_meta_inputs");
// Treat empty list as "update all" by passing the full list
// to lock_update; it calls bare `nix flake update` when empty.
match crate::meta::lock_update(inputs).await {
Ok(()) => ManagerResponse::Ok,
Err(e) => ManagerResponse::Err {
message: format!("update_meta_inputs({label}): {e:#}"),
},
}
}
ManagerRequest::Ask {
question,
options,

View file

@ -190,14 +190,12 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
/// Used by the dashboard's "update meta inputs" form so the
/// operator can bulk-bump `hyperhive` + selected agents in one
/// shot. Each input name is passed verbatim to
/// `nix flake update`; the caller is responsible for picking
/// real input keys (e.g. via `inputs_view()` snapshotted from
/// the lock file).
#[allow(dead_code)] // wired up by dashboard handler in the same commit
/// Run `nix flake update [inputs...]` on the meta flake and commit the
/// resulting lock changes. When `inputs` is empty, updates ALL inputs
/// (bare `nix flake update`). The caller is responsible for picking
/// real input keys (e.g. via `inputs_view()` snapshotted from the lock
/// file) when targeting specific inputs.
pub async fn lock_update(inputs: &[String]) -> Result<()> {
if inputs.is_empty() {
return Ok(());
}
let _guard = META_LOCK.lock().await;
let dir = meta_dir();
let mut args: Vec<&str> = vec!["flake", "update"];
@ -209,7 +207,9 @@ pub async fn lock_update(inputs: &[String]) -> Result<()> {
return Ok(());
}
git(&dir, &["add", "flake.lock"]).await?;
let msg = if inputs.len() == 1 {
let msg = if inputs.is_empty() {
"lock update: all inputs".to_string()
} else if inputs.len() == 1 {
format!("lock update: {}", inputs[0])
} else {
format!("lock update: {}", inputs.join(", "))

View file

@ -842,6 +842,17 @@ pub enum ManagerRequest {
/// message into the manager's own broker inbox. `from` is caller-
/// chosen; `body` becomes the wake prompt body.
Wake { from: String, body: String },
/// Run `nix flake update [inputs...]` on the meta flake and commit
/// the resulting `flake.lock` changes. `inputs` is the list of
/// named inputs to update (e.g. `["bitburner-agent", "nixpkgs"]`).
/// Pass an empty list to update ALL inputs (equivalent to bare
/// `nix flake update`). Blocks until the lock step completes;
/// does NOT trigger a rebuild — call `update` on each affected
/// agent separately after the lock settles.
UpdateMetaInputs {
#[serde(default)]
inputs: Vec<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]