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:
atlas 2026-09-19 03:47:18 +02:00 committed by mara
commit 87970a8c93
17 changed files with 48 additions and 450 deletions

View file

@ -89,9 +89,7 @@ umount-old / mount-new / restart-cascade step.
| operation | who can do it | | operation | who can do it |
| ----------------------------------------------------------- | --------------------------------------------------------------------------------------- | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
| config change via forge PR (any descendant's config) | 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 | | moderate reminders (cancel any open thread of a descendant) | any ancestor |
| `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else | | `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) | | `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |

View file

@ -617,10 +617,10 @@ root agent. Variants (`hive_sh4re::manager::HelperEvent`):
tombstone, three `POLL_INTERVAL`s — closes the race where a tombstone, three `POLL_INTERVAL`s — closes the race where a
lifecycle op finishes between two crash-watch polls and the lifecycle op finishes between two crash-watch polls and the
container shows briefly as "stopped without transient" before 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 - `NeedsUpdate { agent }` — sub-agent's recorded flake rev is
stale. The root agent calls `update(name)` to rebuild — idempotent, stale. The operator rebuilds it from the dashboard.
no approval required.
The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`, The remaining lower-urgency lifecycle notices — `Rebuilt`, `Killed`,
`Destroyed`, `NeedsLogin`, `LoggedIn` — are "FYI, check `Destroyed`, `NeedsLogin`, `LoggedIn` — are "FYI, check

View file

@ -310,10 +310,9 @@ binary flavor.
| `meta` | `get_agent_meta` (`set_status` is always-on, see below) | | `meta` | `get_agent_meta` (`set_status` is always-on, see below) |
| `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` | | `inbox` | `get_loose_ends`, `cancel_loose_end`, `remind` |
| `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. | | `execution` | vestigial — `mcp__bash__run` / `mcp__bash__status` are always available unconditionally via `extraMcpServers`; this group's entries expand to non-existent `mcp__hyperhive__run` / `mcp__hyperhive__status` and have no effect. See `docs/tools/bash.md`. |
| `lifecycle` | `kill`, `start`, `restart`, `update`, `list_containers` *(privileged)* | | `lifecycle` | `list_containers` *(privileged)* |
| `approvals` | `request_update_meta_inputs` *(privileged)* | | `approvals` | `request_update_meta_inputs` *(privileged)* |
| `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* | | `scheduling` | `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, `edit_schedule`, `list_schedules` *(privileged)* |
| `diagnostics` | `get_logs` *(privileged)* |
| `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) | | `forge` | `create_repo` — create git repos through hive-c0re (operator-gated merge) |
| `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) | | `web_tools` | none (gates the Claude built-ins `WebFetch`/`WebSearch`, not an MCP tool) |

View file

@ -40,12 +40,10 @@ debug agent behavior.
- **[forge-cli](forge-cli.md)** — the exhaustive, autogenerated - **[forge-cli](forge-cli.md)** — the exhaustive, autogenerated
flag-by-flag reference for `hive-forge`, kept in lockstep with the flag-by-flag reference for `hive-forge`, kept in lockstep with the
binary by CI the same way `hivectl-cli.md` is. binary by CI the same way `hivectl-cli.md` is.
- **[lifecycle](lifecycle.md)** — kill/start/restart/update for the - **[lifecycle](lifecycle.md)** — listing the agents in a caller's own
agents in a caller's own subtree, plus the approval-gated subtree, plus the approval-gated config-change tools.
config-change tools.
- **[matrix](matrix.md)** — the matrix MCP tool surface - **[matrix](matrix.md)** — the matrix MCP tool surface
(`mcp__matrix__*`) for agents with a matrix account, multiple (`mcp__matrix__*`) for agents with a matrix account, multiple
accounts per agent, and declaring extra MCP servers generally. accounts per agent, and declaring extra MCP servers generally.
- **[scheduling](scheduling.md)** — scheduled prompts (operator - **[scheduling](scheduling.md)** — scheduled prompts (operator
approval required) and the diagnostics tools (`get_logs`, approval required) and the `get_host_journal` diagnostics tool.
`get_host_journal`).

View file

@ -11,25 +11,6 @@ everything sits under it.
No operator approval required. The caller's own subtree. 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_containers()`
List the caller's whole **subtree** with running status — children, 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 / pass `[]` for all inputs. Returns immediately; the lock update runs
on operator approval. 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. agents after the approval resolves.
## Boundary summary ## Boundary summary
| Operation | Requires approval? | Scope | | Operation | Requires approval? | Scope |
| --------------------------------------- | ------------------ | ---------------------------- | | ---------------------------- | ------------------ | ---------------------------- |
| `kill` / `start` / `restart` / `update` | No | Own subtree | | `list_containers` | No | Own subtree, caller included |
| `list_containers` | No | Own subtree, caller included | | `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
## See also ## See also

View file

@ -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 look up an id before cancelling, or to audit upcoming wake-ups in
your subtree. 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-<name>`). `lines` defaults to 50, host-capped at 500.
## `read_host_journal` capability ## `read_host_journal` capability
Capability-gated (not a tool group) — the operator enables it in the Capability-gated (not a tool group) — the operator enables it in the

