Commit graph hyperhive/docs/tools
Author SHA1 Message Date
atlas
11097ed336 remove the get_host_journal MCP tool and its capability
swarm-logs covers every host-tier unit this tool could reach, so the
second, capability-gated path into host journald earns nothing and is
removed outright rather than disabled behind a flag.

Removed end to end: the MCP tool definition + handler, the
GetHostJournal/HostJournal wire variants, hive-c0re's
dispatch_host_journal handler, the ReadHostJournal capability, and the
harness-side capability->--allowedTools gate. get_host_journal was the
only capability that mapped to an MCP tool, so allowed_capability_tools
could only ever return an empty vec; it goes too rather than linger as a
function that provably does nothing.

capabilities::has_cap/caps_for stay: #4624 gave ManageRootAgent's
bind-mount enforcement (hive-c0re/src/lifecycle/host_config.rs) a
second caller of has_cap, so they're no longer callerless once this
lands on top of it.

hive-sh4re's journal module (JournalPriority) had no consumer outside
this tool and is deleted.

An existing capabilities.json still naming read_host_journal does not
error: capabilities::prune_unknown drops unrecognised names with a
warn!, and an agent left with no capabilities has its entry removed. No
migration step is needed.

Untouched: hive-c0re/src/dashboard/journal.rs's
read_host_journal_response, which matches the name but is the private
helper behind the operator-only GET /api/journal-host dashboard route
and carries no capability check.
2026-09-21 19:31:45 +02:00
atlas
f9deb34081 docs/tools/subagent.md: clear the two vale error hits from PR #4603 2026-09-21 17:21:40 +02:00
atlas
85ac62968e hive-subagent-mcp: fold the task out of the system prompt on the no-role path too
mara's ruling on this PR: the principle that landed for the role path is
general, so the no-role path moves the task out of the system prompt as
well. `system_prompt_and_trigger`'s no-role branch still passed
`prompt_file` straight through as `--append-system-prompt-file` —
byte-identical to main's pre-PR behaviour, and the exact bug this PR
exists to fix. Since no agent ships roles yet, every real dispatch takes
this path, so the standing-system-prompt bug (a task re-asserting itself
as an instruction on turn 2/3/N under continue/goal) was still live for
100% of usage; only the unused role path had actually been fixed.

`compose_prompt` now always folds the task into `trigger`, role or not.
Without a role there is no role text to hold a system-prompt file open
for, so none exists at all — `--append-system-prompt-file` is omitted
from the spawn entirely, not pointed at anything task-shaped.

Replaces `without_a_role_the_task_file_is_what_claude_is_pointed_at`
(which pinned the bug as "unchanged") with a test asserting the task is
absent from what reaches `--append-system-prompt-file` and present in
the trigger. Role-path tests are untouched in behavior; only mechanical
fallout from `compose_prompt`'s `Option<PathBuf>` return and `&str`
trigger params (clippy needless_pass_by_value once both branches only
borrowed it).

Blast radius: this changes behaviour for every existing dispatch, since
prompt_file's content has always gone into the system-prompt file before
this fix.
2026-09-21 17:21:40 +02:00
atlas
657875b2fa hive-subagent-mcp: name a role at dispatch, load it as the system prompt
A `start` may now name a role: `role: "reviewer"` loads the spawning
agent's own `subagent_roles/reviewer.md` and renders it, alone, into one
per-session file that `--append-system-prompt-file` points at. The role is
the system prompt; the task is the turn, never the other way round — a
task baked into the system prompt would re-assert itself as an
instruction on every later turn of a continued session, not just the one
it was written for. The task instructions (`prompt_file`) are read and
folded ahead of the turn's own prompt instead, the same channel that
carries them to the subagent without a role.

The argument is optional, so every existing call is unchanged — pinned by
a test that a pre-role payload still deserializes with `role` absent from
the schema's required set, and another that the no-role path reaches
claude with the caller's own file, unrendered, and the trigger untouched.
With a role, one test pins the system-prompt file to the role's text and
nothing of the task, and another pins the task still reaching the
subagent as the turn's prompt.

A role name with no file fails the call, before the session name is even
reserved, and the error lists the roles the directory does hold. No agent
ships roles yet, so named-but-missing is the ordinary first-run state; a
fallback there would spawn a subagent under a prompt missing every clause
the role existed to carry. An empty file and a name that is not a plain
identifier refuse the same way.
2026-09-21 17:21:40 +02:00
atlas
4121e11d87 hive-c0re: drop the subtree check from the scheduling verbs
The topology predicate `is_descendant_of` gated the four schedule-
managing verbs: a caller could only name a schedule owned by an agent at
or below itself in `topology.json`. Those gates now permit any requester,
so the predicate, its pure `_in` form and the `schedule_authorized`
wrapper built on it are gone rather than left returning a constant. The
other two wrappers went earlier with the verbs they served —
`require_descendant` with the lifecycle MCP verbs in 87970a8c, and
`resolve_agent_state_target` with `get_loose_ends`'s agent parameter.

`require_group(agent, "scheduling", ...)` is untouched and still fires at
dispatch for every one of the five scheduling verbs, so holding the tool
group remains the gate; what goes is the ownership restriction layered on
top of it.

The three schedule-mutating verbs keep their row lookup as a plain
existence check, so a caller naming a schedule that does not exist still
gets `not found` rather than a message from deeper in the cancel path.
`list_schedules` stops filtering per row: it would only have hidden rows
the requester may act on anyway.

Error messages, tool descriptions and docs that described the subtree
relation are reworded — a refusal message naming a topology that no
longer decides anything is worse than none.

