feat(#1178): add kill + update tools to AgentServer (topology-scoped)

This commit is contained in:
damocles 2026-06-03 21:57:22 +02:00 committed by mara
commit 2722e1548c
3 changed files with 98 additions and 0 deletions

View file

@ -43,6 +43,8 @@ Tools (hyperhive surface):
<!-- role:agent -->
- `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.

View file

@ -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<KillArgs>) -> 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<UpdateArgs>) -> 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

View file

@ -371,6 +371,60 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
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(),