View file

@ -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 Tool groups (`HIVE_TOOL_GROUPS`) gate tool access. The default
preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and preset (`AGENT_DEFAULT`) includes `messaging`, `meta`, `inbox`, and
`execution`. Privileged groups (`lifecycle`, `approvals`, `scheduling`, `execution`. Privileged groups (`lifecycle`, `approvals`, `scheduling`)
`diagnostics`) are opt-in via the P3RM1SS10NS tab. are opt-in via the P3RM1SS10NS tab.
## Core tools (always available) ## 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 - **Subagent spawning** — headless claude sub-instances as background
tasks, shipped default-on like bash execution (no tool group gates it tasks, shipped default-on like bash execution (no tool group gates it
yet). See [`docs/tools/subagent.md`](../tools/subagent.md). yet). See [`docs/tools/subagent.md`](../tools/subagent.md).
- **Lifecycle + config** (`lifecycle`, `approvals`) — manage child - **Lifecycle + config** (`lifecycle`, `approvals`) — list the child
agents, spawn new ones, apply config commits. See agents in your own subtree, apply config commits. See
[`docs/tools/lifecycle.md`](../tools/lifecycle.md). [`docs/tools/lifecycle.md`](../tools/lifecycle.md).
- **Scheduling + diagnostics** (`scheduling`, `diagnostics`) — - **Scheduling** (`scheduling`) — scheduled prompts. See
scheduled prompts, `get_logs`. See
[`docs/tools/scheduling.md`](../tools/scheduling.md). [`docs/tools/scheduling.md`](../tools/scheduling.md).
- **Forge repos** (`forge`) — `create_repo` — the only agent path to - **Forge repos** (`forge`) — `create_repo` — the only agent path to
create a repo under the `agents/` org (direct forge token creation is create a repo under the `agents/` org (direct forge token creation is

View file

@ -453,7 +453,7 @@ named buckets of MCP tools; each agent starts with a role default
(sub-agents: `messaging`, `meta`, `inbox`, `execution` (sub-agents: `messaging`, `meta`, `inbox`, `execution`
`ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to `ToolGroup::AGENT_DEFAULT`; hive-c0re seeds the root agent to
`ToolGroup::MANAGER_DEFAULT``messaging`, `meta`, `inbox`, `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 / that is, every group except `forge` and `web_tools`). Checking /
unchecking stages which groups are active for the agent; the unchecking stages which groups are active for the agent; the
page-level **save all** button (below) commits it. Columns come from 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. takes effect.
The current tool groups are: `messaging`, `meta`, `inbox`, `lifecycle`, 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`. All listed in `ToolGroup::ALL` in `hive-sh4re`. The
`web_tools` group is special: it carries no MCP tools; instead it adds `web_tools` group is special: it carries no MCP tools; instead it adds
Claude's built-in `WebFetch` and `WebSearch` to `--tools` / Claude's built-in `WebFetch` and `WebSearch` to `--tools` /

View file

@ -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)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct SetStatusArgs { pub struct SetStatusArgs {
/// Status text to display on the dashboard card. Pass an empty string to clear. /// Status text to display on the dashboard card. Pass an empty string to clear.
@ -125,24 +119,6 @@ pub struct GetAgentMetaArgs {
pub name: Option<String>, pub name: Option<String>,
} }
#[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)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct CancelLooseEndArgs { pub struct CancelLooseEndArgs {
/// Which kind of thread to cancel — `"reminder"` for a scheduled /// Which kind of thread to cancel — `"reminder"` for a scheduled
@ -265,17 +241,6 @@ pub struct EditScheduleArgs {
pub targets_remove: Option<Vec<String>>, pub targets_remove: Option<Vec<String>>,
} }
#[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<u32>,
}
/// Arguments for `get_host_journal` (capability-gated: `read_host_journal`). /// Arguments for `get_host_journal` (capability-gated: `read_host_journal`).
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] #[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct GetHostJournalArgs { pub struct GetHostJournalArgs {

View file

@ -26,8 +26,8 @@ mod render;
pub use args::{ pub use args::{
AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs, AckUntilArgs, AgentGetLooseEndsArgs, CancelLooseEndArgs, CancelScheduleArgs, CompactArgs,
CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs, CreateRepoArgs, EditScheduleArgs, FireScheduleNowArgs, GetAgentMetaArgs, GetHostJournalArgs,
GetLogsArgs, KillArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, MarkTodosDoneArgs, RecvArgs, RemindArgs, RequestSchedulePromptArgs, SendArgs, SetStatusArgs,
RestartArgs, SendArgs, SetStatusArgs, StartArgs, UpdateArgs, UpdateMetaInputsArgs, UpdateMetaInputsArgs,
}; };
pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv}; pub use render::{annotate_retries, format_ack, format_agent_meta, format_recv};
@ -557,78 +557,6 @@ impl AgentServer {
.await .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<RestartArgs>) -> 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<KillArgs>) -> 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<UpdateArgs>) -> 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 // IMPORTANT: this tool is only available when the `lifecycle` tool group
// is granted to this agent. Returns the calling agent's whole subtree, // is granted to this agent. Returns the calling agent's whole subtree,
// itself included, with running status. // itself included, with running status.
@ -709,70 +637,13 @@ impl AgentServer {
.await .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<StartArgs>) -> 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<GetLogsArgs>) -> 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( #[tool(
description = "Queue an approval for the operator to run `nix flake update` on the \ 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 \ 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 \ 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. \ inputs. Returns immediately the lock update runs when the operator approves. \
Does NOT trigger container rebuilds call `update` on each affected agent \ Does NOT trigger container rebuilds the operator rebuilds affected agents \
separately after the approval resolves." after the approval resolves."
)] )]
async fn request_update_meta_inputs( async fn request_update_meta_inputs(
&self, &self,

View file

@ -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. - **Inbox / messaging** (always available): `mcp__hyperhive__recv`, `mcp__hyperhive__ack_until`, `mcp__hyperhive__send`, `mcp__hyperhive__get_loose_ends`, `mcp__hyperhive__cancel_loose_end`, `mcp__hyperhive__mark_todos_done`, `mcp__hyperhive__remind`, `mcp__hyperhive__set_status`, `mcp__hyperhive__get_agent_meta`. One habit worth internalizing beyond the tool descriptions themselves: prefer ending the turn over repeatedly polling `recv` when idle (only turn-boundaries observe in-container todo wakes — bash-task completions, matrix unread, forge activity — and ending the turn is also your checkpoint). For a large todo backlog (`get_loose_ends` caps at 40 rows), clear reviewed ids in bulk with `mark_todos_done` rather than cancelling one at a time — there's no blind range-clear, only ids you've actually looked at.
- **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time. - **Extra MCP tools** (some agents only): `mcp__<server>__<tool>` — agent-specific (matrix client, scraper, db connector, etc.) declared in your `agent.nix` under `hyperhive.extraMcpServers`. First-class tools, already operator-approved at deploy time.
- **Lifecycle** (_requires `lifecycle` tool group_, your own subtree — children, their children, and so on down, no approval needed): `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`. - **Approvals** (_requires `approvals` tool group_, queues an operator approval): `request_update_meta_inputs`.
- **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns). - **Scheduling** (_requires `scheduling` tool group_): `request_schedule_prompt` (queues an approval), `cancel_schedule`, `fire_schedule_now`, `edit_schedule`, `list_schedules` (these four don't need approval — you can manage schedules you own or that a sub-agent in your subtree owns).
- **Diagnostics**: `get_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/<name>` 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. 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/<name>` 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. 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/<name>` 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/<name>` 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: 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. - `needs_update` — agent's flake rev is stale. Ask the operator to rebuild it.
- `container_crash`restart with `start(name)`. If it crashes again, ask the operator. - `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 (`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. 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,15 +498,11 @@ fn tool_icon(name: &str) -> &'static str {
"mcp__hyperhive__cancel_loose_end" => "✂️", "mcp__hyperhive__cancel_loose_end" => "✂️",
"mcp__hyperhive__ack_until" => "", "mcp__hyperhive__ack_until" => "",
"mcp__hyperhive__get_agent_meta" => "", "mcp__hyperhive__get_agent_meta" => "",
"mcp__hyperhive__restart" => "",
"mcp__hyperhive__kill" => "⏹️",
"mcp__hyperhive__start" => "▶️",
"mcp__hyperhive__update" => "🔄",
"mcp__hyperhive__list_containers" "mcp__hyperhive__list_containers"
| "mcp__matrix__list_rooms" | "mcp__matrix__list_rooms"
| "mcp__matrix__list_room_members" | "mcp__matrix__list_room_members"
| "mcp__matrix__list_invites" => "📋", | "mcp__matrix__list_invites" => "📋",
"mcp__hyperhive__get_logs" | "mcp__hyperhive__get_host_journal" => "📜", "mcp__hyperhive__get_host_journal" => "📜",
"mcp__matrix__read_room" | "Read" => "📖", "mcp__matrix__read_room" | "Read" => "📖",
"mcp__matrix__mark_read" => "👁️", "mcp__matrix__mark_read" => "👁️",
"mcp__bash__kill" => "🛑", "mcp__bash__kill" => "🛑",
@ -643,10 +639,6 @@ fn fmt_hyperhive_tool(name: &str, short: &str, input: &Value) -> String {
format!("{short} {}", parts.join(" · ")) 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" => { "mcp__hyperhive__ack_until" => {
let up_to = input let up_to = input
.get("up_to") .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()); .map_or_else(|| "?".to_owned(), |n| n.to_string());
format!("{short}{up_to}") 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" => { "mcp__hyperhive__get_host_journal" => {
let mut parts = Vec::new(); let mut parts = Vec::new();
if let Some(c) = input.get("container").and_then(Value::as_str) { if let Some(c) = input.get("container").and_then(Value::as_str) {

View file

@ -107,10 +107,7 @@ pub(super) async fn post_kill(
// submitted by other sub-agents still process through the // submitted by other sub-agents still process through the
// host-side approval queue without the manager up, and // host-side approval queue without the manager up, and
// operator-driven meta-input updates work from the dashboard // operator-driven meta-input updates work from the dashboard
// either way. The MCP-surface self-kill guard in // either way.
// `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.
if let Err(e) = if let Err(e) =
crate::job_queue::power::stop_many(&state.coord, std::slice::from_ref(&logical), false) crate::job_queue::power::stop_many(&state.coord, std::slice::from_ref(&logical), false)
.await .await

View file

@ -1,97 +1,12 @@
//! Container-lifecycle request handlers (`Start` / `Restart` / `Kill` / //! `ListDescendants` request handler — the `lifecycle` tool group's
//! `Update` / `ListDescendants`). All are topology-guarded via //! remaining verb, a read of the caller's own subtree.
//! `super::require_descendant`.
use std::sync::Arc; use std::sync::Arc;
use hive_core_agent_sock::Response; use hive_core_agent_sock::Response;
use super::require_descendant;
use crate::coordinator::Coordinator; 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 /// `ListDescendants` — every topological descendant of `agent` with
/// its running/stopped state, parents before children. /// its running/stopped state, parents before children.
pub(super) async fn handle_list_descendants(coord: &Arc<Coordinator>, agent: &str) -> Response { pub(super) async fn handle_list_descendants(coord: &Arc<Coordinator>, agent: &str) -> Response {

View file

@ -31,9 +31,7 @@ pub(crate) use schedules::filter_ghost_schedule_targets;
pub use schedules::schedule_to_wire_public; pub use schedules::schedule_to_wire_public;
use config_approvals::handle_request_update_meta_inputs; use config_approvals::handle_request_update_meta_inputs;
use lifecycle_handlers::{ use lifecycle_handlers::handle_list_descendants;
handle_kill, handle_list_descendants, handle_restart, handle_start, handle_update,
};
use schedules::{ use schedules::{
EditSchedulePatch, handle_cancel_schedule, handle_edit_schedule, handle_fire_schedule_now, EditSchedulePatch, handle_cancel_schedule, handle_edit_schedule, handle_fire_schedule_now,
handle_list_schedules, handle_request_schedule_prompt, handle_list_schedules, handle_request_schedule_prompt,
@ -542,41 +540,31 @@ fn handle_requeue_inflight(
/// Unified dispatch for every socket connection — per-agent sockets and the /// Unified dispatch for every socket connection — per-agent sockets and the
/// (now pure-transport) manager socket alike. There is no privilege bit; /// (now pure-transport) manager socket alike. There is no privilege bit;
/// authority derives uniformly from the caller's identity: subtree-relational /// authority derives uniformly from the caller's identity: hive-wide
/// verbs (lifecycle/config/logs) require the caller to be an ancestor of the /// agent-state queries require the `QueryAgentState` capability; hive-wide
/// target (`is_descendant_of`, so the root covers all); hive-wide agent-state /// orchestration verbs (schedules / meta-inputs) require the matching
/// queries require the `QueryAgentState` capability; hive-wide orchestration /// tool-group (the grantable capability).
/// verbs (schedules / meta-inputs) require the matching tool-group (the
/// grantable capability).
async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response { async fn dispatch(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
if let Some(resp) = dispatch_shared(req, agent, coord).await { if let Some(resp) = dispatch_shared(req, agent, coord).await {
return resp; return resp;
} }
match req { match req {
// 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, Request::ListDescendants => handle_list_descendants(coord, agent).await,
// Agent-state queries: own subtree is free; other agents + the // Agent-state queries: own subtree is free; other agents + the
// hive-wide `"*"` sweep require `QueryAgentState`. // hive-wide `"*"` sweep require `QueryAgentState`.
Request::GetLooseEnds { agent: target } => { Request::GetLooseEnds { agent: target } => {
handle_get_loose_ends(coord, agent, target.as_deref()) handle_get_loose_ends(coord, agent, target.as_deref())
} }
// Orchestration / diagnostics verbs — gated per-verb on tool-group // Orchestration verbs — gated per-verb on tool-group membership
// membership or topology (see `dispatch_orchestration`). // (see `dispatch_orchestration`).
_ => dispatch_orchestration(req, agent, coord).await, _ => dispatch_orchestration(req, agent, coord).await,
} }
} }
/// Handle the hive-wide orchestration verbs (scheduling, meta-input updates) /// Handle the hive-wide orchestration verbs (scheduling, meta-input updates).
/// plus container-log reads. No blanket socket gate: each verb gates on the /// No blanket socket gate: each verb gates on the grantable capability that
/// grantable capability that authorises it — the matching tool-group /// authorises it — the matching tool-group (`scheduling` / `approvals`). Any
/// (`scheduling` / `approvals`), or `is_descendant_of` for `get_logs` /// other variant is a host-admin / unknown request invalid on either socket.
/// (a parent reads its subtree's logs). 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 { async fn dispatch_orchestration(req: &Request, agent: &str, coord: &Arc<Coordinator>) -> Response {
match req { match req {
Request::RequestUpdateMetaInputs { 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 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. // Host-admin-only / unknown variants: never valid on either socket.
_ => Response::Err { _ => Response::Err {
message: "request not handled on this socket".to_owned(), 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 /// Capability guard for the hive-wide orchestration verbs: the caller must
/// hold the given tool-group. The tool-group (c0re-owned `tool_groups.json`, /// 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 /// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View file

@ -154,21 +154,6 @@ pub enum Request {
}, },
// ---- privileged (manager socket only for now) --------------------------- // ---- 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<u32>,
},
/// *(privileged)* Queue an approval to run `nix flake update [inputs...]`. /// *(privileged)* Queue an approval to run `nix flake update [inputs...]`.
RequestUpdateMetaInputs { RequestUpdateMetaInputs {
#[serde(default)] #[serde(default)]
@ -215,7 +200,7 @@ pub enum Request {
} }
/// Unified response enum for both agent and manager sockets. Privileged /// 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")] #[serde(tag = "kind", rename_all = "snake_case")]
pub enum Response { pub enum Response {
@ -282,9 +267,6 @@ pub enum Response {
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
matrix_accounts: Vec<MatrixIdentity>, matrix_accounts: Vec<MatrixIdentity>,
}, },
/// `GetLogs` result: journal lines for the requested container.
/// Returned on the manager socket only.
Logs { content: String },
/// `GetHostJournal` result: host journal lines matching the /// `GetHostJournal` result: host journal lines matching the
/// requested filters. Returned on the agent socket when the agent /// requested filters. Returned on the agent socket when the agent
/// holds the `read_host_journal` capability. /// holds the `read_host_journal` capability.

View file

@ -152,15 +152,13 @@ pub enum ToolGroup {
Meta, Meta,
/// `get_loose_ends`, `cancel_loose_end`, `remind` /// `get_loose_ends`, `cancel_loose_end`, `remind`
Inbox, Inbox,
/// `kill`, `start`, `restart`, `update` - *(privileged)* /// `list_containers` - *(privileged)*
Lifecycle, Lifecycle,
/// `request_update_meta_inputs` - *(privileged)* /// `request_update_meta_inputs` - *(privileged)*
Approvals, Approvals,
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`, /// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
/// `edit_schedule`, `list_schedules` - *(privileged)* /// `edit_schedule`, `list_schedules` - *(privileged)*
Scheduling, Scheduling,
/// `get_logs` - *(privileged)*
Diagnostics,
/// `create_repo` — create git repos through hive-c0re (the only path /// `create_repo` — create git repos through hive-c0re (the only path
/// now that agents can't create them directly). Opt-in per /// now that agents can't create them directly). Opt-in per
/// agent so the operator controls who can spin up repos. /// agent so the operator controls who can spin up repos.
@ -225,7 +223,7 @@ impl ToolGroup {
Self::Messaging => &["send", "recv", "ack_until"], Self::Messaging => &["send", "recv", "ack_until"],
Self::Meta => &["get_agent_meta"], Self::Meta => &["get_agent_meta"],
Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"], Self::Inbox => &["get_loose_ends", "cancel_loose_end", "remind"],
Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"], Self::Lifecycle => &["list_containers"],
Self::Approvals => &["request_update_meta_inputs"], Self::Approvals => &["request_update_meta_inputs"],
Self::Scheduling => &[ Self::Scheduling => &[
"request_schedule_prompt", "request_schedule_prompt",
@ -234,7 +232,6 @@ impl ToolGroup {
"edit_schedule", "edit_schedule",
"list_schedules", "list_schedules",
], ],
Self::Diagnostics => &["get_logs"],
Self::Forge => &["create_repo"], Self::Forge => &["create_repo"],
// Both empty, for different reasons — see each variant's own // Both empty, for different reasons — see each variant's own
// doc comment above. `Execution` grants the out-of-process // doc comment above. `Execution` grants the out-of-process
@ -300,7 +297,6 @@ impl ToolGroup {
Self::Lifecycle, Self::Lifecycle,
Self::Approvals, Self::Approvals,
Self::Scheduling, Self::Scheduling,
Self::Diagnostics,
Self::Execution, Self::Execution,
]; ];
@ -314,7 +310,6 @@ impl ToolGroup {
Self::Lifecycle, Self::Lifecycle,
Self::Approvals, Self::Approvals,
Self::Scheduling, Self::Scheduling,
Self::Diagnostics,
Self::Forge, Self::Forge,
Self::Execution, Self::Execution,
Self::WebTools, Self::WebTools,
@ -329,18 +324,13 @@ impl ToolGroup {
"get_agent_meta — identity introspection (set_status is always available)" "get_agent_meta — identity introspection (set_status is always available)"
} }
Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling", Self::Inbox => "get_loose_ends, cancel_loose_end, remind — self-scheduling",
Self::Lifecycle => { Self::Lifecycle => "list_containers — own-subtree container listing (privileged)",
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
}
Self::Approvals => { Self::Approvals => {
"request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)" "request_update_meta_inputs — operator-approved meta-flake input bumps (privileged)"
} }
Self::Scheduling => { Self::Scheduling => {
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)" "request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"
} }
Self::Diagnostics => {
"get_logs — read a sub-agent container's systemd journal (privileged)"
}
Self::Forge => { Self::Forge => {
"create_repo — create git repos through hive-c0re (operator-gated merge)" "create_repo — create git repos through hive-c0re (operator-gated merge)"
} }