From 87970a8c9308da20680211462b3b68f87a52c0a4 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 19 Sep 2026 03:47:18 +0200 Subject: [PATCH] mcp: remove the restart/kill/start/update/get_logs agent verbs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/agent-lifecycle/agent-hierarchy.md | 2 - docs/agent-lifecycle/approvals.md | 6 +- docs/process/conventions.md | 3 +- docs/tools/README.md | 8 +- docs/tools/lifecycle.md | 30 +--- docs/tools/scheduling.md | 11 -- docs/turn-loop/mcp.md | 11 +- docs/web-ui/dashboard.md | 4 +- hive-agent-mcp/src/mcp/args.rs | 37 +---- hive-agent-mcp/src/mcp/mod.rs | 137 +----------------- hive-agent/prompts/system.md | 10 +- hive-agent/src/stream_enrich.rs | 18 +-- hive-c0re/src/dashboard/lifecycle_ops.rs | 5 +- .../src/socket_server/lifecycle_handlers.rs | 89 +----------- hive-c0re/src/socket_server/mod.rs | 91 ++---------- hive-core-agent-sock/src/lib.rs | 20 +-- hive-sh4re/src/permissions.rs | 16 +- 17 files changed, 48 insertions(+), 450 deletions(-) diff --git a/docs/agent-lifecycle/agent-hierarchy.md b/docs/agent-lifecycle/agent-hierarchy.md index 481d4994..90276284 100644 --- a/docs/agent-lifecycle/agent-hierarchy.md +++ b/docs/agent-lifecycle/agent-hierarchy.md @@ -89,9 +89,7 @@ umount-old / mount-new / restart-cascade step. | operation | who can do it | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------- | -| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor | | config change via forge PR (any descendant's config) | any ancestor | -| `get_logs` (any 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 | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | diff --git a/docs/agent-lifecycle/approvals.md b/docs/agent-lifecycle/approvals.md index 80222b13..cbdec977 100644 --- a/docs/agent-lifecycle/approvals.md +++ b/docs/agent-lifecycle/approvals.md @@ -617,10 +617,10 @@ root agent. Variants (`hive_sh4re::manager::HelperEvent`): tombstone, three `POLL_INTERVAL`s — closes the race where a lifecycle op finishes between two crash-watch polls and the container shows briefly as "stopped without transient" before - the next start). The root agent can `start` it again or escalate. + the next start). The root agent escalates to the operator, who + starts it again from the dashboard. - `NeedsUpdate { agent }` — sub-agent's recorded flake rev is - stale. The root agent calls `update(name)` to rebuild — idempotent, - no approval required. + stale. The operator rebuilds it from the dashboard. The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`, `Destroyed`, `NeedsLogin`, `LoggedIn` — are "FYI, check diff --git a/docs/process/conventions.md b/docs/process/conventions.md index 54523c7c..cafcc929 100644 --- a/docs/process/conventions.md +++ b/docs/process/conventions.md @@ -310,10 +310,9 @@ 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` | `kill`, `start`, `restart`, `update`, `list_containers` *(privileged)* | +| `lifecycle` | `list_containers` *(privileged)* | | `approvals` | `request_update_meta_inputs` *(privileged)* | | `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* | -| `diagnostics` | `get_logs` *(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) | diff --git a/docs/tools/README.md b/docs/tools/README.md index 23918906..8fea9e1d 100644 --- a/docs/tools/README.md +++ b/docs/tools/README.md @@ -40,12 +40,10 @@ 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)** — kill/start/restart/update for the - agents in a caller's own subtree, plus the approval-gated - config-change tools. +- **[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. - **[scheduling](scheduling.md)** — scheduled prompts (operator - approval required) and the diagnostics tools (`get_logs`, - `get_host_journal`). + approval required) and the `get_host_journal` diagnostics tool. diff --git a/docs/tools/lifecycle.md b/docs/tools/lifecycle.md index c59612fb..c556098a 100644 --- a/docs/tools/lifecycle.md +++ b/docs/tools/lifecycle.md @@ -11,25 +11,6 @@ everything sits under it. No operator approval required. The caller's own subtree. -### `kill(name)` - -Graceful stop. Container state is preserved; recreating the agent -reuses prior config and credentials. - -### `start(name)` - -Start a stopped sub-agent. - -### `restart(name)` - -Stop + start in one call. - -### `update(name)` - -Rebuild: re-applies the current hyperhive flake + `agent.nix`, -then restarts. Idempotent — safe to call repeatedly. Used in response -to `needs_update` system events. - ### `list_containers()` List the caller's whole **subtree** with running status — children, @@ -60,16 +41,15 @@ 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 — call `update(name)` on affected +**Doesn't** trigger container rebuilds — the operator rebuilds affected agents after the approval resolves. ## Boundary summary -| Operation | Requires approval? | Scope | -| --------------------------------------- | ------------------ | ---------------------------- | -| `kill` / `start` / `restart` / `update` | No | Own subtree | -| `list_containers` | No | Own subtree, caller included | -| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | +| Operation | Requires approval? | Scope | +| ---------------------------- | ------------------ | ---------------------------- | +| `list_containers` | No | Own subtree, caller included | +| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) | ## See also diff --git a/docs/tools/scheduling.md b/docs/tools/scheduling.md index 5b7b6517..1a4dba27 100644 --- a/docs/tools/scheduling.md +++ b/docs/tools/scheduling.md @@ -61,17 +61,6 @@ and `last_result`, `next_fire_at_unix`, `interval_seconds`. Use to look up an id before cancelling, or to audit upcoming wake-ups in your subtree. -## `diagnostics` tool group - -### `get_logs(agent, lines?)` - -Fetch recent journal lines for a sub-agent container. Useful for -diagnosing MCP-registration failures, startup crashes, plugin install -errors, or any harness issue you can't see from inside the container. - -Pass the plain logical agent name (for example `"gui"`) — hive-c0re resolves -the machine name (`h-`). `lines` defaults to 50, host-capped at 500. - ## `read_host_journal` capability Capability-gated (not a tool group) — the operator enables it in the diff --git a/docs/turn-loop/mcp.md b/docs/turn-loop/mcp.md index 6272126b..99be1f11 100644 --- a/docs/turn-loop/mcp.md +++ b/docs/turn-loop/mcp.md @@ -20,8 +20,8 @@ for what reaches a subagent's own `--mcp-config` and what doesn't. Tool groups (`HIVE_TOOL_GROUPS`) gate tool access. The default preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and -`execution`. Privileged groups (`lifecycle`, `approvals`, `scheduling`, -`diagnostics`) are opt-in via the P3RM1SS10NS tab. +`execution`. Privileged groups (`lifecycle`, `approvals`, `scheduling`) +are opt-in via the P3RM1SS10NS tab. ## Core tools (always available) @@ -135,11 +135,10 @@ 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`) — manage child - agents, spawn new ones, apply config commits. See +- **Lifecycle + config** (`lifecycle`, `approvals`) — list the child + agents in your own subtree, apply config commits. See [`docs/tools/lifecycle.md`](../tools/lifecycle.md). -- **Scheduling + diagnostics** (`scheduling`, `diagnostics`) — - scheduled prompts, `get_logs`. See +- **Scheduling** (`scheduling`) — scheduled prompts. See [`docs/tools/scheduling.md`](../tools/scheduling.md). - **Forge repos** (`forge`) — `create_repo` — the only agent path to create a repo under the `agents/` org (direct forge token creation is diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 1751f6e2..81ac6929 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -453,7 +453,7 @@ named buckets of MCP tools; each agent starts with a role default (sub-agents: `messaging`, `meta`, `inbox`, `execution` — `ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to `ToolGroup::MANAGER_DEFAULT` — `messaging`, `meta`, `inbox`, -`lifecycle`, `approvals`, `scheduling`, `diagnostics`, `execution`, +`lifecycle`, `approvals`, `scheduling`, `execution`, that is, every group except `forge` and `web_tools`). Checking / unchecking stages which groups are active for the agent; the page-level **save all** button (below) commits it. Columns come from @@ -461,7 +461,7 @@ page-level **save all** button (below) commits it. Columns come from takes effect. The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`, -`approvals`, `scheduling`, `diagnostics`, `forge`, `execution`, +`approvals`, `scheduling`, `forge`, `execution`, `web_tools`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The `web_tools` group is special: it carries no MCP tools; instead it adds Claude's built-in `WebFetch` and `WebSearch` to `--tools` / diff --git a/hive-agent-mcp/src/mcp/args.rs b/hive-agent-mcp/src/mcp/args.rs index 1f88bbaa..7843649f 100644 --- a/hive-agent-mcp/src/mcp/args.rs +++ b/hive-agent-mcp/src/mcp/args.rs @@ -94,15 +94,9 @@ pub struct CompactArgs { } // ----------------------------------------------------------------------------- -// Privileged tool arg types (lifecycle, approvals, scheduling, diagnostics) +// Privileged tool arg types (lifecycle, approvals, scheduling) // ----------------------------------------------------------------------------- -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct KillArgs { - /// Sub-agent name (without the `h-` container prefix). - pub name: String, -} - #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct SetStatusArgs { /// Status text to display on the dashboard card. Pass an empty string to clear. @@ -125,24 +119,6 @@ pub struct GetAgentMetaArgs { pub name: Option, } -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct StartArgs { - /// Sub-agent name (without the `h-` container prefix). - pub name: String, -} - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct RestartArgs { - /// Sub-agent name (without the `h-` container prefix). - pub name: String, -} - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct UpdateArgs { - /// Sub-agent name (without the `h-` container prefix). - pub name: String, -} - #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct CancelLooseEndArgs { /// Which kind of thread to cancel — `"reminder"` for a scheduled @@ -265,17 +241,6 @@ pub struct EditScheduleArgs { pub targets_remove: Option>, } -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct GetLogsArgs { - /// Logical agent name to fetch logs for (e.g. `gui`, `iris`). - /// hive-c0re maps it to the underlying machine name (`h-gui`) - /// itself — pass the plain agent name, not the `h-` form. - pub agent: String, - /// How many journal lines to return (default: 50, max: 500). - #[serde(default)] - pub lines: Option, -} - /// Arguments for `get_host_journal` (capability-gated: `read_host_journal`). #[derive(Debug, serde::Deserialize, schemars::JsonSchema)] pub struct GetHostJournalArgs { diff --git a/hive-agent-mcp/src/mcp/mod.rs b/hive-agent-mcp/src/mcp/mod.rs index a932fc9e..dbd767e4 100644 --- a/hive-agent-mcp/src/mcp/mod.rs +++ b/hive-agent-mcp/src/mcp/mod.rs @@ -26,8 +26,8 @@ mod render; pub use args::{ AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, - GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, - RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs, + MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs, + UpdateMetaInputsArgs, }; pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv}; @@ -557,78 +557,6 @@ impl AgentServer { .await } - // IMPORTANT: this tool is only available when the `lifecycle` tool group - // is granted to this agent. hive-c0re enforces the topology check - // server-side: the call is rejected unless `name` is in the caller's - // subtree. - #[tool( - description = "Restart a sub-agent container (stop + start). Only succeeds if `name` \ - is somewhere in this agent's subtree — a child, a child's child, and so on down \ - the topology tree — which the server enforces. No approval required." - )] - async fn restart(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - let name = args.name.clone(); - run_tool_envelope("restart", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::Restart { name: args.name }) - .await; - annotate_retries( - format_ack(resp, "restart", format!("restarted {name}")), - retries, - ) - }) - .await - } - - // IMPORTANT: this tool is only available when the `lifecycle` tool group - // is granted to this agent. hive-c0re enforces the topology check - // server-side: the call is rejected unless `name` is in the caller's - // subtree. - #[tool( - description = "Stop a sub-agent container (graceful). Only succeeds if `name` \ - is somewhere in this agent's subtree — a child, a child's child, and so on down \ - the topology tree — which the server enforces. No approval required. \ - State dir is kept; recreating the agent reuses prior config + credentials." - )] - async fn kill(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - let name = args.name.clone(); - run_tool_envelope("kill", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::Kill { name: args.name }) - .await; - annotate_retries(format_ack(resp, "kill", format!("killed {name}")), retries) - }) - .await - } - - // IMPORTANT: this tool is only available when the `lifecycle` tool group - // is granted to this agent. hive-c0re enforces the topology check - // server-side: the call is rejected unless `name` is in the caller's - // subtree. - #[tool( - description = "Rebuild a sub-agent: re-applies the current hyperhive flake + agent.nix \ - and restarts the container. Only succeeds if `name` is somewhere in this agent's \ - subtree — a child, a child's child, and so on down the topology tree — which the \ - server enforces. No approval required. Idempotent — use when an agent needs its \ - config reapplied." - )] - async fn update(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - let name = args.name.clone(); - run_tool_envelope("update", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::Update { name: args.name }) - .await; - annotate_retries( - format_ack(resp, "update", format!("updated {name}")), - retries, - ) - }) - .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. @@ -709,70 +637,13 @@ impl AgentServer { .await } - // IMPORTANT: this tool is only available when the `lifecycle` tool group - // is granted to this agent. hive-c0re enforces the topology check - // server-side: the call is rejected unless `name` is in the caller's - // subtree. - #[tool( - description = "Start a stopped sub-agent container. Only succeeds if `name` \ - is somewhere in this agent's subtree — a child, a child's child, and so on down \ - the topology tree — which the server enforces. No approval required." - )] - async fn start(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - let name = args.name.clone(); - run_tool_envelope("start", log, async move { - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::Start { name: args.name }) - .await; - annotate_retries( - format_ack(resp, "start", format!("started {name}")), - retries, - ) - }) - .await - } - - #[tool( - description = "Fetch recent journal log lines for a sub-agent container. Useful \ - for diagnosing MCP server registration failures, startup crashes, plugin install \ - errors, or any harness issue you can't see from inside the container. Pass the \ - plain logical agent name (e.g. `gui`) — hive-c0re resolves the machine name. \ - `lines` defaults to 50 (max capped at 500 on the host side)." - )] - async fn get_logs(&self, Parameters(args): Parameters) -> String { - let log = format!("{args:?}"); - let agent = args.agent.clone(); - run_tool_envelope("get_logs", log, async move { - let lines = args.lines.map(|n| n.min(500)); - let (resp, retries) = self - .dispatch(hive_core_agent_sock::Request::GetLogs { - agent: agent.clone(), - lines, - }) - .await; - let s = match resp { - Ok(hive_core_agent_sock::Response::Logs { content }) => { - if content.is_empty() { - format!("(no journal output for {agent})") - } else { - content - } - } - other => reply_err(other, "get_logs"), - }; - annotate_retries(s, retries) - }) - .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 — call `update` on each affected agent \ - separately after the approval resolves." + Does NOT trigger container rebuilds — the operator rebuilds affected agents \ + after the approval resolves." )] async fn request_update_meta_inputs( &self, diff --git a/hive-agent/prompts/system.md b/hive-agent/prompts/system.md index e4a98c31..c4b85380 100644 --- a/hive-agent/prompts/system.md +++ b/hive-agent/prompts/system.md @@ -4,21 +4,21 @@ 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____` — 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): `restart`, `kill`, `start`, `update`, `list_containers`. +- **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_logs` (_requires `diagnostics` tool group_), `get_host_journal` (_requires `read_host_journal` capability_). +- **Diagnostics**: `get_host_journal` (_requires `read_host_journal` capability_). Need new packages, env vars, or other NixOS config for yourself? You can't edit your own config directly — open a PR on your `agent-configs/` repo, or contact the operator directly. Config repos live at `/agents/{label}/config/` (read-only inside your container). All changes flow through operator-approved commits. Your config repo is mounted **read-only** at `/agents/{label}/config/` — `agent.nix` plus whatever extra files define you (declared packages, env vars, MCP servers). Read it to see exactly what defines you before asking for a change, so you can point at the precise file and line. -Approval boundary: lifecycle ops on _existing_ agents in your subtree (`kill`, `start`, `restart`) are at your discretion — no operator approval needed (requires `lifecycle` tool group). _Creating_ a new agent is not something you can do from here — ask the operator, who scaffolds the new agent's config repo and spawns it from the dashboard. _Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/` repo, which queues a `MergeConfigPr` approval on open/update. The operator only signs off on changes; you run the day-to-day. +Approval boundary: starting, stopping and rebuilding containers is the operator's — ask them when an agent in your subtree needs one. _Creating_ a new agent is not something you can do from here — ask the operator, who scaffolds the new agent's config repo and spawns it from the dashboard. _Changing_ any agent's config is not a tool call at all — it's a forge PR on the agent's `agent-configs/` repo, which queues a `MergeConfigPr` approval on open/update. The operator only signs off on changes; you run the day-to-day. Messages from sender `system` are hyperhive helper events (JSON body, `event` field discriminates): `approval_resolved`, `container_crash`, `needs_update`. Use these to react to lifecycle changes: -- `needs_update` — agent's flake rev is stale. Call `update(name)` to rebuild — it's idempotent and doesn't need approval. -- `container_crash` — restart with `start(name)`. If it crashes again, ask the operator. +- `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. 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. diff --git a/hive-agent/src/stream_enrich.rs b/hive-agent/src/stream_enrich.rs index 4fbfb56e..33a3848d 100644 --- a/hive-agent/src/stream_enrich.rs +++ b/hive-agent/src/stream_enrich.rs @@ -498,15 +498,11 @@ fn tool_icon(name: &str) -> &'static str { "mcp__hyperhive__cancel_loose_end" => "✂️", "mcp__hyperhive__ack_until" => "✅", "mcp__hyperhive__get_agent_meta" => "ℹ️", - "mcp__hyperhive__restart" => "↻", - "mcp__hyperhive__kill" => "⏹️", - "mcp__hyperhive__start" => "▶️", - "mcp__hyperhive__update" => "🔄", "mcp__hyperhive__list_containers" | "mcp__matrix__list_rooms" | "mcp__matrix__list_room_members" | "mcp__matrix__list_invites" => "📋", - "mcp__hyperhive__get_logs" | "mcp__hyperhive__get_host_journal" => "📜", + "mcp__hyperhive__get_host_journal" => "📜", "mcp__matrix__read_room" | "Read" => "📖", "mcp__matrix__mark_read" => "👁️", "mcp__bash__kill" => "🛑", @@ -643,10 +639,6 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String { format!("{short} {}", parts.join(" · ")) } } - "mcp__hyperhive__kill" - | "mcp__hyperhive__restart" - | "mcp__hyperhive__start" - | "mcp__hyperhive__update" => format!("{short} {}", sv(input, "name")), "mcp__hyperhive__ack_until" => { let up_to = input .get("up_to") @@ -654,14 +646,6 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String { .map_or_else(|| "?".to_owned(), |n| n.to_string()); format!("{short} ≤{up_to}") } - "mcp__hyperhive__get_logs" => { - let lines = input - .get("lines") - .and_then(Value::as_u64) - .map(|n| format!(" · {n}L")) - .unwrap_or_default(); - format!("{short} {}{lines}", sv(input, "agent")) - } "mcp__hyperhive__get_host_journal" => { let mut parts = Vec::new(); if let Some(c) = input.get("container").and_then(Value::as_str) { diff --git a/hive-c0re/src/dashboard/lifecycle_ops.rs b/hive-c0re/src/dashboard/lifecycle_ops.rs index 7490c116..6204d434 100644 --- a/hive-c0re/src/dashboard/lifecycle_ops.rs +++ b/hive-c0re/src/dashboard/lifecycle_ops.rs @@ -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 diff --git a/hive-c0re/src/socket_server/lifecycle_handlers.rs b/hive-c0re/src/socket_server/lifecycle_handlers.rs index e08f2c6b..fe19b735 100644 --- a/hive-c0re/src/socket_server/lifecycle_handlers.rs +++ b/hive-c0re/src/socket_server/lifecycle_handlers.rs @@ -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, 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, 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, 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, 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, agent: &str) -> Response { diff --git a/hive-c0re/src/socket_server/mod.rs b/hive-c0re/src/socket_server/mod.rs index 3eb64313..141c0655 100644 --- a/hive-c0re/src/socket_server/mod.rs +++ b/hive-c0re/src/socket_server/mod.rs @@ -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) -> 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) -> Response { match req { Request::RequestUpdateMetaInputs { @@ -638,15 +626,6 @@ async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc { - 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 Option { - 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-` machine -/// name, which `container_name` derives. -async fn handle_get_logs(agent: &str, lines: Option) -> 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::*; diff --git a/hive-core-agent-sock/src/lib.rs b/hive-core-agent-sock/src/lib.rs index 7dc85323..9731b6d6 100644 --- a/hive-core-agent-sock/src/lib.rs +++ b/hive-core-agent-sock/src/lib.rs @@ -154,21 +154,6 @@ pub enum Request { }, // ---- privileged (manager socket only for now) --------------------------- - /// *(privileged)* Stop a sub-agent (graceful). - Kill { name: String }, - /// *(privileged)* Start a previously-stopped sub-agent container. - Start { name: String }, - /// *(privileged)* Restart a sub-agent container (stop + start). - Restart { name: String }, - /// *(privileged)* Rebuild a sub-agent against the current hyperhive - /// flake + agent.nix. No approval required. - Update { name: String }, - /// *(privileged)* Fetch recent journal lines for a sub-agent container. - GetLogs { - agent: String, - #[serde(default)] - lines: Option, - }, /// *(privileged)* Queue an approval to run `nix flake update [inputs...]`. RequestUpdateMetaInputs { #[serde(default)] @@ -215,7 +200,7 @@ pub enum Request { } /// Unified response enum for both agent and manager sockets. Privileged -/// variants (`Logs`, `Schedules`) are never returned on agent sockets. +/// variants (`Schedules`) are never returned on agent sockets. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum Response { @@ -282,9 +267,6 @@ pub enum Response { #[serde(default, skip_serializing_if = "Vec::is_empty")] matrix_accounts: Vec, }, - /// `GetLogs` result: journal lines for the requested container. - /// Returned on the manager socket only. - Logs { content: String }, /// `GetHostJournal` result: host journal lines matching the /// requested filters. Returned on the agent socket when the agent /// holds the `read_host_journal` capability. diff --git a/hive-sh4re/src/permissions.rs b/hive-sh4re/src/permissions.rs index a8197398..37de1f75 100644 --- a/hive-sh4re/src/permissions.rs +++ b/hive-sh4re/src/permissions.rs @@ -152,15 +152,13 @@ pub enum ToolGroup { Meta, /// `get_loose_ends`, `cancel_loose_end`, `remind` Inbox, - /// `kill`, `start`, `restart`, `update` - *(privileged)* + /// `list_containers` - *(privileged)* Lifecycle, /// `request_update_meta_inputs` - *(privileged)* Approvals, /// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, /// `edit_schedule`, `list_schedules` - *(privileged)* Scheduling, - /// `get_logs` - *(privileged)* - Diagnostics, /// `create_repo` — create git repos through hive-c0re (the only path /// now that agents can't create them directly). Opt-in per /// agent so the operator controls who can spin up repos. @@ -225,7 +223,7 @@ impl ToolGroup { Self::Messaging => &["send", "recv", "ack_until"], Self::Meta => &["get_agent_meta"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], - Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"], + Self::Lifecycle => &["list_containers"], Self::Approvals => &["request_update_meta_inputs"], Self::Scheduling => &[ "request_schedule_prompt", @@ -234,7 +232,6 @@ impl ToolGroup { "edit_schedule", "list_schedules", ], - Self::Diagnostics => &["get_logs"], Self::Forge => &["create_repo"], // Both empty, for different reasons — see each variant's own // doc comment above. `Execution` grants the out-of-process @@ -300,7 +297,6 @@ impl ToolGroup { Self::Lifecycle, Self::Approvals, Self::Scheduling, - Self::Diagnostics, Self::Execution, ]; @@ -314,7 +310,6 @@ impl ToolGroup { Self::Lifecycle, Self::Approvals, Self::Scheduling, - Self::Diagnostics, Self::Forge, Self::Execution, Self::WebTools, @@ -329,18 +324,13 @@ 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 => { - "kill, start, restart, update, list_containers — container lifecycle (privileged)" - } + Self::Lifecycle => "list_containers — own-subtree container listing (privileged)", Self::Approvals => { "request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)" } Self::Scheduling => { "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)" } - Self::Diagnostics => { - "get_logs — read a sub-agent container's systemd journal (privileged)" - } Self::Forge => { "create_repo — create git repos through hive-c0re (operator-gated merge)" }