hivectl: rename hivectl agents to hivectl agent <name> <verb>

This commit is contained in:
damocles 2026-07-27 19:00:44 +02:00 committed by mara
commit 03afbd1316
19 changed files with 367 additions and 525 deletions

View file

@ -26,7 +26,7 @@ above was spawned by `alice`), and the operator can reparent any agent. The
bootstrap container (`ruth`) is just another root. Re-parenting is
operator-driven:
- CLI: `hivectl agents set-parent <child> --parent <new>` (or `--root` to
- CLI: `hivectl agent <child> set-parent --parent <new>` (or `--root` to
promote). Exactly one of `--parent` / `--root` is required.
- Dashboard: `POST /api/topology/set-parent` (form fields `child`,
optional `new_parent` — absent / empty ⇒ promote to root).

View file

@ -119,7 +119,7 @@ kind-specific payload carrier.
- `Spawn` — direct container creation from the agent's config repo.
`commit_ref` is empty. Submitted via `HostRequest::RequestSpawn`
(operator-gated, the `◆ R3QU3ST SP4WN` dashboard button +
`hivectl agents request-spawn` CLI). The host-level `HostRequest::Spawn`
`hivectl agent <name> request-spawn` CLI). The host-level `HostRequest::Spawn`
variant bypasses the approval queue entirely — privileged-context use
only (operator on the host shell, test scripts, one-off recoveries).
This is the **canonical first-spawn**: a new agent's `InitConfig`

View file

@ -212,7 +212,7 @@ bind-mount, and that's the whole design: the harness stats it
in-container via `hive_sh4re::paths::paused_marker`, while hive-c0re
stats it on the host (`Coordinator::is_paused`) to populate the
`paused` field on the agent card, and creates/removes it
(`Coordinator::set_paused`) for `hivectl agents pause|resume` and the
(`Coordinator::set_paused`) for `hivectl agent <name> pause|resume` and the
dashboard toggle. Because the file itself is the only shared state
there's no protocol between them, no round-trip into the container, and
pause keeps working when the harness is wedged or the container is
@ -339,7 +339,7 @@ step would re-fire).
`R3V1V3` queues a Spawn approval that reuses the kept state on
approve (no re-login).
- `PURG3` (opt-in via the dashboard button or
`hivectl agents destroy --purge <name>`) — DESTR0Y plus wipes
`hivectl agent <name> destroy --purge`) — DESTR0Y plus wipes
`/var/lib/hyperhive/{agents,applied}/<name>/`. Config history,
claude creds, /state/ notes, and the harness dir are all gone.
No undo.

View file