The six `is_descendant_of_in` unit tests go with the function they test;
the permit behaviour they leave unasserted is picked up by the next
commit.

Refs #4472
2026-09-21 17:20:44 +02:00
atlas
b88a5b2430 remove the list_containers and request_update_meta_inputs MCP tools
Both agent-facing tools go away end to end, with no replacement. This is
an intentional capability removal: agents can no longer enumerate their
own subtree, and can no longer queue a meta-flake input bump.

The system prompt and docs/tools/lifecycle.md land in this same commit
on purpose. A tool named in the prompt but absent from the server makes
agents confidently call something that doesn't exist, and the failure
then surfaces far from its cause.

Removed:

- MCP registrations and bodies (hive-agent-mcp), plus the now-unused
  UpdateMetaInputsArgs.
- Wire variants Request::ListDescendants,
  Request::RequestUpdateMetaInputs and Response::Containers, plus
  ContainerInfo, whose only consumer was that response.
- hive-c0re's handle_list_descendants (its whole module) and
  handle_request_update_meta_inputs, the two dispatch arms, and the
  require_group(agent, "approvals", ...) gate on the meta-inputs verb.
- The stream_enrich emoji entry and argument formatter.
- docs/tools/lifecycle.md (both tools it documented are gone), its two
  referrers, the tool-group tables and the agent-hierarchy prose.

Tool groups are kept, deliberately. ToolGroup::Lifecycle listed exactly
one tool and now lists none — it is vestigial, but the variant stays so
existing meta/capabilities.json grants still parse; retiring it is a
separate decision. ToolGroup::Approvals also listed exactly one tool,
but the group is NOT dead: check_can_cancel_approval still gates
cancel_loose_end's approval-cancel arm on it server-side.

ApprovalKind::UpdateMetaInputs stays too. Nothing in production code
produces it any more, but pre-existing approval rows may still carry it,
and the operator's own path to a meta update is unaffected — the
dashboard's POST /api/meta-update inserts the meta_update job directly,
bypassing approvals entirely.

The two format_ack tests in hive-agent-mcp that named
request_update_meta_inputs were only using it as a label string while
exercising the generic OkWarn/Ok renderer, so they are retargeted to a
surviving tool rather than deleted.

Note hive-c0re's priv_client::list_containers is a different thing (the
host-side privileged container listing behind hive-priv) and is
untouched.

Closes #4591
2026-09-20 22:47:46 +02:00
atlas
1261b525d6 matrix: one sender account and one sender token per hive
A swarm runs one homeserver and every hive on it logged in as the same
`@hive:` localpart, holding the same access token out of one swarm-wide
store path. That is one matrix identity for N hives: the homeserver
cannot attribute an action to the hive that took it, and revoking one
hive's standing revokes every hive's.

Three changes, and the third is the one that makes the other two real:

- **The localpart carries the hive's name** (`hive-<hive>`), derived in
  one place, `swarm_secret_client::matrix::hive_localpart`.
  `hive-matrix.nix` renders the same string as the appservice
  registration's `sender_localpart`, so the shared account stops being
  created rather than merely stops being used.
- **The store path is templated by hive**, not a constant. The
  "a swarm runs one homeserver, so this is a constant rather than a
  parameter" rationale went with it; it stopped holding the moment two
  hives shared the homeserver it describes.
- **The path moved out from under the grant every hive has.** It sat at
  `swarm/services/matrix/sender-token`, inside the
  `secret/data/swarm/services/*` read stanza `policy::render` gives every
  hive. It now sits under that hive's own stanza,
  `secret/data/swarm/hives/<hive>/*`, which interpolates the reader's
  name — so a hive reads its own token and is refused another's. The
  policy renderer itself is unchanged: narrowing the `services/*` grant
  would break the OIDC-secret read it exists for, and moving the
  credential is what this needed instead. A policy test walks the
  rendered stanzas and asserts none of hive alpha's covers hive beta's
  sender token, so a later stanza that widened it fails here.

`swarm-matrix-ctl` takes a new required `MATRIX_MINT_HIVE` and writes
that hive's path; its store grant in `swarm-bao.nix` follows, scoped to
one hive's leaf via the new `deploy.bao.matrixCtlHiveName` (defaulting to
this host's `hiveName`) rather than a `hives/*` wildcard, which would
hand the matrix container every hive's token back.

Migration: no outage at deploy. `ensure_hive_user` short-circuits on the
local token file, so a hive keeps running on what it has; with no such
file it reads the new per-hive path, finds nothing, and falls through to
the existing register-or-appservice-login ladder against its own
localpart — which needs only the per-hive `as_token` on local disk. The
old shared object is read by nothing afterwards. Rooms do not follow the
identity, and that is the one operator step; both ways out are written
into `docs/integrations/matrix.md`.

No admin standing is granted to the per-hive accounts: `admin_execute`
stays empty and the assertion pinning it is untouched.
2026-09-20 22:07:16 +02:00
atlas
19cae964c8 matrix, docs: describe promotion/reset as rehomed, not as a pending fix
Both spots hedged the swarm-level framing with a temporal qualifier
("until then", "today") that read as future work awaiting a fix, on
top of the framing itself. Drops the qualifiers; the operations are
swarm-level and rehomed there now, not pending anything from the hive.
2026-09-20 22:07:16 +02:00
atlas
fb9c6122df matrix: name the credential after the account it authenticates as
The store path and every identifier around it called this an admin
token. It is not one: of ~15 hive-c0re call sites only two need
homeserver admin, and the homeserver no longer promotes the account at
boot, so the name overstated both what the credential is and what it may
do.

