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