@ -79,13 +79,13 @@ See [`approvals.md`](approvals.md) for the full flow.
```bash
# Roster: all agents, status, rev, parent, pending reminders
hivectl agents list
hivectl list-agents
# Restart a stuck container (no rebuild)
hivectl agents restart <agent>
hivectl agent <agent> restart
# Open a Claude session inside an agent's container
hivectl choom <agent>
hivectl agent <agent> choom
# Open hive web surfaces in a browser (or just print the URLs)
hivectl open # operator dashboard

View file

@ -20,29 +20,29 @@ This document contains the help content for the `hivectl` command-line program.
* [`hivectl gateway create-user`↴](#hivectl-gateway-create-user)
* [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user)
* [`hivectl gateway list-users`↴](#hivectl-gateway-list-users)
* [`hivectl agents`↴](#hivectl-agents)
* [`hivectl agents list`↴](#hivectl-agents-list)
* [`hivectl agents restart`↴](#hivectl-agents-restart)
* [`hivectl agents restart-all`↴](#hivectl-agents-restart-all)
* [`hivectl agents pause`↴](#hivectl-agents-pause)
* [`hivectl agents resume`↴](#hivectl-agents-resume)
* [`hivectl agents spawn`↴](#hivectl-agents-spawn)
* [`hivectl agents request-spawn`↴](#hivectl-agents-request-spawn)
* [`hivectl agents kill`↴](#hivectl-agents-kill)
* [`hivectl agents destroy`↴](#hivectl-agents-destroy)
* [`hivectl agents rebuild`↴](#hivectl-agents-rebuild)
* [`hivectl agents set-parent`↴](#hivectl-agents-set-parent)
* [`hivectl agents set-limits`↴](#hivectl-agents-set-limits)
* [`hivectl agents quota`↴](#hivectl-agents-quota)
* [`hivectl agents quota enable`↴](#hivectl-agents-quota-enable)
* [`hivectl agents quota show`↴](#hivectl-agents-quota-show)
* [`hivectl agents quota set`↴](#hivectl-agents-quota-set)
* [`hivectl agents subvol`↴](#hivectl-agents-subvol)
* [`hivectl agents subvol upgrade`↴](#hivectl-agents-subvol-upgrade)
* [`hivectl agents subvol snapshot`↴](#hivectl-agents-subvol-snapshot)
* [`hivectl agents subvol snapshot create`↴](#hivectl-agents-subvol-snapshot-create)
* [`hivectl agents subvol snapshot delete`↴](#hivectl-agents-subvol-snapshot-delete)
* [`hivectl agents subvol snapshot send`↴](#hivectl-agents-subvol-snapshot-send)
* [`hivectl agent`↴](#hivectl-agent)
* [`hivectl agent restart`↴](#hivectl-agent-restart)
* [`hivectl agent pause`↴](#hivectl-agent-pause)
* [`hivectl agent resume`↴](#hivectl-agent-resume)
* [`hivectl agent spawn`↴](#hivectl-agent-spawn)
* [`hivectl agent request-spawn`↴](#hivectl-agent-request-spawn)
* [`hivectl agent kill`↴](#hivectl-agent-kill)
* [`hivectl agent destroy`↴](#hivectl-agent-destroy)
* [`hivectl agent rebuild`↴](#hivectl-agent-rebuild)
* [`hivectl agent set-parent`↴](#hivectl-agent-set-parent)
* [`hivectl agent set-limits`↴](#hivectl-agent-set-limits)
* [`hivectl agent choom`↴](#hivectl-agent-choom)
* [`hivectl agent quota`↴](#hivectl-agent-quota)
* [`hivectl agent quota show`↴](#hivectl-agent-quota-show)
* [`hivectl agent quota set`↴](#hivectl-agent-quota-set)
* [`hivectl agent subvol`↴](#hivectl-agent-subvol)
* [`hivectl agent subvol upgrade`↴](#hivectl-agent-subvol-upgrade)
* [`hivectl agent subvol snapshot`↴](#hivectl-agent-subvol-snapshot)
* [`hivectl agent subvol snapshot create`↴](#hivectl-agent-subvol-snapshot-create)
* [`hivectl agent subvol snapshot delete`↴](#hivectl-agent-subvol-snapshot-delete)
* [`hivectl agent subvol snapshot send`↴](#hivectl-agent-subvol-snapshot-send)
* [`hivectl list-agents`↴](#hivectl-list-agents)
* [`hivectl quota-enable`↴](#hivectl-quota-enable)
* [`hivectl approvals`↴](#hivectl-approvals)
* [`hivectl approvals pending`↴](#hivectl-approvals-pending)
* [`hivectl approvals approve`↴](#hivectl-approvals-approve)
@ -52,7 +52,6 @@ This document contains the help content for the `hivectl` command-line program.
* [`hivectl wg peer`↴](#hivectl-wg-peer)
* [`hivectl wg status`↴](#hivectl-wg-status)
* [`hivectl peer-config`↴](#hivectl-peer-config)
* [`hivectl choom`↴](#hivectl-choom)
* [`hivectl stop`↴](#hivectl-stop)
* [`hivectl start`↴](#hivectl-start)
* [`hivectl restart`↴](#hivectl-restart)
@ -71,11 +70,12 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
* `matrix` — matrix-tuwunel user provisioning
* `github` — GitHub account provisioning
* `gateway` — Gateway htpasswd user management
* `agents` — Agent container management
* `agent` — Lifecycle actions on ONE managed agent container. Needs the hive-c0re daemon running
* `list-agents` — Show all managed agents with their status and technical state
* `quota-enable` — Enable btrfs qgroup accounting on the agent-state filesystem
* `approvals` — Operator approval queue: list, approve, or deny pending requests
* `wg` — WireGuard inter-hive mesh setup helpers
* `peer-config` — Generate the federation peer-config block for THIS hive
* `choom` — Open an interactive Claude session inside an agent container
* `stop` — Stop containers hive-wide in one operator action
* `start` — Start containers hive-wide — the inverse of `hivectl stop`
* `restart` — Restart containers hive-wide — `stop` then `start` over one scope
@ -84,7 +84,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
###### **Options:**
* `--socket <SOCKET>` — Path to the hive-c0re host admin socket, used by the daemon-assisted verbs (`agents`, `stop`, `start`). Global: accepted before or after the subcommand. Verbs that don't talk to the daemon ignore it
* `--socket <SOCKET>` — Path to the hive-c0re host admin socket, used by the daemon-assisted verbs (`agent`, `list-agents`, `stop`, `start`). Global: accepted before or after the subcommand. Verbs that don't talk to the daemon ignore it
Default value: `/run/hyperhive/host.sock`
@ -327,54 +327,41 @@ List all gateway htpasswd usernames, one per line
## `hivectl agents`
## `hivectl agent`
Agent container management.
Lifecycle actions on ONE managed agent container. Needs the hive-c0re daemon running.
Lifecycle actions on managed agent containers. Needs the hive-c0re daemon running.
Everything here targets a single named agent (`hivectl agent foo restart`, `hivectl agent foo choom`, …) — anything that acts hive-wide lives at the top level instead (`list-agents`, `restart`/`stop`/`start` with a scope, `quota-enable`).
**Usage:** `hivectl agents <COMMAND>`
**Usage:** `hivectl agent <NAME> <COMMAND>`
###### **Subcommands:**
* `list` — Show all managed agents with their status and technical state
* `restart` — Stop and start a single agent container without rebuilding config
* `restart-all` — Restart all managed agent containers
* `pause` — Park an agent's turn loop, leaving the container running
* `resume` — Resume a paused agent — it drains whatever queued up while parked
* `spawn` — Spawn a new agent container directly, bypassing the approval queue
* `restart` — Stop and start this agent container without rebuilding config
* `pause` — Park this agent's turn loop, leaving the container running
* `resume` — Resume this paused agent — it drains whatever queued up while parked
* `spawn` — Spawn this agent container directly, bypassing the approval queue
* `request-spawn` — Queue a spawn request for operator approval
* `kill` — Stop a managed container (graceful)
* `destroy` — Tear down a sub-agent container, keeping its state by default. No undo
* `rebuild` — Apply pending config to a managed container
* `set-parent` — Move an agent in the topology tree — under a new parent, or to root
* `set-limits` — Declare an agent's CPU/memory limits, overriding the hive-wide defaults
* `quota` — Per-agent disk accounting + optional quotas via btrfs qgroups
* `subvol` — btrfs subvolume management for agent state dirs
## `hivectl agents list`
Show all managed agents with their status and technical state
**Usage:** `hivectl agents list [OPTIONS]`
###### **Options:**
* `--json` — Emit the raw JSON rows instead of the padded table (for scripting). The table is the default human-readable shape
## `hivectl agents restart`
Stop and start a single agent container without rebuilding config
**Usage:** `hivectl agents restart [OPTIONS] <NAME>`
* `kill` — Stop this managed container (graceful)
* `destroy` — Tear down this sub-agent container, keeping its state by default. No undo
* `rebuild` — Apply pending config to this managed container
* `set-parent` — Move this agent in the topology tree — under a new parent, or to root
* `set-limits` — Declare this agent's CPU/memory limits, overriding the hive-wide defaults
* `choom` — Open an interactive Claude session inside this agent's container
* `quota` — This agent's disk accounting + optional quota via btrfs qgroups
* `subvol` — btrfs subvolume management for this agent's state dir
###### **Arguments:**
* `<NAME>` — Agent name (e.g. `damocles`, `ruth`)
* `<NAME>` — Agent name (e.g. `damocles`, `iris`)
## `hivectl agent restart`
Stop and start this agent container without rebuilding config
**Usage:** `hivectl agent restart [OPTIONS]`
###### **Options:**
@ -382,91 +369,55 @@ Stop and start a single agent container without rebuilding config
## `hivectl agents restart-all`
## `hivectl agent pause`
Restart all managed agent containers
**Usage:** `hivectl agents restart-all [OPTIONS]`
###### **Options:**
* `--no-wait` — Return immediately after the restart DAGs are queued
## `hivectl agents pause`
Park an agent's turn loop, leaving the container running.
Park this agent's turn loop, leaving the container running.
The harness stops driving turns but keeps serving its web UI and MCP daemons, so the container, its mounts and its warm caches stay up while it burns no tokens. Inbox messages queue unacked and the backlog drains on `resume`. Sticky: it survives a restart, and pausing a stopped agent makes it come up paused.
**Usage:** `hivectl agents pause <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent pause`
## `hivectl agents resume`
## `hivectl agent resume`
Resume a paused agent — it drains whatever queued up while parked
Resume this paused agent — it drains whatever queued up while parked
**Usage:** `hivectl agents resume <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent resume`
## `hivectl agents spawn`
## `hivectl agent spawn`
Spawn a new agent container directly, bypassing the approval queue.
Spawn this agent container directly, bypassing the approval queue.
Operator-on-the-host only; use `request-spawn` for an approval-gated spawn.
**Usage:** `hivectl agents spawn <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name (e.g. `iris`)
**Usage:** `hivectl agent spawn`
## `hivectl agents request-spawn`
## `hivectl agent request-spawn`
Queue a spawn request for operator approval
**Usage:** `hivectl agents request-spawn <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent request-spawn`
## `hivectl agents kill`
## `hivectl agent kill`
Stop a managed container (graceful)
Stop this managed container (graceful)
**Usage:** `hivectl agents kill <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent kill`
## `hivectl agents destroy`
## `hivectl agent destroy`
Tear down a sub-agent container, keeping its state by default. No undo
Tear down this sub-agent container, keeping its state by default. No undo
**Usage:** `hivectl agents destroy [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent destroy [OPTIONS]`
###### **Options:**
@ -474,46 +425,34 @@ Tear down a sub-agent container, keeping its state by default. No undo
## `hivectl agents rebuild`
## `hivectl agent rebuild`
Apply pending config to a managed container
Apply pending config to this managed container
**Usage:** `hivectl agents rebuild <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent rebuild`
## `hivectl agents set-parent`
## `hivectl agent set-parent`
Move an agent in the topology tree — under a new parent, or to root
Move this agent in the topology tree — under a new parent, or to root
**Usage:** `hivectl agents set-parent [OPTIONS] <CHILD>`
###### **Arguments:**
* `<CHILD>` — Agent to move
**Usage:** `hivectl agent set-parent [OPTIONS]`
###### **Options:**
* `--parent <PARENT>` — New parent agent name. Mutually exclusive with `--root`
* `--root` — Promote `child` to root (no parent)
* `--root` — Promote this agent to root (no parent)
## `hivectl agents set-limits`
## `hivectl agent set-limits`
Declare an agent's CPU/memory limits, overriding the hive-wide defaults.
Declare this agent's CPU/memory limits, overriding the hive-wide defaults.
Replaces the agent's whole override entry rather than merging into it: any limit you don't pass returns to the hive-wide default. To change one and keep the other, pass both. Disk is a separate resource with its own group — see `agents quota`.
Replaces the agent's whole override entry rather than merging into it: any limit you don't pass returns to the hive-wide default. To change one and keep the other, pass both. Disk is a separate resource with its own group — see `quota`.
**Usage:** `hivectl agents set-limits [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name
**Usage:** `hivectl agent set-limits [OPTIONS]`
###### **Options:**
@ -523,85 +462,77 @@ Replaces the agent's whole override entry rather than merging into it: any limit
## `hivectl agents quota`
## `hivectl agent choom`
Per-agent disk accounting + optional quotas via btrfs qgroups.
Open an interactive Claude session inside this agent's container.
Opt-in: enable qgroup accounting, then report per-agent usage or cap an agent. No-op on non-btrfs hosts.
A fresh session by default, or resume a prior one. Requires root and a running container.
**Usage:** `hivectl agents quota <COMMAND>`
**Usage:** `hivectl agent choom [OPTIONS]`
###### **Options:**
* `--resume <SESSION>` — Resume a prior claude session by its session id, passed through as `claude --resume <value>` (claude's `--continue` takes no value — it resumes the cwd's latest session, which is the harness's, so choom never uses it; this flag matches the claude flag it maps to). Omit for a fresh blank session. A value is required when the flag is given
## `hivectl agent quota`
This agent's disk accounting + optional quota via btrfs qgroups.
Needs `hivectl quota-enable` run once hive-wide first. No-op on non-btrfs hosts.
**Usage:** `hivectl agent quota <COMMAND>`
###### **Subcommands:**
* `enable` — Enable btrfs qgroup accounting on the agent-state filesystem
* `show` — Report per-agent disk usage from btrfs qgroups (all agents, or one by name)
* `set` — Set or clear an agent's disk-usage quota
* `show` — Report this agent's disk usage from btrfs qgroups
* `set` — Set or clear this agent's disk-usage quota
## `hivectl agents quota enable`
## `hivectl agent quota show`
Enable btrfs qgroup accounting on the agent-state filesystem.
Report this agent's disk usage from btrfs qgroups
Run once before `show` / `limit`. No-op on non-btrfs hosts.
**Usage:** `hivectl agents quota enable`
**Usage:** `hivectl agent quota show`
## `hivectl agents quota show`
## `hivectl agent quota set`
Report per-agent disk usage from btrfs qgroups (all agents, or one by name)
Set or clear this agent's disk-usage quota
**Usage:** `hivectl agents quota show [NAME]`
**Usage:** `hivectl agent quota set <SIZE>`
###### **Arguments:**
* `<NAME>` — Agent to show (omit for all agents with a state subvolume)
## `hivectl agents quota set`
Set or clear an agent's disk-usage quota.
Named `set` rather than `set-quota` because the enclosing `quota` group already carries the noun — `agents quota set iris 5G`. The `set-<noun>` spelling stays for the flat verbs (`set-parent`, `set-limits`), which have no group to inherit it from.
**Usage:** `hivectl agents quota set <NAME> <SIZE>`
###### **Arguments:**
* `<NAME>` — Agent whose state subvolume to limit
* `<SIZE>` — Size cap (`5G`, `500M`, `1073741824`) or `none` to clear
## `hivectl agents subvol`
## `hivectl agent subvol`
btrfs subvolume management for agent state dirs.
btrfs subvolume management for this agent's state dir.
Upgrade an existing plain-dir agent's state into a btrfs subvolume so it gains snapshots and per-subvol usage/quota.
**Usage:** `hivectl agents subvol <COMMAND>`
**Usage:** `hivectl agent subvol <COMMAND>`
###### **Subcommands:**
* `upgrade` — Convert a plain-dir agent state root into a btrfs subvolume in place, so it gains snapshots and per-subvol usage/quota
* `snapshot` — Read-only snapshots of an agent's state subvolume
* `snapshot` — Read-only snapshots of this agent's state subvolume
## `hivectl agents subvol upgrade`
## `hivectl agent subvol upgrade`
Convert a plain-dir agent state root into a btrfs subvolume in place, so it gains snapshots and per-subvol usage/quota.
Bounces the agent to migrate its state, so it requires `--yes`.
**Usage:** `hivectl agents subvol upgrade [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name (e.g. `damocles`, `iris`)
**Usage:** `hivectl agent subvol upgrade [OPTIONS]`
###### **Options:**
@ -609,11 +540,11 @@ Bounces the agent to migrate its state, so it requires `--yes`.
## `hivectl agents subvol snapshot`
## `hivectl agent subvol snapshot`
Read-only snapshots of an agent's state subvolume
Read-only snapshots of this agent's state subvolume
**Usage:** `hivectl agents subvol snapshot <COMMAND>`
**Usage:** `hivectl agent subvol snapshot <COMMAND>`
###### **Subcommands:**
@ -623,15 +554,11 @@ Read-only snapshots of an agent's state subvolume
## `hivectl agents subvol snapshot create`
## `hivectl agent subvol snapshot create`
Create a read-only snapshot (agent must already be a subvolume)
**Usage:** `hivectl agents subvol snapshot create --label <LABEL> <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name (e.g. `damocles`, `iris`)
**Usage:** `hivectl agent subvol snapshot create --label <LABEL>`
###### **Options:**
@ -639,28 +566,26 @@ Create a read-only snapshot (agent must already be a subvolume)
## `hivectl agents subvol snapshot delete`
## `hivectl agent subvol snapshot delete`
Delete a snapshot created by `subvol snapshot create`
**Usage:** `hivectl agents subvol snapshot delete <NAME> <LABEL>`
**Usage:** `hivectl agent subvol snapshot delete <LABEL>`
###### **Arguments:**
* `<NAME>` — Agent name the snapshot belongs to
* `<LABEL>` — Snapshot label passed to `subvol snapshot create --label`
## `hivectl agents subvol snapshot send`
## `hivectl agent subvol snapshot send`
Export a snapshot to a local file via `btrfs send` (the local-file half of inter-hive migration transport; the cross-hive `ssh ... btrfs receive` leg isn't wired up yet). Also useful standalone as a point-in-time backup: a full send with no `--parent` produces a self-contained archive of the snapshot
**Usage:** `hivectl agents subvol snapshot send [OPTIONS] --dest <DEST> <NAME> <LABEL>`
**Usage:** `hivectl agent subvol snapshot send [OPTIONS] --dest <DEST> <LABEL>`
###### **Arguments:**
* `<NAME>` — Agent name the snapshot belongs to
* `<LABEL>` — Snapshot label passed to `subvol snapshot create --label`
###### **Options:**
@ -670,6 +595,30 @@ Export a snapshot to a local file via `btrfs send` (the local-file half of inter
## `hivectl list-agents`
Show all managed agents with their status and technical state.
Global — not scoped to one agent, so it lives at the top level rather than under `hivectl agent <name>`. Needs the hive-c0re daemon running.
**Usage:** `hivectl list-agents [OPTIONS]`
###### **Options:**
* `--json` — Emit the raw JSON rows instead of the padded table (for scripting). The table is the default human-readable shape
## `hivectl quota-enable`
Enable btrfs qgroup accounting on the agent-state filesystem.
Global one-shot toggle (not per-agent), hence top-level rather than under `hivectl agent <name>`. Idempotent — safe to re-run. Once enabled, `hivectl agent <name> quota show`/`quota set` work.
**Usage:** `hivectl quota-enable`
## `hivectl approvals`
Operator approval queue: list, approve, or deny pending requests.
@ -787,24 +736,6 @@ Prints the nix a peer operator pastes into their swarm config to trust and reach
## `hivectl choom`
Open an interactive Claude session inside an agent container.
A fresh session by default, or resume a prior one. Requires root and a running container.
**Usage:** `hivectl choom [OPTIONS] <NAME>`
###### **Arguments:**
* `<NAME>` — Agent name (e.g. `damocles`, `iris`)
###### **Options:**
* `--resume <SESSION>` — Resume a prior claude session by its session id, passed through as `claude --resume <value>` (claude's `--continue` takes no value — it resumes the cwd's latest session, which is the harness's, so choom never uses it; this flag matches the claude flag it maps to). Omit for a fresh blank session. A value is required when the flag is given
## `hivectl stop`
Stop containers hive-wide in one operator action.

View file

@ -132,18 +132,19 @@ username updates the password hash in place.
## Agents
Container lifecycle shortcuts that go through the host admin socket.
Requires the `hive-c0re` daemon to be running.
Requires the `hive-c0re` daemon to be running. Everything scoped to a
single agent lives under `hivectl agent <name> <verb>` — the name is
hoisted onto the parent command, so none of the verbs below repeat it.
```bash
hivectl agents list # roster: every agent's status + technical state
hivectl agents list --json # same data as raw JSON rows (for scripting)
hivectl agents restart iris # stop + start the `iris` container (no rebuild)
hivectl agents restart-all # stop + start every managed agent container in sequence
hivectl agents pause iris # park iris's turn loop, leave the container running
hivectl agents resume iris # let it drive turns again, draining what queued up
hivectl list-agents # roster: every agent's status + technical state
hivectl list-agents --json # same data as raw JSON rows (for scripting)
hivectl agent iris restart # stop + start the `iris` container (no rebuild)
hivectl agent iris pause # park iris's turn loop, leave the container running
hivectl agent iris resume # let it drive turns again, draining what queued up
```
`list` prints a padded table with one row per managed agent —
`list-agents` prints a padded table with one row per managed agent —
`NAME STATUS REV PARENT REMIND`. STATUS collapses the health flags
(`running` / `stopped`, plus ` paused` / ` needs-login` / ` needs-update`
when set — `paused` is orthogonal to running, see below);
@ -153,10 +154,13 @@ pending reminders. It reuses the same per-agent aggregation the dashboard
renders, so the CLI roster and the web UI never drift. `--json` emits the
raw rows instead of the table.
`restart` is the manual equivalent of the MCP `restart` tool — useful
when you need to kick a container from the host without going through
the agent hierarchy. Failures on `restart-all` are collected and
reported at the end rather than aborting mid-run.
`agent <name> restart` is the manual equivalent of the MCP `restart`
tool — useful when you need to kick a container from the host without
going through the agent hierarchy. For more than one agent at once, use
the top-level `hivectl restart --agents` (or `--agent <name>` repeated) —
it rides a single DAG and reports per-target failures at the end rather
than aborting mid-run, instead of shelling out to `agent <name> restart`
in a loop.
`pause` / `resume` are the "stop burning tokens without losing the
container" pair. Pausing writes a marker file into the agent's harness
@ -175,15 +179,15 @@ resume drains the backlog rather than dropping it. Points worth knowing:
handshake is skipped for a paused agent (it would never answer), which
is safe precisely because the pause check sits at the top of the loop:
a paused agent has no turn in flight to checkpoint.
- Visible as ` paused` in `agents list`'s STATUS column, as a `paused`
- Visible as ` paused` in `list-agents`'s STATUS column, as a `paused`
field on the JSON rows, and as a badge on the dashboard card.
### Per-agent resource limits
```bash
hivectl agents set-limits sock --cpu-quota 400% --memory-max 8G
hivectl agents set-limits sock --memory-max 8G # CPU falls back to the hive default
hivectl agents set-limits sock --reset # drop all overrides
hivectl agent sock set-limits --cpu-quota 400% --memory-max 8G
hivectl agent sock set-limits --memory-max 8G # CPU falls back to the hive default
hivectl agent sock set-limits --reset # drop all overrides
```
Overrides the hive-wide `services.hyperhive.agentCpuQuota` /
@ -224,8 +228,8 @@ tells you it needs root, instead of reporting a permission problem with
the state dir.
```bash
hivectl choom iris # fresh blank Claude session in iris's container
hivectl choom iris --resume <session-id> # rejoin a prior session by id
hivectl agent iris choom # fresh blank Claude session in iris's container
hivectl agent iris choom --resume <session-id> # rejoin a prior session by id
```
Bare `choom` starts a fresh blank session. `--resume <value>` passes

View file

@ -15,7 +15,7 @@ agents) runs:
losing it; reminders and todo wakes buffer in their channels. The
check runs before the self-continue slot is consumed, so a pending
`request_next_turn` survives the pause. Set it with
`hivectl agents pause <name>` or the dashboard toggle; see
`hivectl agent <name> pause` or the dashboard toggle; see
[persistence](persistence.md#-harnesspaused-per-agent).
1. Long-poll `Recv` on its socket. The host-side broker
(`broker.rs::recv_blocking_batch`) returns immediately if there's

View file

@ -295,7 +295,7 @@ pub async fn graceful_restart(
/// `Reconcile` (nothing to stop). Restart never writes `wanted`, so the
/// tail `Reconcile` converges each agent to its EXISTING intent — a
/// deliberately-stopped agent stays down. The whole hive-wide
/// `hivectl restart` / `restart-all` is one DAG.
/// `hivectl restart` is one DAG.
pub async fn restart_many(
coord: &Arc<Coordinator>,
agents: &[String],

View file

@ -104,7 +104,6 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
HostRequest::SetPaused { name, paused } => {
handle_set_paused(&coord, name, *paused).await
}
HostRequest::RestartAll => handle_restart_all(&coord).await?,
HostRequest::RestartScoped { scope, graceful } => {
handle_restart_scoped(&coord, scope, *graceful).await?
}
@ -447,7 +446,7 @@ async fn handle_set_resource_limits(
"{name}: CPUQuota={cpu} MemoryMax={mem} — the cgroup cap itself is live now (restart \
the container if it's running and needs the new cap immediately), but the derived \
Claude/JSC heap ceiling is baked in at build time, so it needs a REBUILD \
(`hivectl agents rebuild {name}`) to actually track this change"
(`hivectl agent {name} rebuild`) to actually track this change"
)]))
}
@ -612,7 +611,7 @@ async fn handle_quota_show(name: Option<&str>) -> Result<HostResponse> {
// varies across btrfs-progs versions).
if msg.to_ascii_lowercase().contains("quota not enabled") {
return Ok(HostResponse::error(
"btrfs quota not enabled — run `hivectl agents quota enable` first",
"btrfs quota not enabled — run `hivectl quota-enable` first",
));
}
// A plain-dir agent (no subvolume) has no qgroup; note it
@ -787,41 +786,11 @@ async fn submit_single(coord: &Arc<Coordinator>, name: &str, verb: Verb) -> Host
HostResponse::queued(vec![id])
}
/// Restart every container in **one** DAG — a per-agent restart subgraph
/// each, running concurrently on their own leases (so unrelated agents'
/// restarts overlap while nothing races an in-flight rebuild). Returns
/// once queued; per-node progress surfaces on the single DAG.
async fn handle_restart_all(coord: &Arc<Coordinator>) -> Result<HostResponse> {
tracing::info!("restart-all");
let containers = lifecycle::list().await?;
let agents: Vec<String> = containers
.iter()
.filter_map(|a| a.strip_prefix(lifecycle::AGENT_PREFIX).map(str::to_owned))
.collect();
let queued = if agents.is_empty() {
Vec::new()
} else {
vec![
crate::job_queue::submit::restart_many(
coord,
&agents,
false,
crate::job_queue::Source::Manual,
"manual restart via hivectl restart-all".to_owned(),
)
.await,
]
};
let mut resp = HostResponse::list(agents);
resp.queued_dags = Some(queued);
Ok(resp)
}
/// Stop the given `agents` (resolved logical names) then `infra` containers
/// (`hivectl stop`). Agents go down before infra so they're not mid-request
/// against a forge/matrix that's already gone. Per-target failures are
/// aggregated rather than aborting on the first error, mirroring
/// `handle_restart_all`. Callers resolve the [`LifecycleScope`] to these
/// `finish_lifecycle` below. Callers resolve the [`LifecycleScope`] to these
/// explicit name lists up front — this never sees the "all" flag.
///
/// Every agent rides the job queue: a `graceful` stop submits the
@ -1105,7 +1074,7 @@ fn scoped_infra(scope: &LifecycleScope) -> Vec<InfraContainer> {
/// Build the aggregated lifecycle response: `ok` with the touched names when
/// every target succeeded, otherwise `ok: false` with the joined errors and
/// the partial success list (matches `handle_restart_all`).
/// the partial success list.
fn finish_lifecycle(ok_items: Vec<String>, errors: &[String]) -> HostResponse {
if errors.is_empty() {
HostResponse::list(ok_items)

View file

@ -112,10 +112,6 @@ pub enum HostRequest {
/// `true` pauses, `false` resumes. Idempotent either way.
paused: bool,
},
/// Stop and restart all managed containers in sequence. Convenience
/// wrapper for `hivectl agents restart-all`; iterates the live
/// container list and restarts each one.
RestartAll,
/// Restart containers hive-wide (`hivectl restart`), scoped like
/// `Stop`/`Start`. Each targeted agent rides exactly one DAG
/// server-side — the `Restart` template (mechanical stop + reconcile),
@ -146,7 +142,7 @@ pub enum HostRequest {
/// persistent state dir under the agents root. Answered daemon-side
/// because that root is `0700 hive-core`: a client stat-ing it
/// without root gets EACCES, so the pre-flight "does this agent
/// exist?" guard in front of `hivectl choom` / `agents subvol` used
/// exist?" guard in front of `hivectl agent <name> choom` / `subvol` used
/// to fail with a permission error instead of an answer. The daemon
/// already runs as the owning user and does the same check for its
/// own provisioning paths. Result: [`HostResponse::agent_exists`].
@ -157,7 +153,7 @@ pub enum HostRequest {
AgentExists { name: Ident },
/// List managed agents with their full status + technical state
/// (running / needs-login / needs-update / deployed sha / parent /
/// pending reminders) — the `hivectl agents list` roster view.
/// pending reminders) — the `hivectl list-agents` roster view.
/// Reuses the dashboard's per-agent `ContainerView` aggregation.
AgentStatus,
/// Report this hive's canonical DNS domain
@ -309,43 +305,46 @@ pub enum HostRequest {
/// the agent's `gh` wrapper / git credential helper — no rebuild needed.
SetAgentGithubToken { agent: Ident, token: String },
/// Turn on btrfs qgroup accounting on the agent-state filesystem, via
/// the privileged helper. Daemon-side equivalent of `hivectl agents quota
/// enable`. Returns advisory lines in [`HostResponse::messages`].
/// the privileged helper. Daemon-side equivalent of `hivectl
/// quota-enable`. Returns advisory lines in [`HostResponse::messages`].
QuotaEnable,
/// Set (or, with `limit = None`, clear) an agent's btrfs disk quota via
/// the privileged helper. Daemon-side equivalent of `hivectl agents quota
/// set`; the size string is parsed to bytes client-side. Returns a
/// bare success — the client prints the confirmation from the value it
/// sent.
/// the privileged helper. Daemon-side equivalent of `hivectl agent
/// <name> quota set`; the size string is parsed to bytes client-side.
/// Returns a bare success — the client prints the confirmation from
/// the value it sent.
QuotaLimit {
name: Ident,
#[serde(default)]
limit: Option<u64>,
},
/// Report per-agent btrfs qgroup usage (`hivectl agents quota show [name]`).
/// The daemon resolves the agent set (all kept state dirs when `name`
/// is absent) and reads each subvolume's referenced/exclusive usage via
/// the privileged helper. Result rows land in [`HostResponse::quota`];
/// a "btrfs quota not enabled" error short-circuits the whole sweep as a
/// plain [`HostResponse::error`] so the client can print the enable hint.
/// Report btrfs qgroup usage (`hivectl agent <name> quota show`, or all
/// kept state dirs when `name` is absent — no CLI path reaches the
/// absent case today, but the daemon still supports it). The daemon
/// resolves the agent set and reads each subvolume's referenced/
/// exclusive usage via the privileged helper. Result rows land in
/// [`HostResponse::quota`]; a "btrfs quota not enabled" error
/// short-circuits the whole sweep as a plain [`HostResponse::error`]
/// so the client can print the enable hint.
QuotaShow {
#[serde(default)]
name: Option<Ident>,
},
/// Migrate an agent's plain state dir to a btrfs subvolume via the
/// privileged helper (`hivectl agents subvol upgrade`). The agent MUST already
/// be stopped — the client orchestrates stop → this → start. Returns a
/// bare success; the client prints its own progress lines.
/// privileged helper (`hivectl agent <name> subvol upgrade`). The agent
/// MUST already be stopped — the client orchestrates stop → this →
/// start. Returns a bare success; the client prints its own progress
/// lines.
UpgradeSubvolume { name: Ident },
/// Create a read-only btrfs snapshot of an agent's state subvolume
/// (`hivectl agents subvol snapshot create`). `label` is validated client-side
/// AND by hive-priv. Returns the snapshot's host path in
/// [`HostResponse::messages`].
/// (`hivectl agent <name> subvol snapshot create`). `label` is
/// validated client-side AND by hive-priv. Returns the snapshot's
/// host path in [`HostResponse::messages`].
SnapshotSubvolume { name: Ident, label: String },
/// Delete a snapshot created by `SnapshotSubvolume` (`hivectl agents subvol
/// Delete a snapshot created by `SnapshotSubvolume` (`hivectl agent <name> subvol
/// snapshot delete`). Bare success; the client prints the confirmation.
DeleteSnapshot { name: Ident, label: String },
/// Export a snapshot to a local file via `btrfs send` (`hivectl agents subvol
/// Export a snapshot to a local file via `btrfs send` (`hivectl agent <name> subvol
/// snapshot send`). `dest` is a bare filename (hive-priv rejects paths);
/// `parent` names an optional parent snapshot for an incremental send.
/// Returns the written file's host path in [`HostResponse::messages`].

View file

@ -1273,7 +1273,7 @@ async fn snapshot_agent_subvolume(
let agent_root = PathBuf::from(AGENT_STATE_ROOT).join(agent_name);
if !is_btrfs_subvolume(&agent_root) {
bail!(
"{} is not a btrfs subvolume — nothing to snapshot (run `hivectl agents subvol upgrade` first)",
"{} is not a btrfs subvolume — nothing to snapshot (run `hivectl agent <name> subvol upgrade` first)",
agent_root.display()
);
}

View file

@ -364,10 +364,10 @@ pub struct ContainerInfo {
/// One row in a `HostRequest::AgentStatus` result — the operator-CLI
/// projection of the dashboard's per-agent `ContainerView`. Carries the
/// agent's running/health flags plus the technical state an operator
/// wants in a roster overview (`hivectl agents list`).
/// wants in a roster overview (`hivectl list-agents`).
//
// Four orthogonal, independently-observed facts about one agent, each
// rendered as its own column/token by `hivectl agents list` and read
// rendered as its own column/token by `hivectl list-agents` and read
// individually by `--json` consumers. Any combination is meaningful
// (a stopped agent can be paused and need an update), so folding them
// into a state machine or nested flag structs would only add

View file

@ -1,15 +1,18 @@
//! `hivectl agents` — everything scoped to a managed agent: container
//! lifecycle over the host admin socket
//! (list/restart/restart-all/pause/resume/spawn/kill/destroy/rebuild/
//! set-parent/set-limits), plus the `quota` and `subvol` groups, whose
//! handlers live in their own modules.
//! `hivectl agent <name>` — everything scoped to ONE managed agent:
//! container lifecycle over the host admin socket (restart/pause/resume/
//! spawn/kill/destroy/rebuild/set-parent/set-limits/choom), plus the
//! `quota` and `subvol` groups, whose handlers live in their own modules.
//! `agents_list` (`hivectl list-agents`) is the one genuinely hive-wide
//! read that lives in this module too since it shares the same daemon
//! request as everything else here, even though it's dispatched from a
//! top-level `Cmd` variant, not `AgentCmd`.
use std::path::Path;
use anyhow::{Context as _, Result, bail};
use hive_host_sock::HostRequest;
use crate::cli::{AgentsCmd, QuotaCmd};
use crate::cli::{AgentCmd, AgentQuotaCmd};
use crate::dag_progress::wait_for_dags;
use crate::util::render;
@ -33,11 +36,11 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()>
}
}
/// `hivectl agents list` — fetch the per-agent status roster from the
/// `hivectl list-agents` — fetch the per-agent status roster from the
/// daemon (`HostRequest::AgentStatus`) and render it as a padded table,
/// or the raw JSON rows with `--json`. Reuses the dashboard's
/// `ContainerView` aggregation, so the CLI and the web UI never drift.
async fn agents_list(socket: &Path, json: bool) -> Result<()> {
pub(crate) async fn agents_list(socket: &Path, json: bool) -> Result<()> {
let resp = crate::client::request(socket, hive_host_sock::HostRequest::AgentStatus)
.await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
@ -112,28 +115,7 @@ async fn agents_list(socket: &Path, json: bool) -> Result<()> {
Ok(())
}
async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> {
let resp = crate::client::request(socket, hive_host_sock::HostRequest::RestartAll)
.await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
let agents = resp.agents.as_deref().unwrap_or(&[]);
if agents.is_empty() {
println!("restart-all: no managed containers found");
} else {
for a in agents {
println!("restart queued: {a}");
}
}
if !resp.ok {
bail!(
"restart-all: {}",
resp.error.as_deref().unwrap_or("unknown error")
);
}
wait_for_dags(socket, resp.queued_dags.unwrap_or_default(), no_wait).await
}
/// `hivectl agents pause|resume` — flip the agent's pause marker. Not a
/// `hivectl agent <name> pause|resume` — flip the agent's pause marker. Not a
/// DAG, so there's nothing to wait on: the daemon writes the marker and
/// the harness picks it up on its next poll.
async fn set_paused(socket: &Path, name: &str, paused: bool) -> Result<()> {
@ -159,41 +141,36 @@ async fn set_paused(socket: &Path, name: &str, paused: bool) -> Result<()> {
}
}
/// Dispatch `hivectl agents <verb>` — container lifecycle over the host
/// admin socket.
pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
/// Dispatch `hivectl agent <name> <verb>` — container lifecycle over the
/// host admin socket, all scoped to the single `name` hoisted from the
/// parent command.
pub(crate) async fn run_agent(socket: &Path, name: &str, cmd: AgentCmd) -> Result<()> {
match cmd {
AgentsCmd::List { json } => agents_list(socket, json).await,
AgentsCmd::Restart { name, no_wait } => agents_restart(socket, &name, no_wait).await,
AgentsCmd::RestartAll { no_wait } => agents_restart_all(socket, no_wait).await,
AgentsCmd::Pause { name } => set_paused(socket, &name, true).await,
AgentsCmd::Resume { name } => set_paused(socket, &name, false).await,
AgentsCmd::Spawn { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Restart { no_wait } => agents_restart(socket, name, no_wait).await,
AgentCmd::Pause => set_paused(socket, name, true).await,
AgentCmd::Resume => set_paused(socket, name, false).await,
AgentCmd::Spawn => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Spawn { name }).await?)
}
AgentsCmd::RequestSpawn { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::RequestSpawn => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?)
}
AgentsCmd::Kill { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Kill => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Kill { name }).await?)
}
AgentsCmd::Destroy { name, purge } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Destroy { purge } => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Destroy { name, purge }).await?)
}
AgentsCmd::Rebuild { name } => {
let name = crate::util::parse_ident(&name)?;
AgentCmd::Rebuild => {
let name = crate::util::parse_ident(name)?;
render(crate::client::request(socket, HostRequest::Rebuild { name }).await?)
}
AgentsCmd::SetParent {
child,
parent,
root,
} => {
let child = crate::util::parse_ident(&child)?;
AgentCmd::SetParent { parent, root } => {
let child = crate::util::parse_ident(name)?;
let new_parent = if root {
None
} else {
@ -204,16 +181,15 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
.await?,
)
}
AgentsCmd::SetLimits {
name,
AgentCmd::SetLimits {
cpu_quota,
memory_max,
reset,
} => {
let name = crate::util::parse_ident(&name)?;
let name = crate::util::parse_ident(name)?;
// `--reset` is the only way to reach an all-`None` request;
// clap rejects a bare `set-limits <name>` with neither flag,
// so a forgotten value can't silently clear the overrides.
// clap rejects a bare `set-limits` with neither flag, so a
// forgotten value can't silently clear the overrides.
let (cpu_quota, memory_max) = if reset {
(None, None)
} else {
@ -231,13 +207,16 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
.await?,
)
}
// `quota` and `subvol` keep their own modules — this arm is just
// the reparenting glue that moved them under `agents`.
AgentsCmd::Quota { cmd } => match cmd {
QuotaCmd::Enable => crate::quota::quota_enable(socket).await,
QuotaCmd::Show { name } => crate::quota::quota_show(socket, name.as_deref()).await,
QuotaCmd::Set { name, size } => crate::quota::quota_limit(socket, &name, &size).await,
AgentCmd::Choom { resume_session } => {
crate::choom::choom(socket, name, resume_session.as_deref()).await
}
// `quota` and `subvol` keep their own modules — these arms are
// just the reparenting glue that hoists `name` in from the
// parent `agent <name>` command.
AgentCmd::Quota { cmd } => match cmd {
AgentQuotaCmd::Show => crate::quota::quota_show(socket, name).await,
AgentQuotaCmd::Set { size } => crate::quota::quota_limit(socket, name, &size).await,
},
AgentsCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, cmd).await,
AgentCmd::Subvol { cmd } => crate::subvol::dispatch_subvol(socket, name, cmd).await,
}
}

View file

@ -1,4 +1,4 @@
//! `hivectl choom <agent>` — drop into an interactive Claude session inside
//! `hivectl agent <name> choom` — drop into an interactive Claude session inside
//! an agent container by exec-ing `machinectl shell` running claude as the
//! agent user, mirroring the harness's per-turn claude invocation.

View file

@ -17,8 +17,9 @@ recovery / debugging verbs.\
)]
pub struct Cli {
/// Path to the hive-c0re host admin socket, used by the daemon-assisted
/// verbs (`agents`, `stop`, `start`). Global: accepted before or after
/// the subcommand. Verbs that don't talk to the daemon ignore it.
/// verbs (`agent`, `list-agents`, `stop`, `start`). Global: accepted
/// before or after the subcommand. Verbs that don't talk to the daemon
/// ignore it.
#[arg(long, global = true, default_value = DEFAULT_HOST_SOCKET)]
pub(crate) socket: PathBuf,
#[command(subcommand)]
@ -61,14 +62,36 @@ pub enum Cmd {
#[command(subcommand)]
cmd: GatewayCmd,
},
/// Agent container management.
/// Lifecycle actions on ONE managed agent container. Needs the
/// hive-c0re daemon running.
///
/// Lifecycle actions on managed agent containers. Needs the hive-c0re
/// daemon running.
Agents {
/// Everything here targets a single named agent (`hivectl agent foo
/// restart`, `hivectl agent foo choom`, …) — anything that acts
/// hive-wide lives at the top level instead (`list-agents`,
/// `restart`/`stop`/`start` with a scope, `quota-enable`).
Agent {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
#[command(subcommand)]
cmd: AgentsCmd,
cmd: AgentCmd,
},
/// Show all managed agents with their status and technical state.
///
/// Global — not scoped to one agent, so it lives at the top level
/// rather than under `hivectl agent <name>`. Needs the hive-c0re
/// daemon running.
ListAgents {
/// Emit the raw JSON rows instead of the padded table (for
/// scripting). The table is the default human-readable shape.
#[arg(long)]
json: bool,
},
/// Enable btrfs qgroup accounting on the agent-state filesystem.
///
/// Global one-shot toggle (not per-agent), hence top-level rather than
/// under `hivectl agent <name>`. Idempotent — safe to re-run. Once
/// enabled, `hivectl agent <name> quota show`/`quota set` work.
QuotaEnable,
/// Operator approval queue: list, approve, or deny pending requests.
///
/// Needs the hive-c0re daemon running.
@ -98,22 +121,6 @@ pub enum Cmd {
#[arg(long)]
wg_endpoint: Option<String>,
},
/// Open an interactive Claude session inside an agent container.
///
/// A fresh session by default, or resume a prior one. Requires root
/// and a running container.
Choom {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Resume a prior claude session by its session id, passed
/// through as `claude --resume <value>` (claude's `--continue`
/// takes no value — it resumes the cwd's latest session, which
/// is the harness's, so choom never uses it; this flag matches
/// the claude flag it maps to). Omit for a fresh blank session.
/// A value is required when the flag is given.
#[arg(long = "resume", value_name = "SESSION")]
resume_session: Option<String>,
},
/// Stop containers hive-wide in one operator action.
///
/// Bare `hivectl stop` stops everything; scope flags narrow it to
@ -435,128 +442,68 @@ pub enum WgCmd {
Status,
}
#[derive(Subcommand)]
pub enum QuotaCmd {
/// Enable btrfs qgroup accounting on the agent-state filesystem.
///
/// Run once before `show` / `limit`. No-op on non-btrfs hosts.
Enable,
/// Report per-agent disk usage from btrfs qgroups (all agents, or one
/// by name).
Show {
/// Agent to show (omit for all agents with a state subvolume).
name: Option<String>,
},
/// Set or clear an agent's disk-usage quota.
///
/// Named `set` rather than `set-quota` because the enclosing `quota`
/// group already carries the noun — `agents quota set iris 5G`. The
/// `set-<noun>` spelling stays for the flat verbs (`set-parent`,
/// `set-limits`), which have no group to inherit it from.
Set {
/// Agent whose state subvolume to limit.
name: String,
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
size: String,
},
}
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
// default via `hive_host_sock::HOST_SOCKET` — the daemon binds there
// and `hivectl agents` connects to it.
// and `hivectl` connects to it.
pub(crate) use hive_host_sock::HOST_SOCKET as DEFAULT_HOST_SOCKET;
/// Verbs under `hivectl agent <name> <verb>` — every one of these targets
/// the single agent named on the parent command, so none of them carry
/// their own `name` field.
#[derive(Subcommand)]
pub enum AgentsCmd {
/// Show all managed agents with their status and technical state.
List {
/// Emit the raw JSON rows instead of the padded table (for
/// scripting). The table is the default human-readable shape.
#[arg(long)]
json: bool,
},
/// Stop and start a single agent container without rebuilding config.
pub enum AgentCmd {
/// Stop and start this agent container without rebuilding config.
Restart {
/// Agent name (e.g. `damocles`, `ruth`).
name: String,
/// Return immediately after the restart DAG is queued.
#[arg(long)]
no_wait: bool,
},
/// Restart all managed agent containers.
RestartAll {
/// Return immediately after the restart DAGs are queued.
#[arg(long)]
no_wait: bool,
},
/// Park an agent's turn loop, leaving the container running.
/// Park this agent's turn loop, leaving the container running.
///
/// The harness stops driving turns but keeps serving its web UI and
/// MCP daemons, so the container, its mounts and its warm caches stay
/// up while it burns no tokens. Inbox messages queue unacked and the
/// backlog drains on `resume`. Sticky: it survives a restart, and
/// pausing a stopped agent makes it come up paused.
Pause {
/// Agent name.
name: String,
},
/// Resume a paused agent — it drains whatever queued up while parked.
Resume {
/// Agent name.
name: String,
},
/// Spawn a new agent container directly, bypassing the approval queue.
Pause,
/// Resume this paused agent — it drains whatever queued up while parked.
Resume,
/// Spawn this agent container directly, bypassing the approval queue.
///
/// Operator-on-the-host only; use `request-spawn` for an approval-gated
/// spawn.
Spawn {
/// Agent name (e.g. `iris`).
name: String,
},
Spawn,
/// Queue a spawn request for operator approval.
RequestSpawn {
/// Agent name.
name: String,
},
/// Stop a managed container (graceful).
Kill {
/// Agent name.
name: String,
},
/// Tear down a sub-agent container, keeping its state by default. No
/// undo.
RequestSpawn,
/// Stop this managed container (graceful).
Kill,
/// Tear down this sub-agent container, keeping its state by default.
/// No undo.
Destroy {
/// Agent name.
name: String,
/// Also wipe the agent's state dirs (config + creds + notes).
#[arg(long)]
purge: bool,
},
/// Apply pending config to a managed container.
Rebuild {
/// Agent name.
name: String,
},
/// Move an agent in the topology tree — under a new parent, or to root.
/// Apply pending config to this managed container.
Rebuild,
/// Move this agent in the topology tree — under a new parent, or to
/// root.
SetParent {
/// Agent to move.
child: String,
/// New parent agent name. Mutually exclusive with `--root`.
#[arg(long, conflicts_with = "root", required_unless_present = "root")]
parent: Option<String>,
/// Promote `child` to root (no parent).
/// Promote this agent to root (no parent).
#[arg(long)]
root: bool,
},
/// Declare an agent's CPU/memory limits, overriding the hive-wide defaults.
/// Declare this agent's CPU/memory limits, overriding the hive-wide
/// defaults.
///
/// Replaces the agent's whole override entry rather than merging into
/// it: any limit you don't pass returns to the hive-wide default. To
/// change one and keep the other, pass both. Disk is a separate
/// resource with its own group — see `agents quota`.
/// resource with its own group — see `quota`.
SetLimits {
/// Agent name.
name: String,
/// systemd `CPUQuota=` value, e.g. `400%` (100% = one full core).
#[arg(long, conflicts_with = "reset")]
cpu_quota: Option<String>,
@ -573,15 +520,29 @@ pub enum AgentsCmd {
)]
reset: bool,
},
/// Per-agent disk accounting + optional quotas via btrfs qgroups.
/// Open an interactive Claude session inside this agent's container.
///
/// Opt-in: enable qgroup accounting, then report per-agent usage or
/// cap an agent. No-op on non-btrfs hosts.
/// A fresh session by default, or resume a prior one. Requires root
/// and a running container.
Choom {
/// Resume a prior claude session by its session id, passed
/// through as `claude --resume <value>` (claude's `--continue`
/// takes no value — it resumes the cwd's latest session, which
/// is the harness's, so choom never uses it; this flag matches
/// the claude flag it maps to). Omit for a fresh blank session.
/// A value is required when the flag is given.
#[arg(long = "resume", value_name = "SESSION")]
resume_session: Option<String>,
},
/// This agent's disk accounting + optional quota via btrfs qgroups.
///
/// Needs `hivectl quota-enable` run once hive-wide first. No-op on
/// non-btrfs hosts.
Quota {
#[command(subcommand)]
cmd: QuotaCmd,
cmd: AgentQuotaCmd,
},
/// btrfs subvolume management for agent state dirs.
/// btrfs subvolume management for this agent's state dir.
///
/// Upgrade an existing plain-dir agent's state into a btrfs subvolume
/// so it gains snapshots and per-subvol usage/quota.
@ -591,6 +552,17 @@ pub enum AgentsCmd {
},
}
#[derive(Subcommand)]
pub enum AgentQuotaCmd {
/// Report this agent's disk usage from btrfs qgroups.
Show,
/// Set or clear this agent's disk-usage quota.
Set {
/// Size cap (`5G`, `500M`, `1073741824`) or `none` to clear.
size: String,
},
}
/// Operator approval queue: list, approve, or deny pending requests.
#[derive(Subcommand)]
pub enum ApprovalsCmd {
@ -615,14 +587,12 @@ pub enum SubvolCmd {
///
/// Bounces the agent to migrate its state, so it requires `--yes`.
Upgrade {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Confirm: this stops the agent, migrates its state dir, and
/// restarts it. Required — the command refuses without it.
#[arg(long)]
yes: bool,
},
/// Read-only snapshots of an agent's state subvolume.
/// Read-only snapshots of this agent's state subvolume.
Snapshot {
#[command(subcommand)]
cmd: SnapshotCmd,
@ -633,8 +603,6 @@ pub enum SubvolCmd {
pub enum SnapshotCmd {
/// Create a read-only snapshot (agent must already be a subvolume).
Create {
/// Agent name (e.g. `damocles`, `iris`).
name: String,
/// Snapshot label. Mandatory, and must start with `hive-` — the
/// prefix doubles as an allow-list hive-priv checks so only
/// hivectl-issued snapshot names can reach the `btrfs subvolume
@ -644,8 +612,6 @@ pub enum SnapshotCmd {
},
/// Delete a snapshot created by `subvol snapshot create`.
Delete {
/// Agent name the snapshot belongs to.
name: String,
/// Snapshot label passed to `subvol snapshot create --label`.
label: String,
},
@ -655,8 +621,6 @@ pub enum SnapshotCmd {
/// point-in-time backup: a full send with no `--parent` produces a
/// self-contained archive of the snapshot.
Send {
/// Agent name the snapshot belongs to.
name: String,
/// Snapshot label passed to `subvol snapshot create --label`.
label: String,
/// Optional parent snapshot label for an incremental send

View file

@ -2,15 +2,16 @@
//!
//! A thin client for the `hive-c0re` daemon: it speaks the host admin
//! socket protocol (`hive-host-sock`) and does NOT link the daemon crate.
//! Container lifecycle + the approval queue (`agents <spawn|kill|rebuild|
//! restart|…>`, `approvals <pending|approve|deny>`, `stop` / `start`) and
//! provisioning (`forge` / `matrix` / `github` / `gateway`) all forward to
//! the daemon, which owns the broker, the credentials, and the provisioning
//! logic — a running daemon is required for those. A couple of verbs work
//! off local host state directly instead (`wg` / `peer-config` read the mesh
//! key + TLS CA), so they don't need the socket. `choom` execs into a
//! container rather than asking the daemon to do anything, but still uses the
//! socket for its "is this an agent?" pre-flight — that answer lives in a
//! Container lifecycle + the approval queue (`agent <name> <spawn|kill|
//! rebuild|restart|choom|…>`, `list-agents`, `approvals <pending|approve|
//! deny>`, `stop` / `start`) and provisioning (`forge` / `matrix` /
//! `github` / `gateway`) all forward to the daemon, which owns the broker,
//! the credentials, and the provisioning logic — a running daemon is
//! required for those. A couple of verbs work off local host state
//! directly instead (`wg` / `peer-config` read the mesh key + TLS CA), so
//! they don't need the socket. `agent <name> choom` execs into a container
//! rather than asking the daemon to do anything, but still uses the socket
//! for its "is this an agent?" pre-flight — that answer lives in a
//! directory only the daemon's user can read.
//!
//! One module per subcommand family (see the `mod` list below); `main` is
@ -40,13 +41,12 @@ use open::open_url;
mod wg;
use wg::{peer_config, require_hive_domain, wg_init, wg_peer, wg_status};
mod choom;
use choom::choom;
mod github;
use github::github_set_token;
mod forge;
use forge::{forge_create_user, forge_reconcile_config};
mod agents;
use agents::run_agents;
use agents::{agents_list, run_agent};
mod power;
use power::{restart, start, stop};
mod approvals;
@ -93,7 +93,9 @@ async fn main() -> Result<()> {
GatewayCmd::DeleteUser { username } => gateway_delete_user(&socket, &username).await,
GatewayCmd::ListUsers => gateway_list_users(&socket).await,
},
Cmd::Agents { cmd } => run_agents(&socket, cmd).await,
Cmd::Agent { name, cmd } => run_agent(&socket, &name, cmd).await,
Cmd::ListAgents { json } => agents_list(&socket, json).await,
Cmd::QuotaEnable => quota::quota_enable(&socket).await,
Cmd::Approvals { cmd } => run_approvals(&socket, cmd).await,
Cmd::Wg { cmd } => match cmd {
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
@ -123,10 +125,6 @@ async fn main() -> Result<()> {
} => stop(&socket, scope.to_scope(), graceful, no_wait).await,
Cmd::Start { scope, no_wait } => start(&socket, scope.to_scope(), no_wait).await,
Cmd::Restart { scope, graceful } => restart(&socket, scope.to_scope(), graceful).await,
Cmd::Choom {
name,
resume_session,
} => choom(&socket, &name, resume_session.as_deref()).await,
Cmd::MarkdownDocs => {
print!("{}", clap_markdown::help_markdown::<Cli>());
Ok(())

View file

@ -1,6 +1,7 @@
//! `hivectl agents quota` — per-agent disk accounting + optional quotas via
//! btrfs qgroups. The daemon holds the privileged helper that reads /
//! sets qgroups; hivectl relays the request and formats the reply.
//! `hivectl quota-enable` + `hivectl agent <name> quota` — per-agent disk
//! accounting + optional quotas via btrfs qgroups. The daemon holds the
//! privileged helper that reads / sets qgroups; hivectl relays the
//! request and formats the reply.
use std::path::Path;
@ -13,20 +14,20 @@ pub(crate) async fn quota_enable(socket: &Path) -> Result<()> {
daemon_request(socket, hive_host_sock::HostRequest::QuotaEnable, "quota").await
}
/// `quota show [name]` — report per-agent disk usage from btrfs qgroups.
/// The daemon resolves the agent set + reads each subvolume's usage (it
/// holds the privileged helper); the client just formats the returned rows.
pub(crate) async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> {
/// `agent <name> quota show` — report one agent's disk usage from btrfs
/// qgroups. The daemon reads that subvolume's usage (it holds the
/// privileged helper); the client just formats the returned row(s).
pub(crate) async fn quota_show(socket: &Path, name: &str) -> Result<()> {
let resp = crate::client::request(
socket,
hive_host_sock::HostRequest::QuotaShow {
name: name.map(crate::util::parse_ident).transpose()?,
name: Some(crate::util::parse_ident(name)?),
},
)
.await
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
if !resp.ok {
// Carries the "btrfs quota not enabled — run `hivectl agents quota enable`
// Carries the "btrfs quota not enabled — run `hivectl quota-enable`
// first" hint when qgroups are off.
bail!("{}", resp.error.as_deref().unwrap_or("quota show failed"));
}

View file

@ -1,5 +1,6 @@
//! `hivectl agents subvol` — btrfs state-subvolume ops: migrate a plain-dir agent
//! state root to a subvolume (`upgrade`), and snapshot create/delete/send.
//! `hivectl agent <name> subvol` — btrfs state-subvolume ops: migrate a
//! plain-dir agent state root to a subvolume (`upgrade`), and snapshot
//! create/delete/send.
use std::path::Path;
@ -28,25 +29,21 @@ fn single_agent_scope(name: &str) -> hive_host_sock::LifecycleScope {
/// migration via hive-priv, then restart it. The restart is attempted
/// regardless of the migration outcome so a failed migration never leaves
/// the agent down; the migration error (if any) is surfaced afterwards.
/// Route a `hivectl agents subvol …` subcommand. Split out of `main`'s top-level
/// match so the CLI router stays within the clippy line budget and the
/// subvolume-op subcommands are dispatched in one place.
pub(crate) async fn dispatch_subvol(socket: &Path, cmd: SubvolCmd) -> Result<()> {
/// Route a `hivectl agent <name> subvol …` subcommand. Split out of
/// `main`'s top-level match so the CLI router stays within the clippy
/// line budget and the subvolume-op subcommands are dispatched in one
/// place. `name` is hoisted in from the parent `agent <name>` command.
pub(crate) async fn dispatch_subvol(socket: &Path, name: &str, cmd: SubvolCmd) -> Result<()> {
match cmd {
SubvolCmd::Upgrade { name, yes } => subvol_upgrade(socket, &name, yes).await,
SubvolCmd::Upgrade { yes } => subvol_upgrade(socket, name, yes).await,
SubvolCmd::Snapshot { cmd } => match cmd {
SnapshotCmd::Create { name, label } => {
subvol_snapshot_create(socket, &name, label).await
}
SnapshotCmd::Delete { name, label } => {
subvol_snapshot_delete(socket, &name, &label).await
}
SnapshotCmd::Create { label } => subvol_snapshot_create(socket, name, label).await,
SnapshotCmd::Delete { label } => subvol_snapshot_delete(socket, name, &label).await,
SnapshotCmd::Send {
name,
label,
parent,
dest,
} => subvol_snapshot_send(socket, &name, &label, parent.as_deref(), &dest).await,
} => subvol_snapshot_send(socket, name, &label, parent.as_deref(), &dest).await,
},
}
}

View file

@ -210,9 +210,9 @@ in
# hive-wide `services.hyperhive.agentMemoryMax`) — see
# `resource_limits::effective_memory_bytes_from`. Not meant to be set
# directly in an agent.nix, same convention as `hyperhive.otel.*`
# above; the host option (or `hivectl agents set-resource-limits`) is
# above; the host option (or `hivectl agent <name> set-limits`) is
# the real operator knob, and this only reflects the value baked in at
# the agent's *last rebuild* — `set-resource-limits` still applies the
# the agent's *last rebuild* — `set-limits` still applies the
# cgroup cap live via a drop-in reload, but this derived heap ceiling
# needs a rebuild to pick up a new value.
options.hyperhive.claudeMemoryMaxBytes = lib.mkOption {