From 3e098c56ff4ba07e10685941f905bd8c8c05505d Mon Sep 17 00:00:00 2001 From: damocles Date: Fri, 22 May 2026 09:26:09 +0200 Subject: [PATCH] update_meta_inputs: require operator approval, rename to request_update_meta_inputs --- hive-ag3nt/prompts/manager.md | 2 +- hive-ag3nt/src/mcp.rs | 26 ++++++++++++------- hive-c0re/src/actions.rs | 13 ++++++++++ hive-c0re/src/approvals.rs | 3 +++ hive-c0re/src/dashboard.rs | 9 +++++++ hive-c0re/src/manager_server.rs | 46 ++++++++++++++++++++++++++------- hive-sh4re/src/lib.rs | 23 +++++++++++------ 7 files changed, 94 insertions(+), 28 deletions(-) diff --git a/hive-ag3nt/prompts/manager.md b/hive-ag3nt/prompts/manager.md index 64f44c2..d26bc97 100644 --- a/hive-ag3nt/prompts/manager.md +++ b/hive-ag3nt/prompts/manager.md @@ -10,7 +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__request_update_meta_inputs(inputs?, description?)` — queue an approval for the operator to run `nix flake update [inputs...]` on the meta flake. Pass specific input names (e.g. `["bitburner-agent"]`) or omit / pass `[]` for all inputs. Returns immediately; lock update runs on operator approval. Does NOT trigger rebuilds — call `update(name)` on affected agents after approval resolves. - `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/`; 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: ""`). 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. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 3ba1430..eb41797 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -813,6 +813,9 @@ pub struct UpdateMetaInputsArgs { /// Pass an empty list to update ALL inputs. #[serde(default)] pub inputs: Vec, + /// Optional description shown on the dashboard approval card. + #[serde(default)] + pub description: Option, } #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] @@ -1031,31 +1034,36 @@ impl ManagerServer { } #[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). \ + description = "Queue an approval for the operator to run `nix flake update` on the \ + meta flake and commit the resulting lock changes. Pass specific input names to update \ + only those inputs (e.g. `[\"bitburner-agent\"]`), or pass an empty list to update ALL \ + inputs. Returns immediately — the lock update runs when the operator approves. \ Does NOT trigger container rebuilds — call `update` on each affected agent \ - separately after the lock settles." + separately after the approval resolves." )] - async fn update_meta_inputs( + async fn request_update_meta_inputs( &self, Parameters(args): Parameters, ) -> String { let log = format!("{args:?}"); - run_tool_envelope("update_meta_inputs", log, async move { + run_tool_envelope("request_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 { + .dispatch(hive_sh4re::ManagerRequest::RequestUpdateMetaInputs { inputs: args.inputs, + description: args.description, }) .await; annotate_retries( - format_ack(resp, "update_meta_inputs", format!("lock updated: {label}")), + format_ack( + resp, + "request_update_meta_inputs", + format!("approval queued: {label}"), + ), retries, ) }) diff --git a/hive-c0re/src/actions.rs b/hive-c0re/src/actions.rs index 0fa0560..2443a6e 100644 --- a/hive-c0re/src/actions.rs +++ b/hive-c0re/src/actions.rs @@ -69,6 +69,14 @@ pub async fn approve(coord: Arc, id: i64) -> Result<()> { .await; finish_approval(&coord, &approval, result, None) } + ApprovalKind::UpdateMetaInputs => { + // Decode the inputs from the commit_ref field (stored as JSON + // by submit_apply_commit's counterpart in manager_server.rs). + let inputs: Vec = + serde_json::from_str(&approval.commit_ref).unwrap_or_default(); + let result = crate::meta::lock_update(&inputs).await; + finish_approval(&coord, &approval, result, None) + } ApprovalKind::Spawn => { // Run the spawn in the background so the approve POST returns // immediately. The dashboard reads `transient` to render a spinner. @@ -158,6 +166,7 @@ fn finish_approval( ApprovalKind::Spawn => "spawn", ApprovalKind::ApplyCommit => "apply_commit", ApprovalKind::InitConfig => "init_config", + ApprovalKind::UpdateMetaInputs => "update_meta_inputs", }; let sha_short = approval .fetched_sha @@ -199,6 +208,9 @@ fn finish_approval( sha: approval.fetched_sha.clone(), tag: terminal_tag, }), + // UpdateMetaInputs: ApprovalResolved already carries the result. + // No separate lifecycle event needed. + ApprovalKind::UpdateMetaInputs => {} } result } @@ -462,6 +474,7 @@ pub async fn deny(coord: &Coordinator, id: i64, note: Option<&str>) -> Result<() ApprovalKind::Spawn => "spawn", ApprovalKind::ApplyCommit => "apply_commit", ApprovalKind::InitConfig => "init_config", + ApprovalKind::UpdateMetaInputs => "update_meta_inputs", }; let sha_short = sha.as_deref().map(|s| s[..s.len().min(12)].to_owned()); let description = a.description.clone(); diff --git a/hive-c0re/src/approvals.rs b/hive-c0re/src/approvals.rs index ba9f9f2..9afc709 100644 --- a/hive-c0re/src/approvals.rs +++ b/hive-c0re/src/approvals.rs @@ -285,6 +285,7 @@ fn row_to_approval(row: &rusqlite::Row<'_>) -> rusqlite::Result { "apply_commit" => ApprovalKind::ApplyCommit, "spawn" => ApprovalKind::Spawn, "init_config" => ApprovalKind::InitConfig, + "update_meta_inputs" => ApprovalKind::UpdateMetaInputs, other => { return Err(rusqlite::Error::FromSqlConversionFailure( 2, @@ -326,6 +327,7 @@ fn kind_to_str(kind: ApprovalKind) -> &'static str { ApprovalKind::ApplyCommit => "apply_commit", ApprovalKind::Spawn => "spawn", ApprovalKind::InitConfig => "init_config", + ApprovalKind::UpdateMetaInputs => "update_meta_inputs", } } @@ -334,6 +336,7 @@ fn kind_from_str(s: &str) -> Result { "apply_commit" => ApprovalKind::ApplyCommit, "spawn" => ApprovalKind::Spawn, "init_config" => ApprovalKind::InitConfig, + "update_meta_inputs" => ApprovalKind::UpdateMetaInputs, other => bail!("unknown approval kind '{other}'"), }) } diff --git a/hive-c0re/src/dashboard.rs b/hive-c0re/src/dashboard.rs index e66296a..bbce25d 100644 --- a/hive-c0re/src/dashboard.rs +++ b/hive-c0re/src/dashboard.rs @@ -575,6 +575,7 @@ fn history_view(a: Approval) -> ApprovalHistoryView { hive_sh4re::ApprovalKind::ApplyCommit => "apply_commit", hive_sh4re::ApprovalKind::Spawn => "spawn", hive_sh4re::ApprovalKind::InitConfig => "init_config", + hive_sh4re::ApprovalKind::UpdateMetaInputs => "update_meta_inputs", }; ApprovalHistoryView { id: a.id, @@ -623,6 +624,14 @@ async fn build_approval_views(approvals: Vec) -> Vec { diff: None, description: a.description, }, + hive_sh4re::ApprovalKind::UpdateMetaInputs => ApprovalView { + id: a.id, + agent: a.agent, + kind: "update_meta_inputs", + sha_short: None, + diff: None, + description: a.description, + }, }); } out diff --git a/hive-c0re/src/manager_server.rs b/hive-c0re/src/manager_server.rs index 3c432ac..d045b2c 100644 --- a/hive-c0re/src/manager_server.rs +++ b/hive-c0re/src/manager_server.rs @@ -311,21 +311,47 @@ async fn dispatch(req: &ManagerRequest, coord: &Arc) -> ManagerResp }, } } - ManagerRequest::UpdateMetaInputs { inputs } => { + ManagerRequest::RequestUpdateMetaInputs { + inputs, + description, + } => { 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:#}"), - }, - } + tracing::info!(%label, "manager: request_update_meta_inputs"); + // Encode the inputs list as JSON and store it in commit_ref + // (there's no git commit involved; the field carries the + // payload for the approval handler to decode at run time). + let commit_ref = serde_json::to_string(inputs).unwrap_or_default(); + let id = match coord + .approvals + .submit_kind( + hive_sh4re::MANAGER_AGENT, + hive_sh4re::ApprovalKind::UpdateMetaInputs, + &commit_ref, + description.as_deref(), + ) + .map_err(|e| anyhow::anyhow!("{e:#}")) + { + Ok(id) => id, + Err(e) => { + return ManagerResponse::Err { + message: format!("queue update_meta_inputs approval: {e:#}"), + } + } + }; + tracing::info!(%id, %label, "update_meta_inputs approval queued"); + coord.emit_approval_added( + id, + hive_sh4re::MANAGER_AGENT, + "update_meta_inputs", + None, + None, + description.clone(), + ); + ManagerResponse::Ok } ManagerRequest::Ask { question, diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 944cc97..39f591d 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -105,6 +105,11 @@ pub enum ApprovalKind { /// template but does NOT create the container - that requires a /// subsequent `RequestSpawn` approval. InitConfig, + /// Run `nix flake update [inputs...]` on the meta flake and commit + /// the resulting lock changes. The `commit_ref` field stores the + /// JSON-encoded inputs array (`"[]"` = all inputs). Agent field is + /// set to `hm1nd` (the requesting manager). + UpdateMetaInputs, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] @@ -842,16 +847,18 @@ 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 { + /// Queue an approval to run `nix flake update [inputs...]` on the + /// meta flake. `inputs` is the list of named inputs to update + /// (e.g. `["bitburner-agent", "nixpkgs"]`). Pass an empty list to + /// update ALL inputs. On operator approval hive-c0re runs the lock + /// update and commits the result. The `UpdateMetaInputs` approval + /// resolves with `ApprovalResolved` in the manager inbox. + RequestUpdateMetaInputs { #[serde(default)] inputs: Vec, + /// Optional description shown on the dashboard approval card. + #[serde(default, skip_serializing_if = "Option::is_none")] + description: Option, }, }