remove the list_containers and request_update_meta_inputs MCP tools

Both agent-facing tools go away end to end, with no replacement. This is
an intentional capability removal: agents can no longer enumerate their
own subtree, and can no longer queue a meta-flake input bump.

The system prompt and docs/tools/lifecycle.md land in this same commit
on purpose. A tool named in the prompt but absent from the server makes
agents confidently call something that doesn't exist, and the failure
then surfaces far from its cause.

Removed:

- MCP registrations and bodies (hive-agent-mcp), plus the now-unused
  UpdateMetaInputsArgs.
- Wire variants Request::ListDescendants,
  Request::RequestUpdateMetaInputs and Response::Containers, plus
  ContainerInfo, whose only consumer was that response.
- hive-c0re's handle_list_descendants (its whole module) and
  handle_request_update_meta_inputs, the two dispatch arms, and the
  require_group(agent, "approvals", ...) gate on the meta-inputs verb.
- The stream_enrich emoji entry and argument formatter.
- docs/tools/lifecycle.md (both tools it documented are gone), its two
  referrers, the tool-group tables and the agent-hierarchy prose.

Tool groups are kept, deliberately. ToolGroup::Lifecycle listed exactly
one tool and now lists none — it is vestigial, but the variant stays so
existing meta/capabilities.json grants still parse; retiring it is a
separate decision. ToolGroup::Approvals also listed exactly one tool,
but the group is NOT dead: check_can_cancel_approval still gates
cancel_loose_end's approval-cancel arm on it server-side.

ApprovalKind::UpdateMetaInputs stays too. Nothing in production code
produces it any more, but pre-existing approval rows may still carry it,
and the operator's own path to a meta update is unaffected — the
dashboard's POST /api/meta-update inserts the meta_update job directly,
bypassing approvals entirely.

The two format_ack tests in hive-agent-mcp that named
request_update_meta_inputs were only using it as a label string while
exercising the generic OkWarn/Ok renderer, so they are retargeted to a
surviving tool rather than deleted.

Note hive-c0re's priv_client::list_containers is a different thing (the
host-side privileged container listing behind hive-priv) and is
untouched.

Closes #4591
This commit is contained in:
atlas 2026-09-20 19:36:09 +02:00 committed by mara
commit b88a5b2430
18 changed files with 74 additions and 356 deletions

View file

