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

@ -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;