mcp: remove the restart/kill/start/update/get_logs agent verbs
Container lifecycle from inside an agent goes away: an agent no longer starts, stops, restarts or rebuilds a container in its subtree, and no longer reads another container's journal. Those are operator actions — the dashboard and hivectl keep their own paths to the same job-queue and hive-priv plumbing, which is why none of that machinery is removed here, only the five MCP verbs and what they alone reached. What went with them: the `Request` variants and `Response::Logs` on the agent socket, the five tool definitions and their arg structs, the four lifecycle handlers plus `handle_get_logs`, and `require_descendant` — the topology guard those five were the only remaining callers of. `ToolGroup::Diagnostics` goes too: `get_logs` was its only tool, so it would otherwise be a grantable group that grants nothing. `lifecycle` stays, now carrying `list_containers` alone. An agent that gets a `needs_update` or `container_crash` helper event has no remedy of its own left, so the system prompt and the docs now send it to the operator instead of to a tool that no longer exists. Refs #4480
This commit is contained in:
parent
c5f60fd58f
commit
87970a8c93
17 changed files with 48 additions and 450 deletions
|
|
@ -107,10 +107,7 @@ pub(super) async fn post_kill(
|
|||
// submitted by other sub-agents still process through the
|
||||
// host-side approval queue without the manager up, and
|
||||
// operator-driven meta-input updates work from the dashboard
|
||||
// either way. The MCP-surface self-kill guard in
|
||||
// `socket_server.rs::Request::Kill` stays in place: a
|
||||
// manager calling Kill on its own container is self-suicide
|
||||
// mid-call, not a legitimate operator action.
|
||||
// either way.
|
||||
if let Err(e) =
|
||||
crate::job_queue::power::stop_many(&state.coord, std::slice::from_ref(&logical), false)
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -1,97 +1,12 @@
|
|||
//! Container-lifecycle request handlers (`Start` / `Restart` / `Kill` /
|
||||
//! `Update` / `ListDescendants`). All are topology-guarded via
|
||||
//! `super::require_descendant`.
|
||||
//! `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 super::require_descendant;
|
||||
use crate::coordinator::Coordinator;
|
||||
|
||||
/// `Start` — start a container, kicking its next turn. The caller must be an
|
||||
/// ancestor of `name` in the topology (the root covers every agent).
|
||||
pub(super) async fn handle_start(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
|
||||
if let Some(err) = require_descendant(agent, name, "start") {
|
||||
return err;
|
||||
}
|
||||
tracing::info!(%agent, %name, "start container");
|
||||
// Persist `wanted = Up` and submit the Start DAG; the submit layer
|
||||
// upgrades a stale-rev start to a full rebuild so the container
|
||||
// runs current nix derivations before it starts.
|
||||
if let Err(e) = crate::job_queue::power::start_many(coord, &[name.to_owned()]).await {
|
||||
tracing::error!(%agent, %name, error = ?e, "start: insert failed");
|
||||
}
|
||||
Response::Ok
|
||||
}
|
||||
|
||||
/// `Restart` — enqueue a restart for a container. The caller must be an
|
||||
/// ancestor of `name` in the topology. Agents have no infra-container
|
||||
/// restart path: an infra name here just falls through to the topology
|
||||
/// guard like any other non-descendant name.
|
||||
pub(super) async fn handle_restart(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
|
||||
if let Some(err) = require_descendant(agent, name, "restart") {
|
||||
return err;
|
||||
}
|
||||
tracing::info!(%agent, %name, "submit restart");
|
||||
if let Err(e) = crate::job_queue::power::restart_many(coord, &[name.to_owned()], false).await {
|
||||
tracing::error!(%agent, %name, error = ?e, "restart: insert failed");
|
||||
}
|
||||
Response::Ok
|
||||
}
|
||||
|
||||
/// `Kill` — kill a container, unregister it, notify the swarm. The caller
|
||||
/// must be an ancestor of `name` in the topology.
|
||||
pub(super) async fn handle_kill(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
|
||||
if let Some(err) = require_descendant(agent, name, "kill") {
|
||||
return err;
|
||||
}
|
||||
tracing::info!(%agent, %name, "kill container");
|
||||
// Persist the intent even if the kill fails — otherwise the next
|
||||
// reconcile would restart the container.
|
||||
if let Err(e) = coord.power.set(name, crate::power::Wanted::Offline) {
|
||||
tracing::warn!(%name, error = ?e, "agent_power: set wanted=offline failed");
|
||||
}
|
||||
let result: anyhow::Result<()> = async {
|
||||
crate::lifecycle::kill(name).await?;
|
||||
coord.unregister_agent(name);
|
||||
Ok(())
|
||||
}
|
||||
.await;
|
||||
match result {
|
||||
Ok(()) => {
|
||||
crate::swarm_notices::notify(
|
||||
"core",
|
||||
Some(format!("killed:{name}")),
|
||||
format!("agent '{name}' killed"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
Response::Ok
|
||||
}
|
||||
Err(e) => Response::Err {
|
||||
message: format!("{e:#}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// `Update` — enqueue a rebuild for a container. The caller must be an
|
||||
/// ancestor of `name` in the topology.
|
||||
pub(super) fn handle_update(coord: &Arc<Coordinator>, agent: &str, name: &str) -> Response {
|
||||
if let Some(err) = require_descendant(agent, name, "rebuild") {
|
||||
return err;
|
||||
}
|
||||
tracing::info!(%agent, %name, "submit rebuild");
|
||||
if let Err(e) = coord.job_queue.insert_job(|b| {
|
||||
crate::job_queue::templates::rebuild(b, name, true);
|
||||
Vec::new()
|
||||
}) {
|
||||
tracing::error!(%agent, %name, error = ?e, "update: insert failed");
|
||||
}
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
Response::Ok
|
||||
}
|
||||
|
||||
/// `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 {
|
||||
|
|
|
|||
|
|
@ -31,9 +31,7 @@ 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_kill, handle_list_descendants, handle_restart, handle_start, handle_update,
|
||||
};
|
||||
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,
|
||||
|
|
@ -542,41 +540,31 @@ 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: subtree-relational
|
||||
/// verbs (lifecycle/config/logs) require the caller to be an ancestor of the
|
||||
/// target (`is_descendant_of`, so the root covers all); hive-wide agent-state
|
||||
/// queries require the `QueryAgentState` capability; hive-wide orchestration
|
||||
/// verbs (schedules / meta-inputs) require the matching tool-group (the
|
||||
/// grantable capability).
|
||||
/// authority derives uniformly from the caller's identity: hive-wide
|
||||
/// agent-state queries require the `QueryAgentState` capability; hive-wide
|
||||
/// orchestration verbs (schedules / meta-inputs) 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 {
|
||||
// Lifecycle + config: caller must be an ancestor of the target
|
||||
// (a parent owns its whole subtree; the root covers every agent).
|
||||
Request::Start { name } => handle_start(coord, agent, name).await,
|
||||
Request::Restart { name } => handle_restart(coord, agent, name).await,
|
||||
Request::Kill { name } => handle_kill(coord, agent, name).await,
|
||||
Request::Update { name } => handle_update(coord, agent, name),
|
||||
Request::ListDescendants => handle_list_descendants(coord, agent).await,
|
||||
// Agent-state queries: own subtree is free; other agents + the
|
||||
// hive-wide `"*"` sweep require `QueryAgentState`.
|
||||
Request::GetLooseEnds { agent: target } => {
|
||||
handle_get_loose_ends(coord, agent, target.as_deref())
|
||||
}
|
||||
// Orchestration / diagnostics verbs — gated per-verb on tool-group
|
||||
// membership or topology (see `dispatch_orchestration`).
|
||||
// Orchestration verbs — gated per-verb on tool-group membership
|
||||
// (see `dispatch_orchestration`).
|
||||
_ => dispatch_orchestration(req, agent, coord).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle the hive-wide orchestration verbs (scheduling, meta-input updates)
|
||||
/// plus container-log reads. No blanket socket gate: each verb gates on the
|
||||
/// grantable capability that authorises it — the matching tool-group
|
||||
/// (`scheduling` / `approvals`), or `is_descendant_of` for `get_logs`
|
||||
/// (a parent reads its subtree's logs). Any other variant is a host-admin /
|
||||
/// unknown request invalid on either socket.
|
||||
/// 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.
|
||||
async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
|
||||
match req {
|
||||
Request::RequestUpdateMetaInputs {
|
||||
|
|
@ -638,15 +626,6 @@ async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordina
|
|||
}
|
||||
handle_fire_schedule_now(coord, agent, *id).await
|
||||
}
|
||||
Request::GetLogs {
|
||||
agent: target,
|
||||
lines,
|
||||
} => {
|
||||
if let Some(err) = require_descendant(agent, target, "read logs of") {
|
||||
return err;
|
||||
}
|
||||
handle_get_logs(target, *lines).await
|
||||
}
|
||||
// Host-admin-only / unknown variants: never valid on either socket.
|
||||
_ => Response::Err {
|
||||
message: "request not handled on this socket".to_owned(),
|
||||
|
|
@ -654,25 +633,6 @@ async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordina
|
|||
}
|
||||
}
|
||||
|
||||
/// Topology guard for the subtree-relational lifecycle/config/log tools: the
|
||||
/// `target` must be the caller itself or one of its topology descendants — a
|
||||
/// parent owns its whole subtree, and the root (`ruth`) covers every agent as
|
||||
/// a consequence, with no positional/hardcoded privilege. Returns `Some(Err)`
|
||||
/// to short-circuit the dispatch arm when it isn't, `None` when authorised.
|
||||
/// `action` is the verb phrase for the message (e.g. `"start"`).
|
||||
fn require_descendant(agent: &str, target: &str, action: &str) -> Option<Response> {
|
||||
if crate::topology::is_descendant_of(target, agent) {
|
||||
None
|
||||
} else {
|
||||
Some(Response::Err {
|
||||
message: format!(
|
||||
"agent `{agent}` cannot {action} `{target}`: \
|
||||
not in its subtree (topology)"
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Capability guard for the hive-wide orchestration verbs: the caller must
|
||||
/// hold the given tool-group. The tool-group (c0re-owned `tool_groups.json`,
|
||||
/// read server-side via [`crate::tool_groups::groups_for`]) is the grantable
|
||||
|
|
@ -1074,35 +1034,6 @@ pub(crate) fn handle_send(
|
|||
}
|
||||
}
|
||||
|
||||
/// `GetLogs` — read a child container's journal via hive-priv (the
|
||||
/// `-M` read needs root). `journalctl -M` wants the `h-<name>` machine
|
||||
/// name, which `container_name` derives.
|
||||
async fn handle_get_logs(agent: &str, lines: Option<u32>) -> Response {
|
||||
// Clamped here and not only in the MCP layer that also clamps it: a
|
||||
// caller-side limit is not a limit, and anything speaking this socket
|
||||
// sets `lines` itself. Mirrors `handle_get_host_journal`'s own cap.
|
||||
let n = lines.unwrap_or(50).min(500);
|
||||
let machine = crate::lifecycle::container_name(agent);
|
||||
tracing::info!(%agent, %machine, %n, "manager: get_logs");
|
||||
match crate::priv_client::read_container_journal(
|
||||
&machine,
|
||||
hive_priv_sock::JournalQuery {
|
||||
lines: n,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((stdout, stderr)) => {
|
||||
let content = if stdout.is_empty() { stderr } else { stdout };
|
||||
Response::Logs { content }
|
||||
}
|
||||
Err(e) => Response::Err {
|
||||
message: format!("get_logs: {e:#}"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
|||
Loading…
Reference in a new issue