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 |
| 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 |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
"Ancestor" walks `ContainerView.parent` chains; a visited-set guards against
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.
Topology-wise, `ruth` is still just another root agent.
- **Wire-protocol** — the privileged `Request` variants
(`Kill` / `Start` / `Restart` / `Update`;
`GetLogs`; `RequestUpdateMetaInputs`) — marked `*(privileged)*` in
`hive-core-agent-sock`'s unified `Request` enum — are reachable only
from the manager's socket flavour today. Planned rule for each is in the
table above ("any ancestor" for lifecycle/logs);
`RequestUpdateMetaInputs` stays
a root-only capability even post-milestone, not a topology rule.
One exception: `Wake` (inject a `from: <X>` message into the
(`Kill` / `Start` / `Restart` / `Update`; `GetLogs`) — marked
`*(privileged)*` in `hive-core-agent-sock`'s unified `Request` enum —
are reachable only from the manager's socket flavour today. Planned
rule for each is in the table above ("any ancestor" for
lifecycle/logs). One exception: `Wake` (inject a `from: <X>` message into the
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
equivalent on their own socket.
@ -135,9 +131,10 @@ other agents don't:
deploy log). Planned: each agent gets RW to `/agents/<descendant>/`
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
gate RO `/meta` access on a "meta read" capability; only
`request_update_meta_inputs` writes `flake.lock`, gated by its own
capability.
gate RO `/meta` access on a "meta read" capability; no agent-facing
path writes `flake.lock` any more — `request_update_meta_inputs` was
removed, leaving the operator dashboard's `POST
/api/meta-update` as the only entry point.
- **Prompt/tools** — the system prompt uses `<!-- role:agent -->` /
`<!-- role:manager -->` marker blocks, and a `Flavor::{Agent,
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) |
| `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`. |
| `lifecycle` | `list_containers` *(privileged)* |
| `approvals` | `request_update_meta_inputs` *(privileged)* |
| `lifecycle` | none — `list_containers` no longer exists, with no replacement; the variant survives only so existing grants parse. |
| `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)* |
| `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) |
@ -391,7 +391,7 @@ bypass the operator approval gate.
`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
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/`).
## Async forms

View file

