diff --git a/hive-ag3nt/prompts/system.md b/hive-ag3nt/prompts/system.md index 0275a9b7..a23a5410 100644 --- a/hive-ag3nt/prompts/system.md +++ b/hive-ag3nt/prompts/system.md @@ -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//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. diff --git a/hive-ag3nt/src/mcp.rs b/hive-ag3nt/src/mcp.rs index 004d5e48..6e97f76c 100644 --- a/hive-ag3nt/src/mcp.rs +++ b/hive-ag3nt/src/mcp.rs @@ -50,6 +50,8 @@ pub enum SocketReply { /// `list_schedules` result — used by the manager surface only; /// `AgentResponse` has no equivalent variant. Schedules(Vec), + /// `list_containers` result — descendant containers with running status. + Containers(Vec), LooseEnds(Vec), PendingRemindersCount(u64), ReminderRollup(hive_sh4re::ReminderStats), @@ -81,6 +83,7 @@ impl From 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::>() + .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 diff --git a/hive-c0re/src/agent_server.rs b/hive-c0re/src/agent_server.rs index a123e866..ba653733 100644 --- a/hive-c0re/src/agent_server.rs +++ b/hive-c0re/src/agent_server.rs @@ -425,6 +425,42 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc) -> 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 = + 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 = 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() diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 95a3903c..66ce9d3b 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -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 }, + /// `ListDescendants` result: all descendant containers, with running + /// status. Ordered by topology depth (parents before children), then + /// alphabetically within each depth tier. + Containers { containers: Vec }, } /// 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`.