hyperhive/docs/tools/hivectl.md
atlas f778122f5a matrix: mint the appservice sender token in the matrix container
A swarm runs one homeserver and a homeserver has one appservice sender
account, so "mint it once" is a property of the thing being minted
rather than something a lock has to enforce. That is what makes this
account the one to move first: no trigger route, no controller change
and no agent list — a boot-time oneshot beside tuwunel is the whole
mechanism.

`swarm-matrix-minter` runs inside `containers.hive-matrix`, which
already holds the appservice token: the rendered registration is bound
in read-only because that is how tuwunel is handed it. What the
container lacked was an identity of its own, so this adds one — a leaf
from the store's CA with a grant of exactly one path, not the hive's
leaf, which reads every secret in the store.

Both ends of the credential ship here. The minter reads the path it
publishes to before it touches the homeserver, and returning on a
non-empty read IS the "only once"; `hive-c0re`'s `ensure_hive_user`
reads the same path, authenticating with the hive name already in
`HYPERHIVE_HIVE_NAME`. The existing mint-then-`M_USER_IN_USE`-login
ladder stays as the fallback for a store that is empty, unconfigured or
unreachable, which is every swarm deployed before this — so nothing
needs backfilling and nothing breaks if the rest of the sequence never
lands.

The credential is not an admin credential, and is not named like one.
It is the access token of the appservice registration's own
`sender_localpart` — `@hive:<server_name>`, an account the homeserver
creates for itself when it loads the registration. The store path is
`swarm/services/matrix/sender-token`, the host path is
`matrix/access-token`, and the homeserver no longer runs an
`admin_execute` promotion for that account at boot. Everything the hive
provisions with it — the Space, the chat room, their hierarchy and join
rules, the invites — rides on being the creator of those rooms at power
level 100, not on homeserver admin; there is no Synapse admin API here
to need, tuwunel has none.

Two operations do need an admin *sender* and therefore stop working:
`hivectl matrix promote-user` and `hivectl matrix reset-password`, both
`!admin …` messages into `#admins:<server>`, plus the password-reset
recovery path that an agent with a lost password file falls back to.
They are swarm-level operations and are left failing loudly rather than
served by an over-privileged token every other call site would also
carry. The sweep's own admin-rights check and self-repair go with them:
an account that is deliberately not an admin has nothing to check.

`ephemeral = false` stays, and hive root can still read the container's
filesystem. Accepted: what this buys is identity separation — no hive
*process* holds or reads the appservice token — not physical isolation.

Refs #4345
2026-09-20 22:07:16 +02:00

329 lines
17 KiB
Markdown

