Commit graph

4,704 commits

Author SHA1 Message Date
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
2aa9f19ccb docs: clear write-good.Passive hits in docs/trust-boundary/security.md
2 genuine passive-voice rewrites (actor named — a caller, the
hard-coded unit name); 5 false positives left untouched — a
definitional/classificatory statement ('an agent is trusted code'),
a predicate-adjective state description ('is privileged'), two
quoted-literal framing phrases ('can the agent be stopped...', 'the
whole hive is affected'), and the already-established 'is tracked as
a TODO' pattern.
2026-09-20 13:24:24 +02:00
iris
6f0985824c docs: clear write-good.Passive hits in docs/web-ui/shape.md
5 genuine passive-voice rewrites (actor named — DOMPurify.sanitize()
x2, paintAtomic, the keyed pattern, the operator); 2 false positives
left untouched — predicate-adjective state descriptions ('are
unaffected', 'is gone'), same category as several already-established
in this sweep.
2026-09-20 13:24:24 +02:00
iris
ad16f3dba5 docs: clear write-good.Passive hits in docs/agent-lifecycle/agent-hierarchy.md
3 genuine passive-voice rewrites (actor named — the operator,
hive-c0re, the harness); 5 false positives left untouched — a
design-intent idiom ('are meant to run'), a predicate-adjective state
description ('is finished'), a predicate-adjective necessity
description ('is required', the original established example for this
category), a project-planning-state description ('is planned'), and a
quoted literal error string ('must be owned by uid 0...').
2026-09-20 13:24:24 +02:00
iris
c75604aa42 docs: clear write-good.Passive hits in docs/swarm/sso.md
6 genuine passive-voice rewrites (actor named — the operator,
secrets.md, swarm-authelia.nix, the person who introduced the split,
swarmctl); 1 false positive left untouched — a quoted phrase +
predicate adjective ('presenting as "the forge is broken"').
2026-09-20 13:24:24 +02:00
atlas
f304fd3069 docs/process: keep 'automatically' when fixing the auto-deployed hyphen 2026-09-20 05:36:49 +02:00
atlas
f7843d727d hive-c0re: fix stale GetLooseEnds example in dispatch_shared doc comment
The parenthetical claimed the manager can target other agents via
GetLooseEnds — true before 65481273, false since. It was the only
socket-specific example that comment ever had, so rewrite the sentence
to name what dispatch_shared actually defers: ListDescendants and the
orchestration verbs, which need per-verb tool-group gating, plus
host-admin/unknown requests.
2026-09-20 05:36:49 +02:00
atlas
07ec63cc3f hive-agent-mcp: nix fmt 2026-09-20 05:36:49 +02:00
atlas
729c5b4f42 hive-agent-mcp, hive-c0re, hive-sh4re: drop agent param from get_loose_ends
get_loose_ends now always returns the caller's own loose ends, for every
caller including the manager (ruth) — there is no separate manager
surface, ruth is a normal agent with different default capabilities.

- AgentGetLooseEndsArgs removed; get_loose_ends takes no args.
- Wire Request::GetLooseEnds collapses from an Option<String> target to
  a unit variant.
- hive-c0re's handle_get_loose_ends drops the "*" hive-wide branch and
  the subtree/capability resolver (resolve_agent_state_target); both
  are gone since there is no longer a target to resolve.
- loose_ends::hive_wide and Capability::QueryAgentState removed as
  dead code — their only callers were the two functions above.
- is_descendant_of is untouched (still used by lifecycle_handlers.rs
  and schedules.rs independently of this change).
- Docs updated: docs/turn-loop/mcp.md, docs/web-ui/dashboard.md,
  docs/process/conventions.md (Loose-ends wire shape + capabilities
  table), plus the doc comments in hive-core-agent-sock, mcp_config.rs
  and capabilities.rs that described the old shape.

Refs #4480
2026-09-20 05:36:49 +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
3ae8b081e0 docs: clear write-good.Passive hits in docs/trust-boundary/boundary.md
6 genuine passive-voice rewrites (actor named — the core daemon,
hyperhive, the adminUsers option; one flipped subject to match the
table it introduces; two converted to active/imperative phrasing);
2 false positives left untouched — the already-established 'is
tracked as [area:ops] issues' pattern, and a design-intent
contrastive idiom ('ownership is declared, not repaired') matching
the unflagged sibling phrase 'a DAG is declared, not described' in
coordinator.md.
2026-09-20 05:11:37 +02:00
iris
f047e509a7 docs: clear write-good.Passive hits in docs/scheduler/coordinator.md
5 genuine passive-voice rewrites (actor named — nixos-container
create; the passive construction replaced with an active
intransitive verb or 'nothing can X' phrasing matching the pattern
already established on credentials.md); 4 false positives left
untouched — a type-description idiom ('is typed'), a predicate-
adjective in a parallel triple ('is satisfied' alongside two
untouched siblings), and two already-established categories
('is unaffected', 'is tracked' as a follow-up).
2026-09-20 05:10:59 +02:00
iris
f6756de925 docs: clear write-good.Passive hits in docs/getting-started/setup.md
5 genuine passive-voice rewrites (actor named — pkcs11, the operator,
the minting policy; one converted to a runbook imperative; one
converted to an active 'gate on' verb); 5 false positives left
untouched — predicate-adjective necessity/security-property
descriptions ('is needed', 'is stored', 'is authenticated', same
category as the already-established 'is required'), a
project-tracking-state description ('is tracked'), and a topology
claim with no single correct actor to name ('is generated where it's
read' — which component mints a given credential varies per row in
the credentials table).
2026-09-20 05:10:26 +02:00
flake-bot
df283a5260 nix flake update 2026-09-20 05:01:13 +02:00
iris
63830e57b7 docs: clear write-good.Passive hits in docs/scheduler/observability.md
4 genuine passive-voice rewrites (actor named, or the passive
construction replaced with an active imperative); 7 false positives
left untouched — predicate-adjective state/necessity descriptions
('is unchanged'/'is needed'/'is enabled' x2/'is unauthenticated'/
'is unaffected', same category as the already-established 'is
required') and one quoted-literal example string ('this hive is
gone').
2026-09-20 04:40:43 +02:00
müde
78ebd7db3f fix doc-comment pointers broken by the module-eval split
Both referenced the now-deleted nix/module-eval.nix. The nix side of
each pairing is spread across multiple files post-split, so drop the
cross-reference rather than chase it across files.
2026-09-20 04:31:08 +02:00
iris
04cac80c97 docs: clear write-good.Passive hits in docs/process/gotchas.md
7 genuine passive-voice rewrites (actor named, or the passive
construction replaced with a natural active verb/imperative); 5 false
positives left untouched — a temporal 'when it was written' reference,
two predicate-adjective necessity descriptions ('is needed'/'is
expected', same category as 'is required'), one predicate-adjective
state description ('are gone'), and one quoted literal + predicate
adjective ('these inputs are unused by rust').
2026-09-20 04:28:16 +02:00
müde
dc418a5223 nix: split module-eval into per-subsystem checks
The single module-eval derivation forced ~62 full nixosSystem
fixtures live at once to compute its cases list: 10.6GB peak RSS /
5m25s to evaluate, by far the dominant cost in nix flake check.
Splits it into 21 independent checks.module-eval-* derivations
(1-7 fixtures each) sharing builders/helpers via module-eval/lib.nix,
so no single derivation needs more than a handful of fixtures live
at once. A few cases spanning two clusters carry a small duplicated
fixture rather than threading shared state through lib.nix.
2026-09-20 04:25:54 +02:00
iris
69b70a9c6f docs: clear write-good.Passive hits in docs/swarm/credentials.md
6 genuine passive-voice rewrites (actor named or replaced with an
active construction); 7 false positives left untouched — the
design-intent 'is meant to have' idiom, and the six identical
'must be stated' renewal-column table cells (a deliberate terse
placeholder directive, not a describable action with an actor).
2026-09-20 04:22:01 +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
iris
9fac33a026 docs/swarm/secrets.md: fix sense-mis-attribution in certificateFiles rewrite
argus caught it on PR #4557 review: 'certificateFiles reads at system
build time' hands the option itself a verb it can't perform. The
system reads the option's value at build time, not the other way
around.
2026-09-20 04:10:55 +02:00
iris
c985702bb0 docs: clear write-good.Passive hits in swarm secrets/README docs
Part of #4548 — third batch. This PR: docs/swarm/secrets.md (16 hits),
docs/swarm/README.md (15 hits) = 31 more of the 353 (130 total across
the three PRs so far).

Same method as #4551/#4553: minimal, meaning-preserving active-voice
rewrites, no restructuring beyond what each flagged sentence needed,
no vale-off silencing.

Genuine false positives left in place: predicate-adjective state
descriptions ("is required" x4, "is enabled", "is disabled"), one
systemd terminology reference ("is wanted (not required)" — literally
naming systemd's Wants=/Requires= dependency-type distinction, not
passive voice), and one design-intent idiom ("both are expected" —
same shape as earlier docs-lint work).
2026-09-20 04:10:55 +02:00
iris
18c5b5c5ad docs: clear write-good.Passive hits in matrix/persistence docs
Part of #4548 — second batch. This PR: docs/integrations/matrix.md
(18 hits), docs/agent-lifecycle/persistence.md (17 hits) = 35 more of
the 353 (99 total across the two PRs so far).

Same method as the first batch (#4551): minimal, meaning-preserving
active-voice rewrites only, no restructuring beyond what each flagged
sentence needed, no vale-off silencing.

Genuine false positives left in place, with per-file reasoning in the
PR body: predicate-adjective state descriptions ("is wedged", "is
stopped", "are unrelated", "is unaffected", "is needed"), one
design-intent idiom ("was meant to prevent" — same shape as earlier
docs-lint work), and one vale parsing artifact: "is read" flagged
inside the hyphenated compound adjective "read-only", not real passive
voice at all.
2026-09-20 04:10:41 +02:00
müde
27803e9b97 nix: drop module-eval rename/removal shim tests
mkRenamedOptionModule/mkRemovedOptionModule are nixpkgs' own tested
machinery; proving each call site resolves is redundant with what the
library already guarantees. Removes 10 of the ~79 full nixosSystem
fixtures (wireguard/forge/matrix/controller/ui/stores/statusPublish/
peers old-path + matrixMovedAppserviceToken/matrixRemovedToken) and
their 15 dedicated cases. Fixtures still exercised by genuine
behavioral cases (natsOldPath, autheliaOldPath, grafanaOldPath) are
kept, minus their now-redundant rename-only case.
2026-09-20 04:07:01 +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
iris
2e97ab9dc5 hive-c0re: fix a stale function-name reference in a comment
argus caught it in review: a comment still named is_byte_size, which
this PR's own diff renamed to normalized_byte_size.
2026-09-19 15:07:52 +02:00
iris
27bed74d42 hive-c0re: accept friendly MemoryMax size spellings, normalize for systemd
mara hit this directly: setting an agent's memory cap to "16GB"
fails with 'invalid MemoryMax "16GB": expected a size such as
"8G"...'. Confirmed directly against a running systemd 260
(systemd-run -p MemoryMax=<value>): systemd's own parser accepts a
bare byte count, <digits>B, or <digits> plus exactly one uppercase
K/M/G/T, and rejects both "16GB" (redundant B after a multiplier)
and "16g" (lowercase) with "Invalid argument". So the prior
rejection of "16GB" matched systemd, but is bad UX for input a
human reasonably expects to work.

validate_memory_max now returns the value to store (not just an
ok/err verdict): it accepts 8gb/8Gb/8GB/8g/8G interchangeably and
normalizes all of them to systemd's own 8G form before it is ever
persisted or passed downstream. Also fixes the adjacent bug the same
investigation turned up: the old validator incorrectly accepted
lowercase (8g) even though systemd itself rejects it.

Updated both call sites (server.rs, dashboard/lifecycle_ops.rs) to
use the normalized return value. New test
friendly_size_spellings_normalize_to_systemds_own_form; flipped the
old "8GB should be rejected" assertion, which encoded the
human-hostile behavior this fixes.
2026-09-19 15:07:52 +02:00
atlas
a704a844d1 hive-forge: dependency remove accepts the forge's 201 Created
The forge always answers 201 Created for a successful dependency
removal, but the generated forgejo-api client only treats 200 as
success for that endpoint, so every successful removal surfaced as
ForgejoError::UnexpectedStatusCode(201). apply_and_verify caught this,
read the dependency list back, saw the edge was gone, and printed a
"converged despite a reported error" warning — appropriate for a
genuinely unpredictable anomaly, but this mismatch is deterministic and
not a real problem.

Treat that specific status as success outright on the remove path
instead of routing it through the read-back/warning path. The add
path's different, genuinely intermittent 500-despite-success quirk is
untouched, and a real remove failure still goes through the read-back
and still surfaces if it does not converge.

Closes #4531
2026-09-19 14:32:28 +02:00
atlas
97cde357e5 nix: derive hive identities and the token endpoint from the swarm, not this host
Two swarm-wide facts were being read off this machine's deploy set, so the
answer differed between two hosts of one swarm:

  - `swarm.authelia.oidc.hiveIdentities` defaulted to `deploy.nats.enable`,
    so whether a hive gets an identity at all depended on whether the IdP
    host happened to also run the queue. It is on by default now: a swarm's
    hives have identities, and the clients are inert until used.

  - `swarm.statusPublish.tokenEndpoint` defaulted through `queueLocal`
    (`deploy.nats.enable && deploy.authelia.enable`), so a hive that was not
    the swarm host had no token endpoint even when the swarm's IdP was
    reachable and named. It follows `swarm.authelia.url` now — the same
    derivation `swarm-controller.nix`'s own `queue.tokenEndpoint` already
    uses, which is correct for a remote provider.

`deploy.nix:1-30` is what makes this a rule rather than a preference:
`swarm.*` is "identical on every host, byte for byte" and `deploy.*` is
"necessarily different on every host". A swarm value derived from a deploy
value cannot satisfy both.

The all-or-nothing status-publish assertion follows: the token endpoint is
no longer one of the coordinates that says this hive publishes — every hive
in a swarm with an IdP has one — so the two per-host coordinates are what
must agree, and they now require the endpoint rather than being counted
beside it.

`queueLocal` itself stays for the three remaining host-local addresses
(`natsUrl`, `clientSecretFile`, `agentNatsUrl`): each of those is a
`deploy.*` value that genuinely differs per host.

Closes #4048
2026-09-19 14:31:19 +02:00
atlas
a358889384 docs(gateway): describe nginx as per-host, not a deployment-wide singleton
Fix wording flagged on #4521 review: "Single nginx in front of every
hyperhive web surface" and "Runs on the host" claimed a deployment-wide
topology. There's one nginx per host that has something on it, and
potentially more inside service containers. Reword to describe what
this module builds on this host, with no count claim swarm-wide.
2026-09-19 13:58:10 +02:00
atlas
d8e26a17bb nix: give the gateway, resolver and bridge their own enable
`services.hyperhive.gateway.enable`, `gateway.dns.enable` and
`network.enable` replace the `hyperhive.enable` gate on all three. Each
defaults to false; the modules that need one assert it with `mkDefault
true` from inside the guard their own deployment already carries, and
`swarm-required-services.nix` — the module that owns what the
swarm-services toggle implies — asserts all three explicitly.

hive-c0re asserts all three unconditionally, so an ordinary hive keeps
getting them with no opt-in: it is the host's only knowledge that agent
containers exist.

The resolver moves to its own `hive-gateway/dns.nix` so it can be gated
without reindenting the nginx half of the module.

Reinstates `network.enable`, dropping its `mkRemovedOptionModule` shim.
A config still carrying `network.enable = false` from before the removal
now switches the bridge off instead of failing eval.

Also deletes a duplicate `centralToggleOff` fixture in nix/module-eval.nix.
Two sibling slices added it independently (c5f60fd5, ce3b3d94); the merge
was textually clean and left `main` failing to evaluate at all, so this
file could not be gated without removing one.
2026-09-19 13:53:10 +02:00
atlas
a4e4016214 nix: drop duplicate centralToggleOff fixture from rebase conflict
The rebase onto main resolved an additive conflict in
module-eval.nix by keeping both fixture blocks, leaving
centralToggleOff defined twice and failing nix flake check with
'attribute centralToggleOff already defined'. Drop the later
duplicate; the earlier definition already covers every downstream
case that references it.
2026-09-19 13:41:11 +02:00
atlas
ed1fce3329 nix: gate the avatar-sync path unit on the same condition as its service
`systemd.paths.forge-avatar-sync` was gated on `agent.icon != null` alone,
while the `systemd.services.forge-avatar-sync` it triggers is gated on
`agent.icon != null && agent.forge.url != null`. An agent with an icon and no
forge URL therefore rendered a `.path` unit, pulled into multi-user.target,
watching for a forge-token whose arrival would activate a unit that does not
exist.

The module already documents the fixed behaviour: `forge.url`'s own option
description says the tea-login and avatar-sync units are "not generated at all"
when it is null --- an absent integration, never a misdirected one. That
sentence was true of the oneshot and false of its watcher.

Latent, not live: hive-c0re renders `forge.url` into every agent's config from
the host's `HIVE_FORGE_URL`, so on a real hive it is always set and the
asymmetric arm is unreachable. It is reachable wherever the agent modules are
evaluated outside a hive.

A module-eval case pins both halves absent for an agent with an icon and no
forge; it fails on the parent commit, where the path unit renders.
2026-09-19 13:26:17 +02:00
atlas
fb9a2c0830 nix: attach the prometheus receiver whenever either scrape option is set
The receiver was defined for `scrapeTargets != {} || publishedScrapeTargets
!= {}`, but the swarm-tier pipeline named it only for `scrapeTargets != {}`.
A collector with published targets and no loopback ones therefore rendered
scrape configs that belonged to no pipeline: requested, parsed, delivered
nowhere, and valid enough to deploy.

Latent, not live: the module seeds `scrapeTargets.collector` under its own
`enable`, so the loopback set is never empty in a real deploy and the
disagreeing arm is unreachable today. Nothing would have noticed if that
seeding became conditional, which is why the module-eval case pins the
pipeline rather than trusting the seed to keep masking it.

Refs #4511
2026-09-19 10:57:03 +02:00
flake-bot
79f84a8b70 nix flake update 2026-09-19 10:48:39 +02:00
atlas
b6e180dfbf agent: make claudePlugins additive instead of replacing
The base set (skill-creator + base@hyperhive) was declared via the
option's `default`, so a per-agent definition of claudePlugins
replaced it wholesale. Move the base set to a plain `config`
definition instead: a listOf option merges multiple plain definitions
by concatenation, so an agent's own list now adds to the base set
rather than replacing it, while lib.mkForce / lib.mkOverride on the
agent side still replace the whole merged list deliberately (mkDefault
was ruled out explicitly).

Also de-dup at the JSON-render site with lib.unique, so an agent that
names a base-set entry itself doesn't get it installed twice, and
reword the option doc, which still claimed the old REPLACES semantics.

Four module-eval cases cover the unset / agent-adds / mkForce-replaces
/ duplicate-entry shapes.

Refs #4467
2026-09-19 10:48:29 +02:00
atlas
ce3b3d9467 nix: drop the central-toggle conjunct from four compound gates
`services.hyperhive.enable` is going away (Refs #4500). These four blocks
ANDed it with a second condition that was already the load-bearing one:
the bridge-firewall hole asks whether an operator named any
`network.exposeHostPorts`, and the three swarm-level services ask their
own `deploy.*.enable`, each of which defaults false (swarm-ui derives
from the controller's toggle, a sibling deployment decision) and none of
which is derived from the hive toggle. So dropping the conjunct turns
nothing on by itself — the remaining condition still decides.

The way that claim fails is by something becoming unconditional, so
module-eval gets absence arms for all four, each asserted on a host with
the hive ON and on one with it OFF, plus the two controls that make the
absences mean something.

The swarm-controller arm is probed by the credential oneshot and by the
daemon's `ExecStart` rather than by the unit name: hive-tls defines an
environment key on that name, which leaves an inert fragment behind on
any hive with a CA whether or not the controller runs there.
2026-09-19 10:48:12 +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
c5f60fd58f nix: stop three defaults from consulting hyperhive.enable
hive-forge's publicUrl and behindGateway, and swarm-controller's
forgeTokenFile, no longer gate their default on
config.services.hyperhive.enable — none of the three has a reader that
depends on hyperhive being enabled to make sense of the value, so the
extra condition only added a hidden coupling. module-eval.nix gains a
centralToggleOff fixture plus four cases asserting each affected
default now resolves identically whether the toggle is on or off.

Refs #4500
2026-09-19 10:47:01 +02:00
atlas
837e658d4a swarm: courier an agent's store identity into its container, and log in with it
`swarm-controller` mints an agent's mTLS leaf at creation and publishes it
at `swarm/agents/<agent>/bao-mtls`. Nothing read it back. This adds the
hop that carries it the rest of the way, and the in-container consumer
that proves the hop works.

Host side, `lifecycle::agent_identity` reads the row under *this hive's*
own certificate — the hive is a principal the store already knows — and
stages the leaf and its key `0600` under a new `agent-identity/<name>`
state dir, deliberately outside every bind-mounted tree. Both files go in
as systemd credentials rather than binds, the same answer and the same
mode reason as the queue secret beside it: the staged key is unreadable
to the unprivileged agent user, and the container manager reads a
`--load-credential` source as root before re-exposing it under the
consuming unit's own `User=`. The agent is never asked to authenticate in
order to obtain the thing it authenticates with.

Container side, `hive-agent-bao-identity.service` logs in with that
certificate and reads the agent's own path back, failing the unit when
either step does not succeed. It fails loudly where the hive-side readers
degrade quietly, because a refused certificate means an agent that
believes it reaches the store and never does — a cause only the login
itself can name.

The address is the whole switch, no separate `enable`, matching how
`queue.nix` and `logs.nix` already gate themselves. A hive with a store
forwards `HIVE_AGENT_BAO_ADDR` and every agent on it gets the check; a
hive without one forwards nothing and no agent does. That is what keeps
the delivery from landing in a container with nothing to read it.

The hive can now reach an agent's identity, so hive privilege covers
agent privilege. Accepted, not mitigated: the alternative is an agent
fetching its own credential with a credential it does not yet have.

Refs #4137
2026-09-19 01:55:31 +02:00
iris
8aafe4eaee swarm-controller: trim oversized comments, drop a trivial wrapper fn
mara: "pls dont make comments longer than functions or fns that just
call a single other fn". Trimmed wanted.rs's module doc, apply()'s
doc, and declare_new_agent's doc down to what's non-obvious; deleted
WantedWriter::store (a one-line call to open_or_create with a
10-line doc comment above it) and inlined its body into its two
callers.
2026-09-18 22:59:29 +02:00
iris
c908a71f3e swarm-controller: drop the Intent split, one unconditional wanted-state write
mara: "i dont want any logic differene between the two cases" and
"do not refuse to recreate an agent". wanted.rs goes back to a
single write path (set), with no terminal-state refusal at all,
used identically by the pause/resume endpoint and the agent-creation
job node.

The agent-creation node's own idempotency requirement (re-running
create against a name that already has a declaration must not
silently pause it) now lives entirely in declare_new_agent: it reads
the current declaration first and only writes Paused when the agent
has no entry, or its entry is Destroyed (recreating a previously-
destroyed name is the fresh deploy that state's own doc comment
names as the way back).
2026-09-18 22:59:29 +02:00
iris
75af27abe0 swarm-controller: make agent-creation's wanted-state write idempotent
Intent::Create now no-ops when the agent already has a non-Destroyed
declaration, instead of unconditionally overwriting it to the
create path's state. Without this, re-running create against a name
that already has a wanted-state entry (an operator migrating a
pre-existing agent into this bookkeeping, or a retried request)
would silently pause an agent already running under some other
state. write() skips the network round-trip entirely when apply
returns the declaration unchanged.

mara: this does not match the expectation that agent creation is
idempotent so pre existing agents can be migrated
2026-09-18 22:59:29 +02:00
iris
48aa7a1e79 swarm-controller: let creating an agent reuse a destroyed name
Review caught a path the new declaration node breaks: an operator may
already declare an agent `Destroyed` over the per-agent state endpoint,
and `apply` then refuses any transition off that state. Before the
wanted-state node existed the refusal was inert at creation time, but
now creating an agent under a previously-destroyed name builds its
identity, repo and config, fails the declaration, and silently cancels
the deploy — while the caller sees a 200 and a job id.

`AgentState::Destroyed`'s own doc comment already sanctions this case
("no state that brings a destroyed agent back short of a fresh deploy");
nothing implemented it. Give `apply` an `Intent`, keep the refusal for
redeclares, and add `WantedWriter::create` for the one caller that is a
fresh deploy. A separate method rather than a parameter on `set`, so no
other caller can reach the override by passing an argument wrong.
2026-09-18 22:59:29 +02:00
iris
bfd019fe8d swarm-controller: declare a new agent paused at creation
Creating an agent queued its identity, forge repo and deploy, but never
wrote a wanted-state declaration for it — so the agent showed up in
swarm-ui as "no declaration", and the hive brought it up with nothing
saying whether it should be driving turns.

Add a `SetAgentWanted` job node that declares the agent `Paused` in its
hive's wanted-state bucket, using the same `WantedWriter::set` primitive
the per-agent state HTTP handler already uses. A fresh agent therefore
sits paused until the operator explicitly flips it to `Up`.

The node is a root — it needs only the hive and agent names known at
request time — but the deploy trigger now waits on it, so the pause is
in the store before the hive brings the container up rather than landing
some time after a freshly deployed agent has already started taking
turns.
2026-09-18 22:59:29 +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
83595663bb hive-forge: remove the nine renamed verbs that only throw
pr-status, pr-create, pr-commits, pr-merge, pr-reviews,
pr-assign-reviewer, issue-create, issue-edit, and diff existed only to
refuse via renamed() and name their namespaced replacement. Zero live
callers remain (docs/, /knowledge/, scripts/ checked), so drop the
enum variants, their dispatch arms, and the now-unused renamed()
helper in one change.

The kind-agnostic flat aliases (view, comment, comments, assign,
close, labels, timeline) are untouched: each has no single scoped
replacement, since it works on either an issue or a PR without
knowing which.

Refs #3974
2026-09-18 22:14:16 +02:00
iris
d9c8b7ca8f swarm-ui: floating turn-state/model/ctx/cost badges on the agent term preview
Header-bar parity for the swarm-level agent terminal, per mara's ruling
"dont make it a header though - make it floating elements on top of the
terminal". Adds useSwarmAgentStateStream (SSE consumer for
swarm-controller's GET /api/agents/{name}/state/stream) and renders a
small read-only Badge cluster absolutely positioned in
AgentTermPreview's terminal box corner, reusing the same anchor pattern
@hive/shared/terminal.css's .tail-pill already uses.

Shows turn_state always, agent_state only when it isn't the boring "up"
case, model (with resolved-model tooltip), and ctx/cost usage. No badge
renders until the first header event lands - the swarm queue's
agent-state subject is transition-only with no seed/replay, and no
swarm-level endpoint today can seed turn_state/model/ctx/cost
synchronously.

Vocabulary (state tones, token-total/format helpers) ported from
@hive/agent's Root.tsx/lib/format.ts rather than imported, matching the
existing "small helper, not worth a cross-package hop" call
useSwarmTermStream's own appendRow already made.

Verified by rendering the real component tree against a stubbed
EventSource and screenshotting the result - the badge cluster initially
overlapped the terminal's first row of text, fixed by giving this
preview's own .live.terminal extra top padding.
2026-09-18 17:30:59 +02:00