@ -96,7 +96,6 @@ umount-old / mount-new / restart-cascade step.
| config change via forge PR (any descendant's config) | any ancestor | | config change via forge PR (any descendant's config) | any ancestor |
| moderate reminders (cancel any open thread of a descendant) | any ancestor | | moderate reminders (cancel any open thread of a descendant) | any ancestor |
| `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
"Ancestor" walks `ContainerView.parent` chains; a visited-set guards against "Ancestor" walks `ContainerView.parent` chains; a visited-set guards against
cycles at dispatch time (a malformed `topology.json` can't lock cycles at dispatch time (a malformed `topology.json` can't lock
@ -116,14 +115,11 @@ other agents don't:
approval step — every other agent goes through a `Spawn` approval. approval step — every other agent goes through a `Spawn` approval.
Topology-wise, `ruth` is still just another root agent. Topology-wise, `ruth` is still just another root agent.
- **Wire-protocol** — the privileged `Request` variants - **Wire-protocol** — the privileged `Request` variants
(`Kill` / `Start` / `Restart` / `Update`; (`Kill` / `Start` / `Restart` / `Update`; `GetLogs`) — marked
`GetLogs`; `RequestUpdateMetaInputs`) — marked `*(privileged)*` in `*(privileged)*` in `hive-core-agent-sock`'s unified `Request` enum —
`hive-core-agent-sock`'s unified `Request` enum — are reachable only are reachable only from the manager's socket flavour today. Planned
from the manager's socket flavour today. Planned rule for each is in the rule for each is in the table above ("any ancestor" for
table above ("any ancestor" for lifecycle/logs); lifecycle/logs). One exception: `Wake` (inject a `from: <X>` message into the
`RequestUpdateMetaInputs` stays
a root-only capability even post-milestone, not a topology rule.
One exception: `Wake` (inject a `from: <X>` message into the
caller's own inbox) isn't really privileged — every per-agent daemon caller's own inbox) isn't really privileged — every per-agent daemon
(for example `hive-forge-notify`) needs it, and sub-agents already have the (for example `hive-forge-notify`) needs it, and sub-agents already have the
equivalent on their own socket. equivalent on their own socket.
@ -135,9 +131,10 @@ other agents don't:
deploy log). Planned: each agent gets RW to `/agents/<descendant>/` deploy log). Planned: each agent gets RW to `/agents/<descendant>/`
for just its own subtree — the manager's full-forest RW becomes the for just its own subtree — the manager's full-forest RW becomes the
"root's subtree is everything" case of that same rule. hive-c0re will "root's subtree is everything" case of that same rule. hive-c0re will
gate RO `/meta` access on a "meta read" capability; only gate RO `/meta` access on a "meta read" capability; no agent-facing
`request_update_meta_inputs` writes `flake.lock`, gated by its own path writes `flake.lock` any more — `request_update_meta_inputs` was
capability. removed, leaving the operator dashboard's `POST
/api/meta-update` as the only entry point.
- **Prompt/tools** — the system prompt uses `<!-- role:agent -->` / - **Prompt/tools** — the system prompt uses `<!-- role:agent -->` /
`<!-- role:manager -->` marker blocks, and a `Flavor::{Agent, `<!-- role:manager -->` marker blocks, and a `Flavor::{Agent,
Manager}` switch picks the MCP tool allow-list claude sees. Both are Manager}` switch picks the MCP tool allow-list claude sees. Both are

View file

@ -310,8 +310,8 @@ binary flavor.
| `meta` | `get_agent_meta` (`set_status` is always-on, see below) | | `meta` | `get_agent_meta` (`set_status` is always-on, see below) |
| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` |
| `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. |
| `lifecycle` | `list_containers` *(privileged)* | | `lifecycle` | none — `list_containers` no longer exists, with no replacement; the variant survives only so existing grants parse. |
| `approvals` | `request_update_meta_inputs` *(privileged)* | | `approvals` | none — `request_update_meta_inputs` no longer exists, with no replacement. Still a live server-side gate: `cancel_loose_end`'s approval-cancel arm requires it. |
| `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* | | `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* |
| `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) | | `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) |
| `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) | | `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) |
@ -391,7 +391,7 @@ bypass the operator approval gate.
`hive-sh4re/src/permissions.rs` + an arm to `as_str`. Add it to `Capability::ALL` `hive-sh4re/src/permissions.rs` + an arm to `as_str`. Add it to `Capability::ALL`
(the source of truth for the permissions UI columns). Implement the access (the source of truth for the permissions UI columns). Implement the access
check in the relevant handler (`hive-c0re/src/socket_server/mod.rs`, check in the relevant handler (`hive-c0re/src/socket_server/mod.rs`,
`hive-c0re/src/socket_server/lifecycle_handlers.rs`, `coordinator.rs`, or a `hive-c0re/src/socket_server/schedules.rs`, `coordinator.rs`, or a
handler under `hive-c0re/src/dashboard/`). handler under `hive-c0re/src/dashboard/`).
## Async forms ## Async forms

View file

@ -40,8 +40,6 @@ debug agent behavior.
- **[forge-cli](forge-cli.md)** — the exhaustive, autogenerated - **[forge-cli](forge-cli.md)** — the exhaustive, autogenerated
flag-by-flag reference for `hive-forge`, kept in lockstep with the flag-by-flag reference for `hive-forge`, kept in lockstep with the
binary by CI the same way `hivectl-cli.md` is. binary by CI the same way `hivectl-cli.md` is.
- **[lifecycle](lifecycle.md)** — listing the agents in a caller's own
subtree, plus the approval-gated config-change tools.
- **[matrix](matrix.md)** — the matrix MCP tool surface - **[matrix](matrix.md)** — the matrix MCP tool surface
(`mcp__matrix__*`) for agents with a matrix account, multiple (`mcp__matrix__*`) for agents with a matrix account, multiple
accounts per agent, and declaring extra MCP servers generally. accounts per agent, and declaring extra MCP servers generally.

View file

@ -1,58 +0,0 @@
# Lifecycle and approvals tools
Two tool groups govern agent lifecycle management and config changes.
The server scopes both to the caller's **own subtree** (topology-enforced
per `topology.json`: a child, a child's child, every agent below them —
plus the caller itself). No privileged class exists to belong to; the
root agent reaches every agent purely because the check is transitive and
everything sits under it.
## `lifecycle` tool group
No operator approval required. The caller's own subtree.
### `list_containers()`
List the caller's whole **subtree** with running status — children,
their children, every agent below them. The calling agent is part of its
own subtree, so it appears in its own listing; a leaf agent gets a
one-row answer naming itself.
## `approvals` tool group
Meta-flake input bumps route through the operator approval queue.
Creating a new agent is **not** in this group — agents have no tool for
it. The swarm controller's `InitAgentConfigRepo` job scaffolds a new
agent's config repo (`POST /api/agents`, see
`swarm-controller/`), and the operator spawns the container from the
dashboard (`◆ R3QU3ST SP4WN` / `Spawn` approval, routed via
`HostRequest::RequestSpawn`).
Config changes on an existing agent go through a **forge PR** on the
agent's `agent-configs/<name>` repo (queues a `MergeConfigPr` approval
on open/update — no MCP tool involved), not a tool call. See
`docs/agent-lifecycle/approvals.md`.
### `request_update_meta_inputs(inputs?, description?)`
Queue an approval to run `nix flake update [inputs...]` on the meta
flake. Pass specific input names (for example `["bitburner-agent"]`) or omit
/ pass `[]` for all inputs. Returns immediately; the lock update runs
on operator approval.
**Doesn't** trigger container rebuilds — the operator rebuilds affected
agents after the approval resolves.
## Boundary summary
| Operation | Requires approval? | Scope |
| ---------------------------- | ------------------ | ---------------------------- |
| `list_containers` | No | Own subtree, caller included |
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
## See also
- [`docs/agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md) — full approval flow, kinds,
helper events (`approval_resolved`), flake.lock
validation.

