From 2722e1548ca9c20751d5ded297d18ca2fe90727f Mon Sep 17 00:00:00 2001 From: damocles Date: Wed, 3 Jun 2026 21:57:22 +0200 Subject: [PATCH] feat(#1178): add kill + update tools to AgentServer (topology-scoped) --- hive-ag3nt/prompts/system.md | 2 ++ hive-ag3nt/src/mcp.rs | 42 +++++++++++++++++++++++++++ hive-c0re/src/agent_server.rs | 54 +++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index 141ecb45..bb5db67f 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -43,6 +43,8 @@ Tools (hyperhive surface): - `mcp__hyperhive__request_next_turn()` — ask the harness to start another turn immediately after this one ends, even if the inbox is empty. Use for multi-turn tasks (long builds, sequential steps) where you want to continue without waiting for an external message. The next turn starts with `from: "self"` and `body: "continue"`. No-op if new inbox messages arrive before this turn ends (the harness already loops immediately on pending messages). No args. - `mcp__hyperhive__restart(name)` — *(requires `lifecycle` tool group)* restart a direct child sub-agent (stop + start). The server enforces topology: the call is rejected unless `name` is a direct child of yours per `topology.json`. No approval required. +- `mcp__hyperhive__kill(name)` — *(requires `lifecycle` tool group)* stop a direct child sub-agent (graceful). Direct children only — server enforces topology. State dir kept; recreating reuses prior config + credentials. No approval required. +- `mcp__hyperhive__update(name)` — *(requires `lifecycle` tool group)* rebuild a direct child sub-agent: re-applies the current hyperhive flake + agent.nix and restarts it. Direct children only — server enforces topology. No approval required. Idempotent. Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — message the manager (recipient `root`) describing what you need + why. The manager evaluates the request (it doesn't rubber-stamp), edits `/agents/{label}/config/agent.nix` on your behalf, commits, and submits an approval that the operator can accept on the dashboard; on approve hive-c0re rebuilds your container with the new config. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 702df9c0..b40d0ee9 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -966,6 +966,48 @@ impl AgentServer { .await } + // IMPORTANT: this tool is only available when the `lifecycle` tool group + // is granted to this agent. hive-c0re enforces the topology check + // server-side: the call is rejected unless `name` is a direct child. + #[tool( + description = "Stop a direct child sub-agent container (graceful). \ + Only succeeds if `name` is a direct child of this agent in the topology \ + tree — the server enforces this. No approval required. \ + State dir is kept; recreating the agent reuses prior config + credentials." + )] + async fn kill(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + let name = args.name.clone(); + run_tool_envelope("kill", log, async move { + let (resp, retries) = self + .dispatch(hive_sh4re::AgentRequest::Kill { name: args.name }) + .await; + annotate_retries(format_ack(resp, "kill", format!("killed {name}")), retries) + }) + .await + } + + // IMPORTANT: this tool is only available when the `lifecycle` tool group + // is granted to this agent. hive-c0re enforces the topology check + // server-side: the call is rejected unless `name` is a direct child. + #[tool( + description = "Rebuild a direct child sub-agent: re-applies the current hyperhive \ + flake + agent.nix and restarts the container. Only succeeds if `name` is a direct \ + child of this agent in the topology tree — the server enforces this. \ + No approval required. Idempotent — use when a child needs its config reapplied." + )] + async fn update(&self, Parameters(args): Parameters) -> String { + let log = format!("{args:?}"); + let name = args.name.clone(); + run_tool_envelope("update", log, async move { + let (resp, retries) = self + .dispatch(hive_sh4re::AgentRequest::Update { name: args.name }) + .await; + annotate_retries(format_ack(resp, "update", format!("updated {name}")), retries) + }) + .await + } + // IMPORTANT: this tool is capability-gated (`read_host_journal`). // It is added to `--allowedTools` by `allowed_capability_tools` only // when `HIVE_CAPABILITIES` contains `read_host_journal`. hive-c0re diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index 3ff38bd9..807182df 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -371,6 +371,60 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> coord.emit_rebuild_queue_snapshot(); AgentResponse::Ok } + AgentRequest::Kill { name } => { + if !crate::topology::children_of(agent) + .iter() + .any(|c| c == name) + { + return AgentResponse::Err { + message: format!( + "agent `{agent}` cannot kill `{name}`: \ + not a direct child in the topology tree" + ), + }; + } + tracing::info!(%agent, %name, "agent: kill child"); + let result: anyhow::Result<()> = async { + crate::lifecycle::kill(name).await?; + coord.unregister_agent(name); + Ok(()) + } + .await; + match result { + Ok(()) => { + coord.notify_manager(&hive_sh4re::HelperEvent::Killed { + agent: name.clone(), + }); + AgentResponse::Ok + } + Err(e) => AgentResponse::Err { + message: format!("{e:#}"), + }, + } + } + AgentRequest::Update { name } => { + if !crate::topology::children_of(agent) + .iter() + .any(|c| c == name) + { + return AgentResponse::Err { + message: format!( + "agent `{agent}` cannot rebuild `{name}`: \ + not a direct child in the topology tree" + ), + }; + } + tracing::info!(%agent, %name, "agent: enqueue rebuild for child"); + coord.rebuild_queue.enqueue( + crate::rebuild_queue::QueueKind::Rebuild, + name.to_owned(), + crate::rebuild_queue::QueueSource::Manual, + format!("agent `{agent}` update tool"), + None, + ); + coord.emit_rebuild_queue_snapshot(); + AgentResponse::Ok + } // Manager-only variants are not valid on the agent socket. _ => AgentResponse::Err { message: "request not supported on agent socket".to_owned(),