Renaming it to the account was not enough either. "The `@hive:` token"
reads as the token of a hive user, and no such user is provisioned —
`@hive:<server_name>` is the appservice registration's own
`sender_localpart`, an account the homeserver creates for itself when it
loads the registration.

So it is the **sender token**: the matrix appservice sender account's
access token, at `swarm/services/matrix/sender-token`. The name says
what it authenticates as rather than what it may do, which is the part
that was wrong.

The path has one constructor, and the bao grant, the grant assertion and
three unit tests pin its literal independently — so a half-finished
rename fails a check rather than leaving the minter and its readers
disagreeing at runtime. `tracing` messages are renamed with the code, so
the journal reads the way the source does.

The host-side file keeps its name (`matrix/access-token`): it carried no
admin framing, and renaming it would orphan the file on every deployed
hive for nothing.

`docs/tools/hivectl-cli.md` is regenerated from the clap tree.
2026-09-20 22:07:16 +02:00
atlas
bbb4e471ea matrix, docs: drop the last admin framing, fix tense and prose lint
Three leftovers from the rename, plus the gating prose job.

`nix/packages/default.nix` still described the minter as the "matrix
admin credential's minter", and shipped that claim in the package's
`meta.description` — a PR-visible string.

`promote_user_to_admin`'s doc comment pointed at the `admin_execute`
entry in `hive-matrix.nix` as the thing that bootstraps the first
admin. That entry is gone, so the comment referenced nothing. It now
records that the account is ordinary, that the call therefore has no
working sender, and that rehoming at swarm level is the fix rather than
re-granting. `reset_user_password` gained the matching warning; it had
none.

The prose fixes clear all 8 `CI / prose lint (vale, errors)` failures,
all of which were in docs this branch touches. No vale config change,
no exception, no carve-out: contractions, one recast sentence, one
de-hyphenation and one dropped "simply".

Tense: four docs described system behaviour in the future ("will
refuse", "will fetch and trust"). Reference docs get read from the
other side of the change, so they say what the system does.
2026-09-20 22:07:16 +02:00
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
iris
2346bef3b1 docs: gate write-good.Passive on CI
Both blockers are done — #4548 (353 hits in hand-written docs) and
#4549 (46 hits in generated CLI reference docs, hivectl/swarmctl/hive-forge's
own clap help text) — and the previous commit suppresses the reviewed
false-positive remainder. vale --minAlertLevel=error docs now returns
0 errors, matching CI's existing prose-lint-errors job.

Closes #4546.
2026-09-20 16:24:11 +02:00
iris
04e27c4fb6 docs: suppress reviewed write-good.Passive false positives
133 hits across 38 files, all previously classified during #4548's sweep
and deliberately left un-rewritten (predicate-adjective state/necessity
description, design-intent idiom, structural/type-description idiom,
no-single-actor topology claim, parallel-triple exception, vale
substring-match artifact — see hyperhive#4548's per-PR bodies for the
per-hit reasoning).

Wraps each one in a scoped <!-- vale write-good.Passive = NO/YES -->
pair (the supported mechanism — TokenIgnores has a known offset-drift
bug) rather than a blanket per-file or per-rule silence, so a *new*
passive-voice hit anywhere in these files still fails once the rule
gates CI (next commit). Table/list false positives (docs/swarm/credentials.md's
renewal-table cells) wrap the whole block, not each cell.

Part of #4546.
2026-09-20 16:24:11 +02:00
iris
bf9f9126af docs: clear write-good.Passive hits in docs/tools/forge.md 2026-09-20 14:31:20 +02:00
iris
80e17f536d docs: clear write-good.Passive hits in docs/tools/bash.md 2026-09-20 14:30:39 +02:00
iris
19d2142897 docs: clear write-good.Passive hits in docs/tools/scheduling.md 2026-09-20 14:24:12 +02:00
iris
a0ada39191 docs: clear write-good.Passive hits in docs/tools/lifecycle.md 2026-09-20 13:55:27 +02:00
iris
ffc062707b docs: clear write-good.Passive hits in docs/tools/hivectl.md
5 genuine passive-voice rewrites (actor named — create-user, hivectl,
the operator, claude, you the CLI user); 2 false positives left
untouched — predicate-adjective necessity descriptions ('is needed',
'is required'), same category already established across this sweep.
2026-09-20 13:49:49 +02:00
atlas
f0e3ed04d3 hive-forge, hivectl, swarmctl: fix clap help passive voice, regen docs
Rewrites every write-good.Passive hit in the hive-forge clap help text
into terse, imperative, active voice (meaning unchanged) and drops
clap-markdown's own fixed footer ('This document was generated
automatically by...') via MarkdownOptions::show_footer(false), since
that string isn't ours to reword and vale flagged it too.

docs/tools/{hivectl,swarmctl,forge}-cli.md are generated from each
crate's clap tree (see hive-forge/src/main.rs's MarkdownDocs verb) —
regenerated here from the fixed source, not hand-edited.