# hivectl
`hivectl` is the operator-facing host CLI for hyperhive. It lives on the
host (not inside any container). It talks to `hive-c0re` over the host
admin socket `/run/hyperhive/host.sock`, which is root-only by default —
so it needs `sudo` unless you grant sudoless access by listing your
login user in `services.hyperhive.c0re.adminUsers` (adds you to the
`hive-admin` group that owns the socket; see
[`docs/trust-boundary/boundary.md`](../trust-boundary/boundary.md#host-admin-socket-access-hivectl)).
Available via the `hive-c0re` package in the host NixOS config.
Unlike the `hive-c0re` daemon subcommands (which go through the broker),
`hivectl` covers direct host-side administration: manual provisioning of
forge + matrix accounts, gateway htpasswd management, container
lifecycle shortcuts, and interactive agent shell access.
This page is the curated guide. For the exhaustive flag-by-flag
reference autogenerated from the binary's own command tree, see
[`hivectl-cli.md`](hivectl-cli.md) — emitted by the hidden `hivectl
markdown-docs` subcommand and kept in lockstep with the code by the
`hivectl-docs` flake check (CI fails if the committed copy drifts).
Regenerate with `nix build .#default && ./result/bin/hivectl
markdown-docs > docs/tools/hivectl-cli.md`.
## Forge
Manual entry to the same idempotent provisioning flow `hive-c0re` runs
at boot. Useful for recovery, ad-hoc re-provisioning, or fixing a single
agent without bouncing the daemon.
```bash
hivectl forge create-user iris # provision (or refresh) forge account for agent `iris`
hivectl forge create-user mara # create forge account for a human user; prints token to stdout
hivectl forge create-user mara --password hunter2 # set a web-login password
hivectl forge create-user mara --password-stdin # read password from stdin (safer for scripting)
hivectl forge reconcile-config iris # show local-applied <-> forge config divergence, then prompt
hivectl forge reconcile-config iris --from forge # reset local applied checkout to forge main (effective next deploy)
hivectl forge reconcile-config iris --verbose # include the full diff, not just --stat
```
- For **agents** (name has a state dir under `/var/lib/hyperhive/agents/`):
`create-user` persists the token to `<state>/forge-token`. Re-running refreshes the
token (idempotent — scope always matches current `TOKEN_SCOPES`).
- For **non-agents** (humans): creates the account and prints the token to
stdout, creating no state dir. Re-running after account already exists
re-mints the token and prints it again — safe for password resets.
- Without `--password` / `--password-stdin` `create-user` uses a random
throwaway password (fine for agents — they auth by token).
- `reconcile-config <agent>` shows the divergence between the agent's local
applied config checkout and its forge `agent-configs/<agent>` `main`, then
reconciles. `--from forge` resets the local checkout to forge `main` (takes
effect on the next deploy — it doesn't autorebuild). `--from local` isn't
supported yet (forge `main` is core-only branch-protected; resolve via a
config PR). With no `--from` it prompts for the direction after the diff.
## Matrix
Manual entry to the same idempotent matrix provisioning flow
`hive-c0re` runs at boot. Requires the `hive-matrix` container to be
running (`services.hyperhive.deploy.matrix.enable = true`).
```bash
hivectl matrix create-user iris # provision (or re-provision) matrix account for agent `iris`
hivectl matrix create-user mara # create matrix account for a human; prints access_token to stdout
hivectl matrix create-user mara --password hunter2 # set a client-login password
hivectl matrix sync-admin # provision / refresh the hive's own `@hive:` account
hivectl matrix promote-user mara # promote an existing matrix user to homeserver admin
hivectl matrix reset-password iris # generate and set a new random password for `iris`; prints it
hivectl matrix invite mara # invite a user to the hive Space
hivectl matrix invite @mara:server --room '#hive-chat:server' # ...or to a specific room/alias
```
- `create-user`: for agents, persists the `access_token` to
`<state>/matrix-token`. Skips registration when the file already
exists — delete it first to force re-registration.
- `sync-admin`: ensures the hive's own `@hive:` matrix user exists
(the account `hive-c0re` provisions rooms with). Token persisted to the
access token path. Safe to run again — idempotent.
- `promote-user`: promotes an already-registered user to homeserver
admin by an `!admin` command in `#admins`. ⚠️ Needs an admin **sender**,
which `@hive:` is not — this operation is being rehomed at swarm level
and does not work from the hive today.
- `reset-password`: asks the admin room to set a new random
password and prints it to stdout. ⚠️ Needs an admin **sender** too, so
it does not work from the hive today either. Useful if an agent or human lost
credentials.
- `invite`: invites a matrix user (full `@user:server` or a bare
localpart, qualified with the homeserver's `server_name`) to the hive
Space by default, or to a `--room` id / `#alias`. Uses the `@hive:`
token; the account must be a member of the target room with
invite power (it owns the hive Space, so that case always works).
Idempotent — already-member / already-invited is a no-op.
## GitHub
<!-- vale write-good.Passive = NO -->
Write an operator-supplied GitHub personal access token (PAT) into an
agent's token file so its `gh` wrapper + git credential helper can act as
the bot account. Unlike forge/matrix there is no account creation — the PAT
is for an existing GitHub account. A CLI alternative to the dashboard
credentials tab; the [GitHub integration](../integrations/github.md) is on by default
(`services.hyperhive.agent.github.enable`), so no per-agent config is needed.
<!-- vale write-good.Passive = YES -->
```bash
hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred)
hivectl github set-token damocles --token <pat> # inline (visible in shell history)
```
- `set-token`: writes `<state>/github-token` (`0600`, agent-owned) via
hive-priv — the same credential-injection path as forge/matrix tokens.
The `gh` wrapper / git credential helper read it live, so a freshly set
or rotated PAT takes effect with no rebuild or restart. Refuses an empty
token. See [github.md](../integrations/github.md) for the full flow + security notes.
## Gateway
Manage users in the gateway's HTTP Basic auth htpasswd file
(`services.hyperhive.gateway.auth`). `hivectl` sends the request over the
host admin socket; the `hive-c0re` daemon owns the htpasswd file at its
canonical path (`/var/lib/hive-gateway/conf/gateway.htpasswd`) and
performs the write.
```bash
hivectl gateway create-user alice --password-stdin # add (or update) user; read password from stdin
hivectl gateway create-user bob --password hunter2 # add user inline (visible in shell history)
hivectl gateway delete-user bob # remove user
hivectl gateway list-users # list all usernames, one per line
```
The daemon hashes passwords with BCrypt (cost 12). It creates the file
if it doesn't exist. Re-running `create-user` with the same
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. Everything scoped to a
single agent lives under `hivectl agent <name> <verb>``hivectl` hoists
the name onto the parent command, so none of the verbs below repeat it.
```bash
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-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);
REV is the first 12 chars of the agent's locked config sha; PARENT is its
place in the topology tree (`-` for a root agent); REMIND is the count of
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.
`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
dir (`<state>/<name>/harness/paused`) which the harness re-stats every
5 s at the top of its serve loop; while it's there the agent drives no
turns, but the container, its mounts, its warm caches, its web UI and
its MCP daemons all stay up. Inbox messages queue **unacked**, so a
resume drains the backlog rather than dropping it. Points worth knowing:
- **Sticky.** The marker lives on the persistent harness mount, so a
paused agent stays paused across a container restart — and pausing a
_stopped_ agent makes it come up parked.
- **Not a DAG.** Unlike `restart`/`stop`, there's no container operation
to sequence, so it applies immediately with nothing to wait on.
- **Stopping a paused agent is still fast.** hivectl skips the graceful-stop
handshake 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 `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 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.c0re.agentCpuQuota` /
`agentMemoryMax` for one agent, persisted to
`meta/resource-limits.json` (see
[`persistence.md`](../agent-lifecycle/persistence.md)). Values are systemd's
`CPUQuota=` / `MemoryMax=` syntax: a percentage (`400%` = four full
cores) for CPU; a size (`8G`), a percentage of physical RAM, or
`infinity` for memory. `set-limits` validates both before persisting them —
they go into a systemd drop-in verbatim, and a typo there makes the
unit fail to start.
**Declarative, not incremental**: each invocation replaces the agent's
whole entry. `hivectl agent sock set-limits --memory-max 8G` leaves `sock`
with _only_ a memory override, reverting any previously set CPU quota to
the hive default. To avoid a forgotten flag silently wiping an override,
hivectl rejects `set-limits` with no flags at all — clearing requires the
explicit `--reset`.
The command rewrites the container's drop-in and reloads systemd, so
new containers and restarts pick the values up immediately.
## Choom
Drop into an interactive Claude session inside an agent container.
Replaces the current process with `machinectl shell <name>@h-<name>`
running claude from the agent's state dir. Requires root, or membership in
`hive-admin` (the same sudoless-`hivectl` group as the [host admin
socket](../trust-boundary/boundary.md) — hive-c0re ships a polkit rule
granting that group the one action `machinectl shell` needs); `choom`
checks this itself and refuses up front with a message naming the
requirement rather than letting systemd reject the exec later with an
opaque polkit prompt. Same caveat as the socket grant: a shell opened
before the operator added you to `hive-admin` won't see it until you log
back in — secondary group membership applies at login.
It also needs the daemon socket, unlike the other exec-into-a-container
paths: the "is this actually an agent?" pre-flight reads the agents root,
which the daemon's user owns and which isn't group-readable, so the check
is a `HostRequest` rather than a local `stat`. An unprivileged `choom`
therefore names the grant it wants, instead of reporting a permission
problem with the state dir.
```bash
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
through as `claude --resume <value>` to rejoin a prior session by its
session id — the flag name deliberately matches the claude flag it maps
to. (choom never uses claude's `--continue`: that's a bare flag that
takes no argument and resumes the cwd's _latest_ session, that is, the
harness's; claude would consume a value after it as the first prompt,
silently poking the live harness session.) Passing the flag requires a
value. Either way choom never collides with the harness's live
session in the same project dir: the harness pins its own id via
`--resume`, so a blank choom session is invisible to it. The container
must be running.
choom reproduces the harness's own claude invocation so the operator
lands in a faithful copy of the agent's environment:
- **as the agent user**, not root: `machinectl shell` defaults to root,
which would make claude read `/root/.claude` (empty) instead of the
agent's `/home/<name>/.claude` where its OAuth credentials live. choom
prefixes the machine with `<name>@` (the meta-flake sets the agent's
unix user name to its label).
- **from `/agents/<name>/state`**: the session and `CLAUDE.md` (the
persona) resolve against the right project dir.
- **with the harness flags**: `--settings`, `--mcp-config`, and
`--system-prompt-file` from `/run/hive-config/` — the same files the
harness writes each turn — so the operator gets the agent's settings,
the hyperhive/matrix MCP tools, and the role prompt. Each flag is
included only when its file exists.
- **without the onboarding walkthrough**: a boot-time oneshot
(`hive-claude-onboarding`) seeds `hasCompletedOnboarding` and the
project trust flags into `~/.claude.json` once before the harness
starts, so the first interactive choom lands straight in a session
instead of the onboarding/trust dialog the headless harness never
completes. It's the single place hyperhive touches that file.
## Watch
Follow an agent's live turn/tool-call event stream from the CLI:
```bash
hivectl agent iris watch # tail iris's live events; Ctrl-C to stop
```
Dials the same unix socket the gateway's nginx `proxy_pass`es through
(`/run/hive-agent/<name>/web.sock` — see [Per-agent unix-socket
upstream](../networking/gateway.md#per-agent-unix-socket-upstream)) directly and
speaks a bare HTTP/1.1 request for the agent's existing `/events/stream`
SSE endpoint over it. No gateway hop, no daemon round-trip for the
stream itself — the daemon socket is only used for the "does this agent
exist" pre-flight, same reasoning as `choom` above. Requires the agent
to be running with its web UI socket bound; a fresh spawn/rebuild that
hasn't come up yet gets a clear connection-refused hint rather than a
raw OS error.
Prints one compact line per event — reuses the `_icon`/`_summary` fields
the harness already stamps onto stream-json events for the web UI
(`stream_enrich.rs`), so tool calls and turn markers read as short
glyph-prefixed lines instead of raw JSON. Not an attempt at the web
UI's full collapsible-details rendering (`docs/web-ui/terminal-rendering.md`)
— that's presentation for a browser, this is a `tail -f`.
## Open
Print (and best-effort open in a browser) one of the hive's web surfaces.
Requires the `hive-c0re` daemon to be running.
```bash
hivectl open # operator dashboard (same as `open home`)
hivectl open home # operator dashboard (https://<domain>/)
hivectl open forge # the forge (Forgejo) web UI
hivectl open matrix # the matrix GUI (fluffychat)
```
hivectl resolves the URL from the running daemon (`HostRequest::Urls`), which
reads the per-surface public URLs from c0re's service env — so custom
forge / matrix domains resolve correctly instead of assuming
`forge.<domain>`. The URL is **always printed** (the reliable core, since
the host is typically headless / driven over SSH), then hivectl tries
`xdg-open` as a convenience, reporting a missing or failing opener as a
note, not an error.
A surface has no URL when it isn't browser-reachable: `home` needs
`services.hyperhive.domain`; `forge` needs
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command
exits with a hint naming the option to set.