@ -40,8 +40,6 @@ debug agent behavior.
- **[forge-cli](forge-cli.md)** — the exhaustive, autogenerated
flag-by-flag reference for `hive-forge`, kept in lockstep with the
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
(`mcp__matrix__*`) for agents with a matrix account, multiple
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
tasks, shipped default-on like bash execution (no tool group gates it
yet). See [`docs/tools/subagent.md`](../tools/subagent.md).
- **Lifecycle + config** (`lifecycle`, `approvals`) — list the child
agents in your own subtree, apply config commits. See
[`docs/tools/lifecycle.md`](../tools/lifecycle.md).
- **Lifecycle + config** (`lifecycle`, `approvals`) — neither group
carries an MCP tool any more: `list_containers` and
`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
[`docs/tools/scheduling.md`](../tools/scheduling.md).
- **Forge repos** (`forge`) — `create_repo` — the only agent path to

View file

@ -131,17 +131,6 @@ pub struct CancelLooseEndArgs {
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)]
pub struct RequestSchedulePromptArgs {
/// Recipient agents — one schedule fires to many inboxes at the

View file

@ -26,7 +26,7 @@ mod render;
pub use args::{
AckUntilArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs,
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};
@ -547,44 +547,6 @@ impl AgentServer {
.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`).
// It is added to `--allowedTools` by `allowed_capability_tools` only
// when `HIVE_CAPABILITIES` contains `read_host_journal`. hive-c0re
@ -627,43 +589,6 @@ impl AgentServer {
.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(
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`. \

View file

@ -643,12 +643,12 @@ mod tests {
Ok(hive_core_agent_sock::Response::OkWarn {
warnings: vec!["name is reserved".to_owned(), "second thing".to_owned()],
}),
"request_update_meta_inputs",
"update_meta_inputs approval queued".to_owned(),
"request_schedule_prompt",
"schedule approval queued".to_owned(),
);
// The operation HAPPENED — dropping the success line would read as a
// 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("⚠️ second thing"));
}
@ -659,7 +659,7 @@ mod tests {
// warning marker would pass the test above.
let out = format_ack(
Ok(hive_core_agent_sock::Response::Ok),
"request_update_meta_inputs",
"request_schedule_prompt",
"queued".to_owned(),
);
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.
- **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).
- **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.
- `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.

View file

@ -498,8 +498,7 @@ fn tool_icon(name: &str) -> &'static str {
"mcp__hyperhive__cancel_loose_end" => "✂️",
"mcp__hyperhive__ack_until" => "",
"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_invites" => "📋",
"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__request_update_meta_inputs"
| "mcp__hyperhive__list_schedules"
"mcp__hyperhive__list_schedules"
| "mcp__hyperhive__cancel_schedule"
| "mcp__hyperhive__fire_schedule_now"
| "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).
fn fmt_hyperhive_schedule_tool(name: &str, short: &str, input: &Value) -> String {
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__cancel_schedule" => {
let id = input

View file

@ -892,8 +892,8 @@ async fn run_deploy_tail(
/// otherwise just the agents named by `agent-<name>` inputs.
/// Topology-sorted so parents rebuild before their children.
///
/// `inputs` is the caller-supplied flake-input-name list
/// (`RequestUpdateMetaInputs`'s `inputs` field, operator-approved but not
/// `inputs` is the caller-supplied flake-input-name list (the dashboard's
/// `POST /api/meta-update` form field, operator-supplied but not
/// otherwise validated) — the `agent-<name>` branch parses agent names
/// 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

View file

@ -1,5 +1,4 @@
//! Config-approval request handlers: `RequestUpdateMetaInputs`, plus the
//! shared submit helper `submit_merge_config_pr`.
//! Config-approval submit helper `submit_merge_config_pr`.
//!
//! `submit_merge_config_pr` is called from the dashboard webhook handler
//! (`dashboard::webhook`) — agents no longer need an MCP tool for config
@ -8,57 +7,8 @@
use std::sync::Arc;
use hive_core_agent_sock::Response;
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
/// forge, queue the approval row, and emit the `approval_added` event so the
/// 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;
mod config_approvals;
mod lifecycle_handlers;
mod schedules;
pub(crate) use config_approvals::submit_merge_config_pr;
pub(crate) use schedules::filter_ghost_schedule_targets;
pub use schedules::schedule_to_wire_public;
use config_approvals::handle_request_update_meta_inputs;
use lifecycle_handlers::handle_list_descendants;
use schedules::{
EditSchedulePatch, handle_cancel_schedule, handle_edit_schedule, handle_fire_schedule_now,
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
/// the agent socket and the manager socket. Returns `Some(response)` for
/// every variant it handles; returns `None` for the remaining variants —
/// `ListDescendants` and the orchestration verbs (schedules / meta-inputs),
/// which need per-verb tool-group gating — or for host-admin / unknown
/// requests invalid on either socket.
/// `GetLooseEnds` and the orchestration verbs (schedules), which need
/// per-verb tool-group gating — or for host-admin / unknown requests
/// invalid on either socket.
///
/// The unified `dispatch` calls this first; the remaining arms (which gate
/// 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
/// (now pure-transport) manager socket alike. There is no privilege bit;
/// authority derives uniformly from the caller's identity: hive-wide
/// orchestration verbs (schedules / meta-inputs) require the matching
/// tool-group (the grantable capability).
/// orchestration verbs (schedules) require the matching tool-group (the
/// grantable capability).
async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
if let Some(resp) = dispatch_shared(req, agent, coord).await {
return resp;
}
match req {
Request::ListDescendants => handle_list_descendants(coord, agent).await,
Request::GetLooseEnds => handle_get_loose_ends(coord, agent),
// Orchestration verbs — gated per-verb on tool-group membership
// (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).
/// No blanket socket gate: each verb gates on the grantable capability that
/// authorises it — the matching tool-group (`scheduling` / `approvals`). Any
/// other variant is a host-admin / unknown request invalid on either socket.
/// Handle the hive-wide orchestration verbs (scheduling). No blanket socket
/// gate: each verb gates on the grantable capability that authorises it —
/// the matching tool-group (`scheduling`). Any other variant is a
/// host-admin / unknown request invalid on either socket.
async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
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) => {
if let Some(err) = require_group(agent, "scheduling", "schedule a prompt") {
return err;

View file

@ -1,7 +1,11 @@
//! Approval queue. Requests are submitted by the manager
//! (`RequestUpdateMetaInputs`), the config-PR webhook (`MergeConfigPr`), or
//! Approval queue. Requests are submitted by an agent
//! (`RequestSchedulePrompt`), the config-PR webhook (`MergeConfigPr`), or
//! the operator (`Spawn`); the user approves/denies via the host admin CLI;
//! 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::sync::Mutex;

View file

@ -7,7 +7,7 @@
//! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …)
//! 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::journal::JournalPriority;
use hive_sh4re::manager::SchedulePromptPayload;
@ -149,13 +149,6 @@ pub enum Request {
},
// ---- 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.
RequestSchedulePrompt(SchedulePromptPayload),
/// *(privileged)* Cancel a scheduled prompt.
@ -168,12 +161,6 @@ pub enum Request {
/// plus any owned by an agent in its subtree (everything, for the
/// operator).
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.
FireScheduleNow { id: i64 },
/// *(privileged)* Edit an existing schedule's mutable fields.
@ -276,10 +263,6 @@ pub enum Response {
full_name: 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
/// (set by hive-c0re's `GracefulStop` orchestration). Returned in
/// 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
//! `HostRequest::AgentStatus` return, plus the per-account matrix
//! identity shape surfaced by `GetAgentMeta`.
//! Container/agent-roster wire shapes: what `HostRequest::AgentStatus`
//! returns, plus the per-account matrix identity shape surfaced by
//! `GetAgentMeta`.
use chrono::{DateTime, Utc};
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
/// projection of the dashboard's per-agent `ContainerView`. Carries the
/// agent's running/health flags plus the technical state an operator

View file

@ -152,9 +152,15 @@ pub enum ToolGroup {
Meta,
/// `get_loose_ends`, `cancel_loose_end`, `remind`
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,
/// `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,
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
/// `edit_schedule`, `list_schedules` - *(privileged)*
@ -215,16 +221,15 @@ impl ToolGroup {
}
/// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group.
/// Returns `&[]` for `WebTools` — it enables Claude built-in tools,
/// not MCP tools; see `builtin_tools()`.
/// Returns `&[]` for `WebTools` (it enables Claude built-in tools, not MCP
/// tools — see `builtin_tools()`) and for `Lifecycle` / `Approvals`
/// (their tools were removed); see each variant's doc comment.
#[must_use]
pub fn tools(self) -> &'static [&'static str] {
match self {
Self::Messaging => &["send", "recv", "ack_until"],
Self::Meta => &["get_agent_meta"],
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
Self::Lifecycle => &["list_containers"],
Self::Approvals => &["request_update_meta_inputs"],
Self::Scheduling => &[
"request_schedule_prompt",
"fire_schedule_now",
@ -233,16 +238,22 @@ impl ToolGroup {
"list_schedules",
],
Self::Forge => &["create_repo"],
// Both empty, for different reasons — see each variant's own
// doc comment above. `Execution` grants the out-of-process
// `bash` MCP server (`mcp__bash__run`/`status`/`kill`), gated
// at config-render time by `extra_server_required_group` in
// All four empty, for four different reasons — see each
// variant's own doc comment above. `Execution` grants the
// out-of-process `bash` MCP server
// (`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
// out-of-process server has no later enforcement point, so
// that gate is the actual security boundary. `WebTools`
// grants Claude built-in tools, not MCP ones; see
// `builtin_tools()`.
Self::Execution | Self::WebTools => &[],
// `builtin_tools()`. `Lifecycle` and `Approvals` each listed
// 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)"
}
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 => {
"request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)"
"no tools — grants cancel_loose_end's approval-cancel arm (privileged)"
}
Self::Scheduling => {
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"