Refs #4549
2026-09-20 13:49:39 +02:00
iris
67ffb124cb docs: clear write-good.Passive hits in docs/tools/matrix.md
6 genuine passive-voice rewrites (actor named — the operator,
hive-c0re, the daemon; two converted to active/elliptical-imperative
phrasing matching the bullet list's own established voice); 2 false
positives left untouched — 'been invited' (an agent-perspective
description of its own pending invites, no single inviter is the
point) and 'is keyed' (a structural/type-description idiom, same
category as coordinator.md's 'is typed').
2026-09-20 05:16:17 +02:00
iris
cb504f6863 docs: clear write-good.Passive hits in gateway/subagent/dashboard docs
Part of #4548 — rewriting the 353 hand-written-docs write-good.Passive
hits so #4546 can flip the rule to error. This batch: docs/networking/gateway.md
(27 hits), docs/tools/subagent.md (19), docs/web-ui/dashboard.md (18).

Minimal, meaning-preserving rewrites to active voice only — no
restructuring beyond what each flagged sentence needed. Genuine false
positives (predicate-adjective state descriptions like "is required",
"is broken", "is unchanged", adjectival past participles like "named
buckets", and one quoted literal UI string) are left in place; the
per-line reasoning is on the tracking issue.

Note: docs/tools/forge-cli.md (37 hits, would have been the single
biggest file) turned out to be generated from hive-forge's own clap
help text (see hive-forge/src/main.rs's `MarkdownDocs` verb) — same
category as the already-excluded hivectl-cli.md/swarmctl-cli.md, so
it's untouched here. Flagged on #4548.
2026-09-20 04:13:02 +02:00
atlas
15424d3e49 hive-forge: move pr create --remote's origin default onto the clap arg
Puts the default on #[arg(long, default_value = "origin")] instead of
duplicating unwrap_or("origin") at both call sites (REST/--push path
and agit_create). clap now renders the default in --help itself, so
the hand-written doc-comment default is dropped in favor of it.
Regenerated docs/tools/forge-cli.md to match.
2026-09-19 20:33:48 +02:00
atlas
18f0199566 hive-forge: default pr create --push remote to origin
Fresh clones only have origin, not forge. The --remote override
already covers anyone using a different name.

Closes #4538
2026-09-19 18:16:55 +02:00
atlas
87970a8c93 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
2026-09-19 10:47:39 +02:00
atlas
4a1b5f5d51 docs(forge): drop the flat hidden-verb section from the verb reference
pr-create/pr-status/etc no longer parse -- clap deleted them along
with the renamed() helper. The remaining flat verbs (view, comment,
close, ...) are #[command(hide = true)] in clap; documenting a
deliberately hidden set as a group defeats the point of hiding it.
The examples further down that demonstrate individual hidden verbs
are unaffected.

Refs #4509
2026-09-18 22:43:35 +02:00
atlas
992468dccc docs: reword three vale-flagged sentences in matrix.md
Preserve meaning exactly: 'There is no' -> 'No ... exists', 'That is'
-> 'that's', 'is not' -> isn't.

Refs #4475
2026-09-18 10:35:16 +02:00
atlas
99b141f5f2 matrix: drop the per-agent matrix.enable; accounts are the enable signal
`services.hyperhive.agent.matrix.enable` was a second source of truth for
a fact the account set already carried: after ①-③ the hive-internal
`main` account is an ordinary `matrixAccounts` entry, so "does this agent
have matrix" and "does this agent have an account" were the same question
asked twice, with the boolean able to disagree.

The option is gone and a non-empty `matrixAccounts` now gates the daemon
unit, its token path-watcher and the injected `extraMcpServers.matrix`
entry.

That is only a real condition because `matrixAccounts.main` is itself
gated: it is declared when `matrix.url != null`, never unconditionally. A
`main` with no homeserver is an account the daemon can never log in as,
so declaring one always would have made the signal trivially true and
turned matrix on for every agent in every hive. With the URL gate, the
empty set is reachable exactly for an agent the hive gave no homeserver
and whose operator declared no account of its own — the state the old
`enable = false` expressed.

Assertions: "extras require enable" is deleted, having become the
definition of the thing it checked (an external-only account with its own
homeserver is now rendered rather than rejected). `main.tokenFile` stays
pinned, re-guarded on `? main` instead of on the flag, since `main` is
absent whenever the URL is null and an unguarded index would throw there.

Both spellings of the option get `mkRemovedOptionModule`, following
../host-modules/deploy.nix's registrationTokenFile pair rather than a
silent delete: the definition whose meaning changes is `false`, and left
undeclared it would be ignored and hand the agent the tools its operator
turned off. Failing the eval with the replacement spelling is the only
outcome that cannot.

module-eval gains the three arms — URL, nothing, external-only — with the
middle one carrying why it exists: it is the only thing in the suite that
would notice `main` becoming unconditional again.

Refs #4475
2026-09-18 10:35:16 +02:00
atlas
c74249f371 matrix: make the hive-internal main account an ordinary matrixAccounts entry
`matrixAccounts` is meant to be the agent's full account list, but the
hive-internal `main` account was outside it: the nix module emitted only
the extras and `hive-matrix-daemon` prepended a `main` it synthesized
from the per-agent paths, with the option schema forbidding the name
outright.

nix/agent-modules/matrix.nix now declares `main` itself, as an ordinary
entry under `matrix.enable`, from the state-dir paths the module already
used for its token path-watcher (now a shared `stateDir` binding) plus
`matrix.url`. The whole set, `main` included, is serialized to
HIVE_MATRIX_ACCOUNTS.

accounts::configured therefore synthesizes `main` only when the parsed
list carries none, and otherwise takes the declared one verbatim —
hoisting it to index 0, since the daemon reads index 0 as the primary
and nix serializes an attrset, so `main` sorts wherever its key falls.
Declared xor synthesized: an agent whose harness predates this entry
keeps working, a current one gets its own, and there is no arrangement
where `main` is duplicated or missing.

The reserved-name assertion is replaced rather than dropped: the name
must now be legal (the module uses it), but `main`'s tokenFile stays
pinned to `<state>/matrix-token`, since hive-c0re provisions the
hive-internal token there and nowhere else — a retarget would evaluate
fine and then never restore. The other two fields are mkDefault and free
to override.

Refs #4475
2026-09-18 09:34:44 +02:00
atlas
7570597190 docs: repoint agent-tier option paths to services.hyperhive.agent.*
Refs #4479
2026-09-18 03:05:43 +02:00
atlas
42dcf10064 swarm-logs-cli.md: regenerate from the binary, prettierignore it
The committed doc was stale (missing help text under --limit/--format
that the binary's clap-markdown output now includes). Regenerated with
the documented command. While at it: docs/tools/swarm-logs-cli.md was
never added to .prettierignore alongside its three siblings, so nix fmt
silently reformatted it back to a stale layout right after
regeneration — the exact failure mode .prettierignore's own comment
warns about. Added it so the *-docs-fresh check in nix/checks.nix stays
satisfiable.
2026-09-17 01:02:14 +02:00
atlas
a39399f037 swarm-logs: an agent's CLI for the swarm log store
An agent can reach VictoriaLogs only through the gateway, and since the
machine query route landed the way to read it has been to hand-roll a
client_credentials token request and a curl, per query. This is the CLI
that closes that: `swarm-logs query '<LogsQL>'`, matched log lines on
stdout, so the answer pipes into grep like any other command's.

Built to the plan posted on the tracker thread: own crate, own
docs/tools reference generated off the clap tree, `query` as the one
verb, and the JSON error body surfaced on a non-200 rather than
swallowed. No `tail`: streaming is a different endpoint with a different
response shape, and folding it in here would be a fatter scope than the
ask.

Minting the token is NOT implemented here — swarm-queue-client already
owns the client_credentials request, its error type and its CA handling,
and a token-endpoint fix has to be findable in one place. What this crate
adds is the agent-shaped half: the client id arrives as a *file* beside
the secret, so nothing outside nix/agent-modules/queue.nix spells
`hive-<name>-agent` twice. That is the same problem hive-agent's
swarm_queue module solves, and swarm-logs/src/auth.rs is its `decide`
restated over this binary's inputs.

⚠️ The plan named one thing to verify empirically before calling the auth
settled: whether authelia's bearer policy for the logs vhost accepts the
agent client's audience. Measured from inside a container: it does not.
The client minted a token fine but with `aud: []` and `scp: []`, asking
for the logs URL as an audience answered `invalid_target`, and presenting
the audience-less token to the gateway answered a bare 401. So
swarm-authelia.nix's agentClients gains `authelia.bearer.authz` and the
query URL as a second audience — authelia authorises a bearer token by
the URL being requested, and that URL is now one binding read by three
places rather than three spellings of one address.

The URL reaches an agent the same way its queue coordinates do: computed
on the host (a container cannot derive a gateway address), forwarded by
hive_c0re::meta into the container's option set, and consumed by a new
agent module that installs the binary *wrapped* with its coordinates —
the shape swarm-controller.nix installs swarmctl in. Gated on the queue
credential as well as on the URL: a binary that can only answer 401 is
worse than no binary, because an agent reads a 401 as "no logs", which is
the exact confusion the store's machine route was added to end.
2026-09-17 01:02:14 +02:00
atlas
5ce0a357b4 subagent: grant claude's Bash when the agent holds the execution group
A subagent inherited the parent's built-in tool list, which correctly has
no `Bash` -- the agent reaches a shell through the `bash` MCP server, not
the built-in. Subagents get no such server, so the intersection was empty
and they could not run a command at all: no commits, no pushes, no gates.

Add `subagent_builtin_tools_for`/`_arg`, which reuse the shared resolver
and append `Bash` only when `Execution` -- the group that gates the `bash`
MCP server -- is present. Only the subagent spawn path calls them, so the
harness's own `--tools`/`--allowedTools` are unchanged.

The capability transfers; the mechanism does not.

Refs #4422
2026-09-15 18:48:51 +02:00
atlas
9cd30a58ba subagent: say why we refuse an empty --tools, not what one would do
The comments and docs around the empty-`--tools` assert stated a
mechanism: that an empty value parses as *unset* and therefore grants
MORE built-ins than omitting the flag. That claim came from a
measurement, and the installed `claude --help` (2.1.268) says the
opposite in as many words — `Use "" to disable all tools`. One of the two
is wrong and this repo is not the place that settles it.

The rule the code enforces is right under both readings, which is
precisely why it should not be argued from either: we never emit an empty
`--tools`, because what an empty one means is release-dependent and
nothing here wants a subagent with no built-in tools regardless. So the
prose now states the rule and the ambiguity, and asserts nothing about
`""` in either direction — replacing the claim with its opposite would be
the same mistake sourced differently.

No behaviour change: the assert, the resolution and every test are
untouched.

Two test comments also gained the limit they were quietly missing.
`no_spawned_tool_escapes_the_session` hardcodes ten names, so it is a
canary for tools someone thought to list, not a guarantee — the
guarantee is the subset property, which covers tools nobody has invented
yet. And `a_subagent_gets_no_builtin_its_parent_lacks` resolves both
sides in one process off one env var, so it catches a code divergence but
not the two real systemd units disagreeing about `HIVE_TOOL_GROUPS` —
which is what they did until the previous commit, and is not a thing a
unit test can reach.

Refs #4416
2026-09-15 17:40:27 +02:00
atlas
d6c8cd5a6f subagent: hand a subagent its parent's built-in tools, and no others
`build_config` spawned a subagent with `--dangerously-skip-permissions`
and no `--tools` at all, so it got claude's entire built-in set —
`SendMessage` and `ListAgents` (message peers, or the operator, as its
parent), `Task*` including `TaskStop`, which takes an *agent* id and so
reaches clean outside the run, `Cron*`, `RemoteTrigger` and
`EnterWorktree`/`ExitWorktree`. None of that is part of "do this bounded
task in this directory", and none of it is something the parent agent
itself can do: the harness has always passed `--tools`.

Pass the same one. The value comes from
`hive_sh4re::permissions::builtin_tools_arg()` — literally the function
the harness resolves its own session with — so the subagent's set is the
parent's set, `HIVE_TOOL_GROUPS` and all. That inheritance is the
requirement, not an implementation detail: a hardcoded subagent list
would hand `WebFetch`/`WebSearch` to the subagent of an agent without the
`web_tools` group, which is a privilege escalation, and would drift from
the parent's list the first time anyone added a tool to either.

`--tools` is the real gate: it holds under
`--dangerously-skip-permissions`, unlike `--allowedTools`, which only
auto-approves prompts. It does not filter MCP tools, so the
`goal_reached`/`need_help` signal surface is deliberately unnamed in it
and survives on `--strict-mcp-config` alone.

`build_config`'s doc comment claimed `strict_mcp_config` was *the* safety
property and that a subagent got "nothing implicit and nothing more".
That was false for built-ins, and is what hid this gap for as long as it
did; it now says which flag covers which half and that neither
substitutes for the other.

An empty `--tools` value parses as *unset* and grants more than omitting
the flag, so an empty resolution can only be a bug — `build_config`
asserts against it and a test pins the non-emptiness alongside the
subset-of-parent property.

Refs #4416
2026-09-15 17:40:27 +02:00
atlas
34129d776c subagent: give each run its own signal URL, and drop the name argument
`goal_reached`/`need_help` took the session name as a tool argument, so
identity was an assertion by the caller and the only guard on it was
`occupancy()` — "does that name have a turn in flight", which two
concurrently running siblings both satisfy for each other. A subagent
could stop its sibling's run by naming it.

Identity moves into the URL. Each spawned run is minted an unguessable
token (`Uuid::new_v4`, the OS CSPRNG), the URL carrying it goes into that
one subagent's own `--mcp-config`, and the route resolves it back to a
session before dispatching to a handler bound to that session. Neither
tool takes a `name` any more: a subagent has no field in which to name a
sibling, and a sibling's name — which a brief may well mention — is not a
token.

One route with a path parameter, not a route per session: the `Router` is
built once at startup and subagents come and go for the daemon's whole
life. An unminted or revoked token gets a bare 404, the same answer either
way, so nothing enumerates. A run's token is revoked when the run ends
(`finish_turn`) or when a call never reached a spawn.

Two things fall out of that:

- the config file becomes one per session. A single shared path was
  already a race between two `start`s; with a per-session URL in it, the
  loser would read the winner's identity.
- `occupancy()` stops being the identity guard and is gone from the signal
  path entirely rather than kept "just in case" — a revoked token can't
  reach it, and it never answered the question it was standing in for.
  It still backs `status`, which is what it was always actually for.

Refs #4403
Refs #4413
2026-09-14 22:24:51 +02:00
atlas
b18348bc9a subagent: give a run a goal, turns toward it, and a reason it stopped
`start` takes an optional `goal`. With one set a session stops being a
single turn: when a turn ends and nothing has said to stop, the daemon
spawns another turn re-prompting the subagent toward that goal, up to
`max_turns` (default 5, per-session). Without a goal nothing changes —
one turn, one todo, same as before.

Four things end a run, each recorded distinctly and reported by `status`:
the turn ending with no goal, `goal_reached`, `need_help`, and the turn
cap. The last says so out loud rather than stopping quietly — the todo
states the harness limit was reached and the goal was never reported
reached. Every stop extends the done message rather than replacing it,
and lands in the session's report file when it has one. The path is
never inferred: it comes from `start`'s `report_file` or from the
subagent naming where it wrote.

`goal_reached` and `need_help` are the subagent's own, served on a second
route (`/signal/mcp`) that carries those two tools and nothing else, so
reporting on a run can't become starting one. `goal_reached` is built as
a label, never a gate: it is self-reported by a subagent that has just
been re-prompted with "you haven't reached the goal", which is exactly
the incentive to claim it — the same failure class as a build report
asserting the tests pass. Every surface that renders it says so.
`need_help` is the blocking signal, and shows in `status` as its own
state so a parent polling it sees the block without reading a file.

`status` also carries `turn N of M`: with 4330's last-event age, that
separates working from wedged from out of turns off one answer.

Two bugs the new tests caught: a `tokio::fs::File` was dropped without
flushing, so the report line was written to nothing, and the plain idle
answer dropped the turn counter.

Also documents `await_resume`'s third case — a closed channel with no
send, which fails open the same as `Underway` — per argus on #4411.

Refs #4403
2026-09-14 21:46:59 +02:00
atlas
6e2de33f26 subagent: make a missed continue the tool call's own error
`continue` returned "started" the instant `Claude::spawn` handed back a
pid, and a resume that matched nothing only surfaced later, as an
end-of-turn todo. By then the caller had moved on believing it had a
running subagent.

A pid is proof enough for `start`, which creates its session: the spawn
succeeding is the whole story. It is not proof for a resume — claude
exits non-zero a fraction of a second *after* the process exists. So
`continue` now waits for the first real answer and reports a miss as its
own `Err`, carrying claude's message and the directory searched.

The wait ends on whichever comes first, so a successful `continue` pays
no fixed delay: the turn's first non-terminal stream event settles it at
about the same moment a miss's exit would have. Measured on this box:
14 runs of the driver's own invocation against a missing session took
550-1087 ms spawn to exit, and a healthy turn's first event lands at
roughly 500 ms. The five-second cap is ~4.6x the slowest miss and is only
ever reached by a child that neither speaks nor exits.

The underway signal reads the event's kind, not its content: a missed
resume is not silent — it emits a terminal `result` event and stderr
before exiting — so "any sink callback" would have reported every miss as
a successful start. Liveness still counts all three callbacks.

The end-of-turn todo is unchanged for every failure later in the turn;
the only one it no longer repeats is the miss the caller was just handed.

Refs #4405
2026-09-14 20:56:16 +02:00
atlas
31c76ddf32 subagent: say the dir a session was started in, not "pass dir"
A session cannot be moved between directories, so a hint reading
"pass dir" could be misread as pointing an existing session at any
directory. Say instead that dir names the directory the session was
started in.

Refs #4405
2026-09-14 20:56:16 +02:00
atlas
307df77948 subagent: report turn liveness, and stop pre-checking continue
`status` could only answer running / starting / idle / killed / none,
because every turn ran against `&NoopSink` and the whole stream-json
stream was discarded. "Running" describes a wedged subagent exactly as
well as a busy one, leaving a caller to tell them apart from `ps` output
and CPU-time deltas.

So the daemon now keeps a `name -> last_event_at` clock, bumped by
`LivenessSink` on every line of every stream — stream-json events, plain
stdout chatter and stderr alike — and `status` reports its age on a
running answer: a few seconds means working, an age climbing into the
minutes with no end-of-turn todo means wedged. Nothing is read out of the
content; classifying *what* a subagent is doing is a separate question
and waits on its own driver work. In memory with the rest of this
daemon's state, dropped when the turn ends, no persistence.

The clock is seeded at the spawn rather than at the first line, so a
subagent that wedged before emitting anything still reports a climbing
age rather than no age at all — the case an age is worth most in.

Separately, `continue`'s existence pre-check is gone. It could only
repeat the lookup `Claude::spawn` was about to do, and its message —
"no session named `x` exists" — was false in the common failure: the
session existed, just not under the claude home + cwd `build_store`
resolved from. claude's own `--resume` is the authority and exits
non-zero (`does not match any session title`) rather than quietly
starting a fresh session, so the turn fails on its own. `classify_end`
appends the one fact the CLI's message lacks — the directory searched:

  claude error: no session matched the requested id or title (searched
  <claude_home> for cwd <cwd>; if the session was started elsewhere,
  pass `dir`)

The `dirs` map's durability is untouched; whether to persist it stays an
open operator decision.

Module doc, `docs/tools/subagent.md`, the `continue`/`status` tool
descriptions and the `base:claude-subagents` skill all updated — including
`continue`'s `dir` doc, which said "the daemon remembers it" without
saying that a restart is both when it forgets and when you most want it.

Refs #4330
Refs #4405
2026-09-14 20:56:16 +02:00
atlas
30fa54cbc6 feat(swarmctl): add agent create, queueing the swarm-controller creation DAG
`swarmctl agent create <name> --hive <hive>` POSTs `/api/agents` to
swarm-controller over the daemon's unix socket and prints the queued
job's node id.

It deliberately does not wait. The endpoint queues a DAG whose last node
*publishes* a deploy message; the hive's `hive-c0re` then converges on
its own clock, out of the controller's sight. So even a fully settled
graph would not mean the agent is up, and there is nothing this CLI
could wait for that would let it claim otherwise. Printing the id is
exactly what the response says and all of what it says.

Transport is a bare hyper HTTP/1.1 client handshaked onto a tokio
`UnixStream` via `hyper_util::rt::TokioIo` — the same crate family
`hivectl/src/watch.rs` and `hive-agent/src/web_ui/proxy.rs` already use,
all of it already workspace-pinned. The request/response shapes are a
local mirror rather than a shared crate: the controller's own types are
private to its binary and this crate does not link it, the same
separation `hivectl` keeps from `hive-c0re`.

Errors are reduced to one actionable line — the controller answers
RFC 9457 problem+json, so an unknown `--hive` reaches the operator as
the roster of hives that would have worked rather than a body dump.
Response `warnings` are printed when non-empty.

The nix module wraps the binary with `SWARM_CONTROLLER_SOCKET`, read
from the same `socketPath` the daemon binds.

Refs #4399
2026-09-14 19:40:23 +02:00
atlas
a3b672d1d5 refactor(hive-c0re): drop the request_init_config tool and InitConfig approval
swarm-controller's `InitAgentConfigRepo` node already covers config-repo
creation, so this deletes a duplicate rather than a capability; old
`init_config` rows are skipped by `collect_lenient` with no migration, by
operator decision.

Refs #4398
2026-09-14 19:03:44 +02:00
atlas
0bdee751b9 subagent daemon: address review on the killed-session docs
argus's review on #4333 flagged one real vale error (Microsoft.Contractions):
"did not" in the new "A killed turn" section reads as "didn't" instead. The
rest of the diff's new prose (docs/tools/subagent.md and the tool
descriptions in hive-subagent-mcp/src/mcp.rs) has no other instance of the
same expansion, confirmed both by a local `vale --minAlertLevel=error` run
(clean) and by grepping the added lines. Also tidies session.rs's module doc:
the two `//!` runs split to dodge the 30-line comment-block lint had a bare
trailing `//!` right before the blank separator; dropped so the first run
ends on content.

Refs #4326
2026-09-13 15:15:38 +02:00
atlas
f817e27d4c subagent daemon: report a killed session as killed, not idle
A subagent whose claude process died on a signal — the kernel's OOM
killer, a stopped unit, an `interrupt` — was indistinguishable from one
that finished its turn: its entry left the `running` map, `status` fell
through to "a session exists on disk" and answered `idle`, and the
end-of-turn todo said the subagent had "finished". The usual next move
on that reading is `continue`, which resumes work that was cut mid-turn
with nothing having recorded that it was cut.

The driver already preserves how the child ended — `RunningClaude::wait`
returns `Error::Exit` carrying the `ExitStatus`, whose `signal()` is the
whole answer — so this reads it rather than having to recover it:
`classify_end` turns the outcome into `Complete` / `Killed { signal }` /
`Failed`, and `State::finish_turn` remembers a kill against the name
(cleared by the next confirmed spawn under it).

What an agent sees as a result:

- `status` reports the session killed, naming the signal, instead of idle;
- the todo the daemon pushes without being asked says the subagent was
  KILLED mid-turn rather than that it finished;
- `continue` still resumes such a session, but its reply says the
  previous turn was killed, so no caller carries on from cut-off work
  believing it was complete.

Refs #4326
2026-09-13 14:58:45 +02:00
damocles
670e0ccad3 docs: drop the auto-injected hyphen vale flags 2026-09-13 13:57:53 +02:00
damocles
16eec3c314 subagents: add availableToSubagents opt-in toggle for extraMcpServers 2026-09-13 13:57:53 +02:00
damocles
a2d40dc1bd docs: regenerate forge-cli.md for the --limit/--tail opt-in swap
fixes hive-forge-docs-fresh check on #4207
2026-09-11 19:13:30 +02:00
atlas
db0eb8829b hivectl, docs: choom is not root-only, and set-limits takes no agent name
Two unrelated changes landed correctly and left their prose behind, both
on operator-facing surfaces.

`hivectl agent <name> choom` gained a `hive-admin` path when hive-c0re
started shipping a polkit rule for `machinectl shell`. `choom.rs`
implements exactly that — `euid != 0 && !in_hive_admin_group()` — but
the `Choom` variant's clap doc comment still said "Requires root and a
running container", eight weeks on. That string is what
`choom --help` prints and what renders into the generated CLI
reference, so it is the sentence an operator actually reads, and it
tells a `hive-admin` member the command is not available to them.
`docs/turn-loop/mcp.md` carried the same staleness inside a
trust-boundary argument ("an operator (root) action"), where naming a
narrower reachable set than the real one is the wrong way to be wrong.

Dating it rather than asserting it: the acceptance landed 2026-09-07,
the doc string 2026-07-16.

Separately, `docs/tools/hivectl.md` states the agent-name hoist itself
("the name is hoisted onto the parent command, so none of the verbs
below repeat it") and its own example block obeys it, then twice writes
`set-limits sock --memory-max 8G` — the pre-hoist order. The generated
reference is unambiguous: `hivectl agent <NAME> <COMMAND>` versus
`hivectl agent set-limits [OPTIONS]`, with `quota set <SIZE>` nearby
showing that positionals do render when they exist. The costly one is a
complete command in backticks inside the paragraph explaining the
footgun it illustrates.

`docs/tools/hivectl-cli.md` is regenerated rather than hand-edited; the
diff against the committed copy is exactly one line.

Closes #4236.
2026-09-11 19:11:13 +02:00
damocles
f80947f4cc hive-sh4re, docs: fix the two docs argus flagged on ToolGroup::Execution
addresses review on #4245
2026-09-11 19:01:31 +02:00
atlas
6d7565a30d scheduling.md: drop the list_schedules prose, keep the approval-scope fix
mara's call on this PR was "list schedules not being scoped at all is a
bug - dont document it, file the bug and fix it". The bug is fixed in
damocles's separate PR, which also rewrites this page's
`list_schedules()` section.

So both of my paragraphs about scoping go: the "not scoped at all"
sentence in the intro (documenting the bug, which is what she
objected to) and the follow-up in the `list_schedules()` section. That
section is now byte-identical to main again, leaving it entirely to the
PR that changes the behaviour — the two PRs no longer touch a common
hunk in this file.

What stays is the claim this PR was actually filed for: the page said
"All scheduling ops go through the operator approval queue", and only
creating one does. The intro now splits creating from the other four
verbs and states the one authorization rule that covers all of them,
which the scoping fix makes true.
2026-09-11 18:43:10 +02:00
atlas
c28da210b4 scheduling: clamp get_logs host-side, and fix two authorization claims
`docs/tools/scheduling.md` said three things about who may do what. Two
were wrong prose; the third was the code.

"All scheduling ops go through the operator approval queue" — one of the
five does. Cancel, edit, list and fire are `require_group("scheduling")`
and nothing else (socket_server/mod.rs:594-643), which is what the MCP
tool descriptions already tell an agent. The page pushed in the cautious
direction: someone watching a runaway recurring schedule would wait for
an operator rather than cancel it themselves.

The authorization sentence covered "read/cancel/edit". Cancel, edit and
fire really do check `cancel_authorized` (self, operator, or subtree) from
three call sites. `handle_list_schedules` takes no requester at all and
returns every row — now stated, along with the part that matters: the
snapshot includes other agents' schedule bodies.

`lines` was documented as "host-capped at 500" and the 500 was in the
agent's own MCP layer, not the host; `handle_get_logs` passed any u32
straight into JournalQuery. A limit in the caller is not a limit, so the
host clamps instead of the sentence changing. That also makes args.rs's
arg doc and the tool description agents read correct, untouched. The
sibling `get_host_journal` already clamps host-side at 100, which is both
the precedent and the control that the missing clamp here was real.

Closes #4230.
2026-09-11 18:43:10 +02:00