feat(#1178): add list_containers tool to AgentServer (topology-scoped to descendants)

This commit is contained in:
damocles 2026-06-03 22:21:01 +02:00 committed by mara
commit 94c2d651c0
4 changed files with 98 additions and 0 deletions

View file

@ -45,6 +45,7 @@ Tools (hyperhive surface):
- `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.
- `mcp__hyperhive__list_containers()`*(requires `lifecycle` tool group)* list all containers that are topological descendants of this agent (children + their subtrees). Returns each name with running/stopped status, ordered parents-first. Useful before kill/update/restart to check what's under you.
- `mcp__hyperhive__request_init_config(name, description?)`*(requires `approvals` tool group)* initialise a brand-new direct child agent's proposed config repo. Queues an `InitConfig` approval; on approval hive-c0re seeds `/agents/<name>/config/agent.nix`. `name` must be a direct child in the topology tree — server enforces. Fails if the config repo already exists (use `request_apply_commit` instead).
- `mcp__hyperhive__request_apply_commit(agent, commit_ref, description?)`*(requires `approvals` tool group)* submit a config commit for a direct child agent, queued for operator approval. `agent` must be a direct child in the topology tree — server enforces. `commit_ref` must be a 7-40 char hex sha (not a branch/tag name). On approval hive-c0re rebuilds the container with the pinned commit.

View file

@ -50,6 +50,8 @@ pub enum SocketReply {
/// `list_schedules` result — used by the manager surface only;
/// `AgentResponse` has no equivalent variant.
Schedules(Vec<hive_sh4re::WireSchedule>),
/// `list_containers` result — descendant containers with running status.
Containers(Vec<hive_sh4re::ContainerInfo>),
LooseEnds(Vec<hive_sh4re::LooseEnd>),
PendingRemindersCount(u64),
ReminderRollup(hive_sh4re::ReminderStats),
@ -81,6 +83,7 @@ impl From<hive_sh4re::Response> for SocketReply {
hive_sh4re::Response::Logs { content } => Self::Logs(content),
hive_sh4re::Response::HostJournal { content } => Self::HostJournal(content),
hive_sh4re::Response::Schedules { schedules } => Self::Schedules(schedules),
hive_sh4re::Response::Containers { containers } => Self::Containers(containers),
hive_sh4re::Response::AgentMeta {
name,
running,
@ -1008,6 +1011,45 @@ impl AgentServer {
.await
}
// IMPORTANT: this tool is only available when the `lifecycle` tool group
// is granted to this agent. Returns all topological descendants of the
// calling agent with their running status.
#[tool(
description = "List all containers that are topological descendants of this agent \
(direct children + their subtrees). Requires the `lifecycle` tool group. \
Returns every known descendant regardless of running state check the `running` \
field to distinguish live from stopped containers. Ordered by topology depth \
(parents before children), then alphabetically within each tier."
)]
async fn list_containers(&self) -> String {
run_tool_envelope("list_containers", String::new(), async move {
let (resp, retries) = self
.dispatch(hive_sh4re::AgentRequest::ListDescendants)
.await;
let body = match resp {
Ok(SocketReply::Containers(containers)) => {
if containers.is_empty() {
"no descendant containers".to_owned()
} else {
containers
.iter()
.map(|c| {
let status = if c.running { "running" } else { "stopped" };
format!("{} ({})", c.name, status)
})
.collect::<Vec<_>>()
.join("\n")
}
}
Ok(SocketReply::Err(m)) => format!("list_containers failed: {m}"),
Ok(other) => format!("list_containers unexpected response: {other:?}"),
Err(e) => format!("list_containers transport error: {e:#}"),
};
annotate_retries(body, 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

@ -425,6 +425,42 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
coord.emit_rebuild_queue_snapshot();
AgentResponse::Ok
}
AgentRequest::ListDescendants => {
tracing::debug!(%agent, "agent: list descendants");
// All containers known to nixos-container (running only).
let running_set: std::collections::HashSet<String> =
match crate::lifecycle::list().await {
Ok(names) => names
.into_iter()
.filter_map(|c| {
c.strip_prefix(crate::lifecycle::AGENT_PREFIX)
.map(str::to_owned)
})
.collect(),
Err(e) => {
return AgentResponse::Err {
message: format!("list containers failed: {e:#}"),
}
}
};
// Walk the full topology and collect every descendant.
let topo = crate::topology::read();
let mut names: Vec<String> = topo
.keys()
.filter(|name| crate::topology::is_descendant_of(name, agent))
.cloned()
.collect();
// Parents before children, then alpha within each tier.
crate::auto_update::topology_sort(&mut names, &topo);
let containers = names
.into_iter()
.map(|name| {
let running = running_set.contains(&name);
hive_sh4re::ContainerInfo { name, running }
})
.collect();
AgentResponse::Containers { containers }
}
AgentRequest::RequestInitConfig { name, description } => {
if !crate::topology::children_of(agent)
.iter()

View file

@ -542,6 +542,12 @@ pub enum Request {
},
/// *(privileged)* List every schedule in the queue.
ListSchedules,
/// List all containers that are topological descendants of the calling
/// agent (direct children + their subtrees). Scoped to the caller's
/// subtree; gated by the `lifecycle` tool group. The result includes all
/// known descendants regardless of whether the container is currently
/// running — use `running` to distinguish.
ListDescendants,
/// *(privileged)* Fire a scheduled prompt out of band immediately.
FireScheduleNow { id: i64 },
/// *(privileged)* Edit an existing schedule's mutable fields.
@ -627,12 +633,25 @@ pub enum Response {
/// `ListSchedules` result. Snapshot of every schedule.
/// Returned on the manager socket only.
Schedules { schedules: Vec<WireSchedule> },
/// `ListDescendants` result: all descendant containers, with running
/// status. Ordered by topology depth (parents before children), then
/// alphabetically within each depth tier.
Containers { containers: Vec<ContainerInfo> },
}
/// Backwards-compatible response aliases.
pub type AgentResponse = Response;
pub type ManagerResponse = Response;
/// One entry in a `ListDescendants` result.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContainerInfo {
/// Logical agent name (no `h-` prefix).
pub name: String,
/// Whether the container is currently running.
pub running: bool,
}
/// Serde default for the `running` field; keeps wire backwards-compat
/// with pre-running-field payloads. See
/// `docs/conventions.md::Agent metadata`.