View file

@ -130,9 +130,13 @@ hive_name?, swarm_name?, matrix_accounts? }`. `matrix_accounts` is a
- **Subagent spawning** — headless claude sub-instances as background - **Subagent spawning** — headless claude sub-instances as background
tasks, shipped default-on like bash execution (no tool group gates it tasks, shipped default-on like bash execution (no tool group gates it
yet). See [`docs/tools/subagent.md`](../tools/subagent.md). yet). See [`docs/tools/subagent.md`](../tools/subagent.md).
- **Lifecycle + config** (`lifecycle`, `approvals`) — list the child - **Lifecycle + config** (`lifecycle`, `approvals`) — neither group
agents in your own subtree, apply config commits. See carries an MCP tool any more: `list_containers` and
[`docs/tools/lifecycle.md`](../tools/lifecycle.md). `request_update_meta_inputs` no longer exist, with no
replacement. `approvals` survives as a server-side gate on
`cancel_loose_end`'s approval-cancel arm; `lifecycle` gates nothing.
Config changes go through a forge PR on `agent-configs/<name>` — see
[`docs/agent-lifecycle/approvals.md`](../agent-lifecycle/approvals.md).
- **Scheduling** (`scheduling`) — scheduled prompts. See - **Scheduling** (`scheduling`) — scheduled prompts. See
[`docs/tools/scheduling.md`](../tools/scheduling.md). [`docs/tools/scheduling.md`](../tools/scheduling.md).
- **Forge repos** (`forge`) — `create_repo` — the only agent path to - **Forge repos** (`forge`) — `create_repo` — the only agent path to

View file

@ -131,17 +131,6 @@ pub struct CancelLooseEndArgs {
pub id: i64, pub id: i64,
} }
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct UpdateMetaInputsArgs {
/// Flake input names to update (e.g. `["bitburner-agent", "nixpkgs"]`).
/// Pass an empty list to update ALL inputs.
#[serde(default)]
pub inputs: Vec<String>,
/// Optional description shown on the dashboard approval card.
#[serde(default)]
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct RequestSchedulePromptArgs { pub struct RequestSchedulePromptArgs {
/// Recipient agents — one schedule fires to many inboxes at the /// Recipient agents — one schedule fires to many inboxes at the

View file

@ -26,7 +26,7 @@ mod render;
pub use args::{ pub use args::{
AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs, AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs,
EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, MarkTodosDoneArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, MarkTodosDoneArgs,
RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs, UpdateMetaInputsArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs,
}; };
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv}; pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
@ -547,44 +547,6 @@ impl AgentServer {
.await .await
} }
// IMPORTANT: this tool is only available when the `lifecycle` tool group
// is granted to this agent. Returns the calling agent's whole subtree,
// itself included, with running status.
#[tool(
description = "List this agent's whole subtree — children, their children, and so on \
down with running status. The calling agent is part of its own subtree, so it \
appears in the listing too. 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_core_agent_sock::Request::ListDescendants)
.await;
let body = match resp {
Ok(hive_core_agent_sock::Response::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")
}
}
other => reply_err(other, "list_containers"),
};
annotate_retries(body, retries)
})
.await
}
// IMPORTANT: this tool is capability-gated (`read_host_journal`). // IMPORTANT: this tool is capability-gated (`read_host_journal`).
// It is added to `--allowedTools` by `allowed_capability_tools` only // It is added to `--allowedTools` by `allowed_capability_tools` only
// when `HIVE_CAPABILITIES` contains `read_host_journal`. hive-c0re // when `HIVE_CAPABILITIES` contains `read_host_journal`. hive-c0re
@ -627,43 +589,6 @@ impl AgentServer {
.await .await
} }
#[tool(
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 the operator rebuilds affected agents \
after the approval resolves."
)]
async fn request_update_meta_inputs(
&self,
Parameters(args): Parameters<UpdateMetaInputsArgs>,
) -> String {
let log = format!("{args:?}");
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_core_agent_sock::Request::RequestUpdateMetaInputs {
inputs: args.inputs,
description: args.description,
})
.await;
annotate_retries(
format_ack(
resp,
"request_update_meta_inputs",
format!("approval queued: {label}"),
),
retries,
)
})
.await
}
#[tool( #[tool(
description = "Queue an approval to add a scheduled prompt — one body delivered to \ description = "Queue an approval to add a scheduled prompt — one body delivered to \
N agent inboxes at a target time, optionally recurring every `interval_seconds`. \ N agent inboxes at a target time, optionally recurring every `interval_seconds`. \

View file

@ -643,12 +643,12 @@ mod tests {
Ok(hive_core_agent_sock::Response::OkWarn { Ok(hive_core_agent_sock::Response::OkWarn {
warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()], warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()],
}), }),
"request_update_meta_inputs", "request_schedule_prompt",
"update_meta_inputs approval queued".to_owned(), "schedule approval queued".to_owned(),
); );
// The operation HAPPENED — dropping the success line would read as a // The operation HAPPENED — dropping the success line would read as a
// failure and invite a retry that queues a second approval. // failure and invite a retry that queues a second approval.
assert!(out.starts_with("update_meta_inputs approval queued")); assert!(out.starts_with("schedule approval queued"));
assert!(out.contains("⚠️ name is reserved")); assert!(out.contains("⚠️ name is reserved"));
assert!(out.contains("⚠️ second thing")); assert!(out.contains("⚠️ second thing"));
} }
@ -659,7 +659,7 @@ mod tests {
// warning marker would pass the test above. // warning marker would pass the test above.
let out = format_ack( let out = format_ack(
Ok(hive_core_agent_sock::Response::Ok), Ok(hive_core_agent_sock::Response::Ok),
"request_update_meta_inputs", "request_schedule_prompt",
"queued".to_owned(), "queued".to_owned(),
); );
assert_eq!(out, "queued"); assert_eq!(out, "queued");

View file

@ -4,8 +4,6 @@ Tools (hyperhive surface). Full signature + behavior for each comes from the too
- **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at. - **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at.
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time. - **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
- **Lifecycle** (_requires `lifecycle` tool group_, your own subtree — children, their children, and so on down, no approval needed): `list_containers`.
- **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_update_meta_inputs`.
- **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns). - **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns).
- **Diagnostics**: `get_host_journal` (_requires `read_host_journal` capability_). - **Diagnostics**: `get_host_journal` (_requires `read_host_journal` capability_).
@ -19,7 +17,7 @@ Messages from sender `system` are hyperhive helper events (JSON body, `event` fi
- `needs_update` — agent's flake rev is stale. Ask the operator to rebuild it. - `needs_update` — agent's flake rev is stale. Ask the operator to rebuild it.
- `container_crash` — ask the operator to start it again; if it keeps crashing, say so with what you saw. - `container_crash` — ask the operator to start it again; if it keeps crashing, say so with what you saw.
- `approval_resolved` — one of your own submitted approvals (`request_update_meta_inputs`, a scheduled prompt, a config PR, …) was approved, denied, or failed; the body carries the resolution. - `approval_resolved` — one of your own submitted approvals (a scheduled prompt, a config PR, …) was approved, denied, or failed; the body carries the resolution.
Lifecycle notices that don't need an immediate turn — a new agent spawned, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them. Lifecycle notices that don't need an immediate turn — a new agent spawned, a container rebuilt/killed/destroyed, or its login state changing — surface as todos instead of messages now. Call `get_loose_ends` to see them.

View file

@ -498,8 +498,7 @@ fn tool_icon(name: &str) -> &'static str {
"mcp__hyperhive__cancel_loose_end" => "✂️", "mcp__hyperhive__cancel_loose_end" => "✂️",
"mcp__hyperhive__ack_until" => "", "mcp__hyperhive__ack_until" => "",
"mcp__hyperhive__get_agent_meta" => "", "mcp__hyperhive__get_agent_meta" => "",
"mcp__hyperhive__list_containers" "mcp__matrix__list_rooms"
| "mcp__matrix__list_rooms"
| "mcp__matrix__list_room_members" | "mcp__matrix__list_room_members"
| "mcp__matrix__list_invites" => "📋", | "mcp__matrix__list_invites" => "📋",
"mcp__hyperhive__get_host_journal" => "📜", "mcp__hyperhive__get_host_journal" => "📜",
@ -666,8 +665,7 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String {
} }
} }
"mcp__hyperhive__remind" => fmt_hyperhive_remind(short, input), "mcp__hyperhive__remind" => fmt_hyperhive_remind(short, input),
"mcp__hyperhive__request_update_meta_inputs" "mcp__hyperhive__list_schedules"
| "mcp__hyperhive__list_schedules"
| "mcp__hyperhive__cancel_schedule" | "mcp__hyperhive__cancel_schedule"
| "mcp__hyperhive__fire_schedule_now" | "mcp__hyperhive__fire_schedule_now"
| "mcp__hyperhive__edit_schedule" | "mcp__hyperhive__edit_schedule"
@ -792,17 +790,6 @@ fn fmt_hyperhive_edit_schedule(short: &str, input: &Value) -> String {
/// Schedule-management hyperhive tools (list/cancel/fire/edit/request). /// Schedule-management hyperhive tools (list/cancel/fire/edit/request).
fn fmt_hyperhive_schedule_tool(name: &str, short: &str, input: &Value) -> String { fn fmt_hyperhive_schedule_tool(name: &str, short: &str, input: &Value) -> String {
match name { match name {
"mcp__hyperhive__request_update_meta_inputs" => {
let ins = match input.get("inputs").and_then(Value::as_array) {
Some(arr) if !arr.is_empty() => {
let names: Vec<&str> = arr.iter().filter_map(Value::as_str).take(4).collect();
let tail = if arr.len() > 4 { ", …" } else { "" };
format!("[{}{}]", names.join(", "), tail)
}
_ => "all".to_owned(),
};
format!("{short} {ins}")
}
"mcp__hyperhive__list_schedules" => format!("{short}()"), "mcp__hyperhive__list_schedules" => format!("{short}()"),
"mcp__hyperhive__cancel_schedule" => { "mcp__hyperhive__cancel_schedule" => {
let id = input let id = input

View file

@ -892,8 +892,8 @@ async fn run_deploy_tail(
/// otherwise just the agents named by `agent-<name>` inputs. /// otherwise just the agents named by `agent-<name>` inputs.
/// Topology-sorted so parents rebuild before their children. /// Topology-sorted so parents rebuild before their children.
/// ///
/// `inputs` is the caller-supplied flake-input-name list /// `inputs` is the caller-supplied flake-input-name list (the dashboard's
/// (`RequestUpdateMetaInputs`'s `inputs` field, operator-approved but not /// `POST /api/meta-update` form field, operator-supplied but not
/// otherwise validated) — the `agent-<name>` branch parses agent names /// otherwise validated) — the `agent-<name>` branch parses agent names
/// straight out of it, so each is validated through [`hive_types::Ident`] /// straight out of it, so each is validated through [`hive_types::Ident`]
/// before it ever reaches a filesystem path or a forge URL built from a /// before it ever reaches a filesystem path or a forge URL built from a

View file

@ -1,5 +1,4 @@
//! Config-approval request handlers: `RequestUpdateMetaInputs`, plus the //! Config-approval submit helper `submit_merge_config_pr`.
//! shared submit helper `submit_merge_config_pr`.
//! //!
//! `submit_merge_config_pr` is called from the dashboard webhook handler //! `submit_merge_config_pr` is called from the dashboard webhook handler
//! (`dashboard::webhook`) — agents no longer need an MCP tool for config //! (`dashboard::webhook`) — agents no longer need an MCP tool for config
@ -8,57 +7,8 @@
use std::sync::Arc; use std::sync::Arc;
use hive_core_agent_sock::Response;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
/// `RequestUpdateMetaInputs` — queue an `UpdateMetaInputs` approval
/// carrying the JSON-encoded input list in `commit_ref` (no git commit
/// is involved; the field is the payload the approval handler decodes).
pub(super) fn handle_request_update_meta_inputs(
coord: &Arc<Coordinator>,
requester: &str,
inputs: &[String],
description: Option<&str>,
) -> Response {
let label = if inputs.is_empty() {
"all inputs".to_string()
} else {
inputs.join(", ")
};
tracing::info!(%requester, %label, "request_update_meta_inputs");
let commit_ref = serde_json::to_string(inputs).unwrap_or_default();
let id = match coord
.approvals
.submit_kind(
requester,
hive_sh4re::approvals::ApprovalKind::UpdateMetaInputs,
&commit_ref,
description,
requester,
None,
)
.map_err(|e| anyhow::anyhow!("{e:#}"))
{
Ok(id) => id,
Err(e) => {
return Response::Err {
message: format!("queue update_meta_inputs approval: {e:#}"),
};
}
};
tracing::info!(%id, %label, "update_meta_inputs approval queued");
coord.emit_approval_added(crate::coordinator::ApprovalAdded {
id,
agent: requester,
approval_kind: "update_meta_inputs",
sha_short: None,
description: description.map(str::to_owned),
pr_number: None,
});
Response::Ok
}
/// Submit-time half of the PR-merge flow: fetch the PR head sha from the /// Submit-time half of the PR-merge flow: fetch the PR head sha from the
/// forge, queue the approval row, and emit the `approval_added` event so the /// forge, queue the approval row, and emit the `approval_added` event so the
/// dashboard shows the pending card immediately. /// dashboard shows the pending card immediately.

View file

@ -1,48 +0,0 @@
//! `ListDescendants` request handler — the `lifecycle` tool group's
//! remaining verb, a read of the caller's own subtree.
use std::sync::Arc;
use hive_core_agent_sock::Response;
use crate::coordinator::Coordinator;
/// `ListDescendants` — every topological descendant of `agent` with
/// its running/stopped state, parents before children.
pub(super) async fn handle_list_descendants(coord: &Arc<Coordinator>, agent: &str) -> Response {
tracing::debug!(%agent, "agent: list descendants");
// 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);
// Read from the coordinator's cached container snapshot instead of
// live-querying each container's systemd unit state — the same
// `containers_snapshot()` the dashboard's `/api/state` cold-load path
// already uses, kept fresh by `rescan_containers_and_emit()` on every
// mutation plus the crash-watcher's periodic poll. Avoids N
// `systemctl is-active` subprocess spawns per `list_containers` call;
// per mara, daemons should do the expensive work themselves and serve
// clients a cheap cached read.
let snapshot = coord.containers_snapshot().await;
let running_by_name: std::collections::HashMap<&str, bool> = snapshot
.iter()
.map(|v| (v.name.as_str(), v.running))
.collect();
let containers = names
.into_iter()
.map(|name| {
// A descendant absent from the snapshot (not yet scanned since
// its own registration, e.g. mid-spawn) reads as not running
// rather than erroring — matches the old membership-check's
// default-false behavior for an unknown name.
let running = running_by_name.get(name.as_str()).copied().unwrap_or(false);
hive_sh4re::container::ContainerInfo { name, running }
})
.collect();
Response::Containers { containers }
}

View file

@ -23,15 +23,12 @@ use tokio::task::JoinHandle;
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
mod config_approvals; mod config_approvals;
mod lifecycle_handlers;
mod schedules; mod schedules;
pub(crate) use config_approvals::submit_merge_config_pr; pub(crate) use config_approvals::submit_merge_config_pr;
pub(crate) use schedules::filter_ghost_schedule_targets; pub(crate) use schedules::filter_ghost_schedule_targets;
pub use schedules::schedule_to_wire_public; pub use schedules::schedule_to_wire_public;
use config_approvals::handle_request_update_meta_inputs;
use lifecycle_handlers::handle_list_descendants;
use schedules::{ use schedules::{
EditSchedulePatch, handle_cancel_schedule, handle_edit_schedule, handle_fire_schedule_now, EditSchedulePatch, handle_cancel_schedule, handle_edit_schedule, handle_fire_schedule_now,
handle_list_schedules, handle_request_schedule_prompt, handle_list_schedules, handle_request_schedule_prompt,
@ -181,9 +178,9 @@ pub(crate) fn recv_timeout(wait_seconds: Option<u64>) -> std::time::Duration {
/// Handle the subset of `Request` variants that are identical on both /// Handle the subset of `Request` variants that are identical on both
/// the agent socket and the manager socket. Returns `Some(response)` for /// the agent socket and the manager socket. Returns `Some(response)` for
/// every variant it handles; returns `None` for the remaining variants — /// every variant it handles; returns `None` for the remaining variants —
/// `ListDescendants` and the orchestration verbs (schedules / meta-inputs), /// `GetLooseEnds` and the orchestration verbs (schedules), which need
/// which need per-verb tool-group gating — or for host-admin / unknown /// per-verb tool-group gating — or for host-admin / unknown requests
/// requests invalid on either socket. /// invalid on either socket.
/// ///
/// The unified `dispatch` calls this first; the remaining arms (which gate /// The unified `dispatch` calls this first; the remaining arms (which gate
/// on topology / capabilities / tool-groups) are handled there. /// on topology / capabilities / tool-groups) are handled there.
@ -542,14 +539,13 @@ fn handle_requeue_inflight(
/// Unified dispatch for every socket connection — per-agent sockets and the /// Unified dispatch for every socket connection — per-agent sockets and the
/// (now pure-transport) manager socket alike. There is no privilege bit; /// (now pure-transport) manager socket alike. There is no privilege bit;
/// authority derives uniformly from the caller's identity: hive-wide /// authority derives uniformly from the caller's identity: hive-wide
/// orchestration verbs (schedules / meta-inputs) require the matching /// orchestration verbs (schedules) require the matching tool-group (the
/// tool-group (the grantable capability). /// grantable capability).
async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response { async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
if let Some(resp) = dispatch_shared(req, agent, coord).await { if let Some(resp) = dispatch_shared(req, agent, coord).await {
return resp; return resp;
} }
match req { match req {
Request::ListDescendants => handle_list_descendants(coord, agent).await,
Request::GetLooseEnds => handle_get_loose_ends(coord, agent), Request::GetLooseEnds => handle_get_loose_ends(coord, agent),
// Orchestration verbs — gated per-verb on tool-group membership // Orchestration verbs — gated per-verb on tool-group membership
// (see `dispatch_orchestration`). // (see `dispatch_orchestration`).
@ -557,21 +553,12 @@ async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Respo
} }
} }
/// Handle the hive-wide orchestration verbs (scheduling, meta-input updates). /// Handle the hive-wide orchestration verbs (scheduling). No blanket socket
/// No blanket socket gate: each verb gates on the grantable capability that /// gate: each verb gates on the grantable capability that authorises it —
/// authorises it — the matching tool-group (`scheduling` / `approvals`). Any /// the matching tool-group (`scheduling`). Any other variant is a
/// other variant is a host-admin / unknown request invalid on either socket. /// host-admin / unknown request invalid on either socket.
async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response { async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
match req { match req {
Request::RequestUpdateMetaInputs {
inputs,
description,
} => {
if let Some(err) = require_group(agent, "approvals", "request update_meta_inputs") {
return err;
}
handle_request_update_meta_inputs(coord, agent, inputs, description.as_deref())
}
Request::RequestSchedulePrompt(payload) => { Request::RequestSchedulePrompt(payload) => {
if let Some(err) = require_group(agent, "scheduling", "schedule a prompt") { if let Some(err) = require_group(agent, "scheduling", "schedule a prompt") {
return err; return err;

View file

@ -1,7 +1,11 @@
//! Approval queue. Requests are submitted by the manager //! Approval queue. Requests are submitted by an agent
//! (`RequestUpdateMetaInputs`), the config-PR webhook (`MergeConfigPr`), or //! (`RequestSchedulePrompt`), the config-PR webhook (`MergeConfigPr`), or
//! the operator (`Spawn`); the user approves/denies via the host admin CLI; //! the operator (`Spawn`); the user approves/denies via the host admin CLI;
//! on approval the host runs the corresponding action. //! on approval the host runs the corresponding action.
//!
//! `UpdateMetaInputs` rows are legacy: the MCP tool that queued them was
//! removed and nothing produces the kind any more. The variant and
//! its (de)serialization stay so pre-existing rows still read back.
use std::path::Path; use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;

View file

@ -7,7 +7,7 @@
//! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …) //! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …)
//! stay in `hive-sh4re`, which this crate depends on. //! stay in `hive-sh4re`, which this crate depends on.
use hive_sh4re::container::{ContainerInfo, MatrixIdentity}; use hive_sh4re::container::MatrixIdentity;
use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd}; use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd};
use hive_sh4re::journal::JournalPriority; use hive_sh4re::journal::JournalPriority;
use hive_sh4re::manager::SchedulePromptPayload; use hive_sh4re::manager::SchedulePromptPayload;
@ -149,13 +149,6 @@ pub enum Request {
}, },
// ---- privileged (manager socket only for now) --------------------------- // ---- privileged (manager socket only for now) ---------------------------
/// *(privileged)* Queue an approval to run `nix flake update [inputs...]`.
RequestUpdateMetaInputs {
#[serde(default)]
inputs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
/// *(privileged)* Queue an approval to add a scheduled prompt. /// *(privileged)* Queue an approval to add a scheduled prompt.
RequestSchedulePrompt(SchedulePromptPayload), RequestSchedulePrompt(SchedulePromptPayload),
/// *(privileged)* Cancel a scheduled prompt. /// *(privileged)* Cancel a scheduled prompt.
@ -168,12 +161,6 @@ pub enum Request {
/// plus any owned by an agent in its subtree (everything, for the /// plus any owned by an agent in its subtree (everything, for the
/// operator). /// operator).
ListSchedules, ListSchedules,
/// List the calling agent's subtree — children, their children, and so
/// on down, plus the caller itself, which is part of its own subtree.
/// Gated by the `lifecycle` tool group. The result includes every known
/// member regardless of whether the container is currently
/// running — use `running` to distinguish.
ListDescendants,
/// *(privileged)* Fire a scheduled prompt out of band immediately. /// *(privileged)* Fire a scheduled prompt out of band immediately.
FireScheduleNow { id: i64 }, FireScheduleNow { id: i64 },
/// *(privileged)* Edit an existing schedule's mutable fields. /// *(privileged)* Edit an existing schedule's mutable fields.
@ -276,10 +263,6 @@ pub enum Response {
full_name: String, full_name: String,
clone_url: String, clone_url: String,
}, },
/// `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> },
/// `Recv` result when a graceful stop is pending for this agent /// `Recv` result when a graceful stop is pending for this agent
/// (set by hive-c0re's `GracefulStop` orchestration). Returned in /// (set by hive-c0re's `GracefulStop` orchestration). Returned in
/// place of `Messages` — it doubles as the inbound fence: the harness /// place of `Messages` — it doubles as the inbound fence: the harness

View file

@ -1,19 +1,10 @@
//! Container/agent-roster wire shapes: what `ListDescendants` and //! Container/agent-roster wire shapes: what `HostRequest::AgentStatus`
//! `HostRequest::AgentStatus` return, plus the per-account matrix //! returns, plus the per-account matrix identity shape surfaced by
//! identity shape surfaced by `GetAgentMeta`. //! `GetAgentMeta`.
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// 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,
}
/// One row in a `HostRequest::AgentStatus` result — the operator-CLI /// One row in a `HostRequest::AgentStatus` result — the operator-CLI
/// projection of the dashboard's per-agent `ContainerView`. Carries the /// projection of the dashboard's per-agent `ContainerView`. Carries the
/// agent's running/health flags plus the technical state an operator /// agent's running/health flags plus the technical state an operator

View file

@ -152,9 +152,15 @@ pub enum ToolGroup {
Meta, Meta,
/// `get_loose_ends`, `cancel_loose_end`, `remind` /// `get_loose_ends`, `cancel_loose_end`, `remind`
Inbox, Inbox,
/// `list_containers` - *(privileged)* /// Gates no tool today — `list_containers`, its only member, was
/// removed with no replacement. Kept so existing
/// `meta/capabilities.json` grants still parse; `tools()` returns
/// `&[]`.
Lifecycle, Lifecycle,
/// `request_update_meta_inputs` - *(privileged)* /// Grants no MCP tool today — `request_update_meta_inputs`, its only
/// member, was removed with no replacement. Still a live
/// server-side gate: `cancel_loose_end`'s approval-cancel arm
/// requires it (`socket_server::check_can_cancel_approval`).
Approvals, Approvals,
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, /// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
/// `edit_schedule`, `list_schedules` - *(privileged)* /// `edit_schedule`, `list_schedules` - *(privileged)*
@ -215,16 +221,15 @@ impl ToolGroup {
} }
/// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group. /// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group.
/// Returns `&[]` for `WebTools` — it enables Claude built-in tools, /// Returns `&[]` for `WebTools` (it enables Claude built-in tools, not MCP
/// not MCP tools; see `builtin_tools()`. /// tools — see `builtin_tools()`) and for `Lifecycle` / `Approvals`
/// (their tools were removed); see each variant's doc comment.
#[must_use] #[must_use]
pub fn tools(self) -> &'static [&'static str] { pub fn tools(self) -> &'static [&'static str] {
match self { match self {
Self::Messaging => &["send", "recv", "ack_until"], Self::Messaging => &["send", "recv", "ack_until"],
Self::Meta => &["get_agent_meta"], Self::Meta => &["get_agent_meta"],
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
Self::Lifecycle => &["list_containers"],
Self::Approvals => &["request_update_meta_inputs"],
Self::Scheduling => &[ Self::Scheduling => &[
"request_schedule_prompt", "request_schedule_prompt",
"fire_schedule_now", "fire_schedule_now",
@ -233,16 +238,22 @@ impl ToolGroup {
"list_schedules", "list_schedules",
], ],
Self::Forge => &["create_repo"], Self::Forge => &["create_repo"],
// Both empty, for different reasons — see each variant's own // All four empty, for four different reasons — see each
// doc comment above. `Execution` grants the out-of-process // variant's own doc comment above. `Execution` grants the
// `bash` MCP server (`mcp__bash__run`/`status`/`kill`), gated // out-of-process `bash` MCP server
// at config-render time by `extra_server_required_group` in // (`mcp__bash__run`/`status`/`kill`), gated at config-render
// time by `extra_server_required_group` in
// `hive-agent/src/mcp_config.rs`, not by this list — an // `hive-agent/src/mcp_config.rs`, not by this list — an
// out-of-process server has no later enforcement point, so // out-of-process server has no later enforcement point, so
// that gate is the actual security boundary. `WebTools` // that gate is the actual security boundary. `WebTools`
// grants Claude built-in tools, not MCP ones; see // grants Claude built-in tools, not MCP ones; see
// `builtin_tools()`. // `builtin_tools()`. `Lifecycle` and `Approvals` each listed
Self::Execution | Self::WebTools => &[], // exactly one tool — `list_containers` and
// `request_update_meta_inputs` respectively — and both tools
// were removed outright; the variants stay so existing
// grants parse, and `Approvals` still gates
// `cancel_loose_end`'s approval-cancel arm server-side.
Self::Lifecycle | Self::Approvals | Self::Execution | Self::WebTools => &[],
} }
} }
@ -324,9 +335,9 @@ impl ToolGroup {
"get_agent_meta — identity introspection (set_status is always available)" "get_agent_meta — identity introspection (set_status is always available)"
} }
Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling", Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling",
Self::Lifecycle => "list_containers — own-subtree container listing (privileged)", Self::Lifecycle => "no tools — vestigial since list_containers was removed",
Self::Approvals => { Self::Approvals => {
"request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)" "no tools — grants cancel_loose_end's approval-cancel arm (privileged)"
} }
Self::Scheduling => { Self::Scheduling => {
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)" "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"