Commit graph

4,227 commits

Author SHA1 Message Date
atlas
d6c8bcf5d3 module-eval: escape case names in the failure report
`echo 'FAILED: ${c.name}'` hand-quoted a string that is prose, so the
first case name containing an apostrophe to actually fail terminated the
builder script mid-report. The remaining failures never printed, and the
error read as a shell syntax error rather than as a broken property.

Only failing cases are ever echoed, so every green run agreed the
reporter was fine -- the defect was reachable exclusively at the moment
the report was the thing being relied on. Measured: with two cases
failing, the old form printed one line and an EOF error, the escaped
form prints both plus the count.
2026-08-31 19:54:56 +02:00
atlas
694abf4439 swarm-bao: make the reader's identity declarable, not just the store's
`swarm-bao.nix` declared the store's half of the mTLS pair as options —
`serverCertFile`, `serverKeyFile`, `clientCaFile` — and left the reader's
half as a literal inside `glue-bao-tls.nix`, which only runs where
`deploy.bao.enable` is set. A hive that did not host the store therefore
could not read from it and could not be pointed at a certificate even
when one had been placed by hand.

Adds `clientCertFile`, `clientKeyFile` and `serverCaFile` beside their
three server siblings, `mkDefault`ed by the glue to the leaf it already
mints, and moves `glue-matrix-bao-token.nix` onto them. Its gate becomes
"this host holds an identity" rather than "the store is a neighbour",
and the unit ordering that names store-local units is now conditional --
`Requires=` on an absent unit fails the job.

`serverCaFile` is separate from `clientCaFile` on purpose: one is the
store choosing which readers to trust, the other a reader choosing which
store to trust. Self-signing collapses them to one file, which is a
property of that deployment and not of the pairing.

Closes #3855.
2026-08-31 19:54:56 +02:00
iris
d9937ee834 dashboard: also remove the tombstone revive trigger
mara asked on PR #3854 to remove the revive action alongside the
blank-name spawn form. Both posted to /api/request-spawn as the last
two frontend callers; with this the dashboard has no UI path left that
hits that endpoint (backend removal/decision stays damocles's call).

Drops the K3PT ST4T3 tombstone row's ⊕ R3V1V3 form, leaving PURG3 as
the only per-tombstone action. Updated the two doc/comment spots that
described the now-gone button.
2026-08-31 19:35:45 +02:00
iris
8475d9921f dashboard: remove the spawn-new-agent trigger from the Y3R C4LL tab
Frontend half of removing hive-level agent spawning (swarm-level
creation now covers it, and its forge-works confirmation just landed
in hive/hive-chat). Removes call.js's blank-name spawn-request form
(R3QU3ST SP4WN) and its now-orphaned .spawnform CSS.

Deliberately keeps core.js's tombstone-list revive action (R3V1V3) --
a different feature (respawn an *existing* agent, reusing its kept
state), not agent creation, and not what this issue asked to remove.
It happens to post to the same /api/request-spawn action with the
tombstone's own name pre-filled rather than a blank input.

Backend endpoint + wiring is a separate slice (damocles).
2026-08-31 19:30:38 +02:00
iris
d900d25482 agent web UI: wrap the OAuth login URL instead of letting it overflow the card
claude's real OAuth URL is one long unbroken query-string token, no
whitespace for the browser to break on. Without overflow-wrap the
<a> just kept going past the login card's right edge instead of
wrapping at the card's bounded width (mara reported this with a
screenshot). overflow-wrap: anywhere lets it break mid-token as a
last resort, scoped to just the URL line (.login-url) rather than
the whole card.
2026-08-31 19:24:42 +02:00
iris
d49a9df479 useAgentState: re-arm the poll loop from refresh() instead of killing it
argus's review on the original fix: refresh() did a parallel one-off
fetch instead of calling the mount effect's self-rescheduling poll(),
so it never re-armed timerRef after firing. Wiring refresh() to run on
every visibilitychange-to-visible meant the very first tab-switch back
into focus would kill periodic polling for the rest of the session --
reproducing the exact staleness bug this branch set out to fix, just
delayed by one tab switch instead of immediate.

Fix: hoist poll into a ref set by the mount effect so refresh() invokes
the same self-rescheduling function rather than a parallel fetch that
drops the loop. Also gave the visibilitychange effect an empty
dependency array per the review's second note -- it only closes over
stable refs/setters, and now that Root's 1s ticker re-renders the
calling component every second, a deps-less effect would tear down and
reattach the listener that often for no reason.
2026-08-31 19:02:57 +02:00
iris
9c72a4ae3e agent web UI: keep the turn-state badge live instead of freezing until reload
The status badge's elapsed-time text ('thinking Xm Ys') was computed
from Date.now() inside the render function, so it only ever advanced
when the component actually re-rendered. Two gaps let it go stale:

- useAgentState's poll loop is a chained setTimeout, which browsers
  throttle (or suspend outright) once the tab is backgrounded, so
  polling could stall for a long time with no way back to a live
  reading short of a full page reload.
- even under healthy polling, the age text only advanced once every
  ~4s (the poll interval) instead of counting up smoothly.

Fix: resync immediately on visibilitychange (so returning to a
backgrounded tab doesn't leave a stale reading), and drive the age
text off its own 1s interval independent of the poll cadence.
2026-08-31 19:02:57 +02:00
iris
6e6bf62437 IssueReportPage: unmount guard on the repos fetch, aria-sort on sortable headers
Two non-blocking notes from review:
- the /api/repos effect now uses the same cancelled guard the
  repo-filter effect already has, so an unmount mid-flight doesn't call
  setRepos/setError on a gone component.
- Table's TableColumn gains an optional ariaSort field, consumed as the
  <th>'s aria-sort attribute; the issue-report page's sortable columns
  now report ascending/descending/none so a screen reader can announce
  which column and direction is active, not just the sighted ▲/▼ glyph.
2026-08-31 19:02:14 +02:00
iris
f4b61db753 IssueReportPage: match the corrected row shape, assignees is a list
damocles caught it on the swarm-controller PR: forge's assignee field
is legacy single-value, assignees is the real multi-assignee list, and
this repo actually uses multiple. Widen the frontend's row type +
column to match -- rendered comma-joined the same way labels already
are.
2026-08-31 19:02:14 +02:00
iris
e815c7cb5c swarm-ui: add the custom issue-report page
New /issues route: a sortable, filterable table over open issues across
every repo that has one -- repo picker (default: no filter, every repo
combined), hide-blocked toggle, and a label multi-select, consuming
swarm-controller's GET /api/repos + GET /api/issue-report / GET
/api/repos/{org}/{repo}/issue-report (see hyperhive#3831 for the row
shape). blocked and depended_on_by_count arrive pre-resolved per row --
this page does no dependency-graph walking of its own, just sort/filter
over what it's given. Default sort is depended_on_by_count descending,
matching mara's framing of the report's headline ordering.

Widened Table's TableColumn.header from string to ComponentChildren so
a column can carry a real clickable sort-toggle button instead of
forking a second table primitive for this one page.
2026-08-31 19:02:14 +02:00
atlas
5aef2d1afc swarm: remove swarm.ui.domain, serve the UI on the swarm domain
The UI and the swarm-controller it fronts are one service to a reader and to a
certificate, so a separate option only ever created two spellings of one
address — and the controller's own public URL was built from one of them while
an allow-list entry named the other.

Removed rather than aliased. A config still setting it was pinning a name the
controller never used; an alias would preserve exactly that mismatch, so
`mkRemovedOptionModule` says so and fails the build.

⚠️ The option was TOTAL and `swarm.domain` is NULLABLE. That is not incidental:
`ui.domain` fell back to `swarm.invalid` so hive-network.nix's required-domain
assertion is what an operator sees, rather than a coercion error from a
consumer. Each site keeps that property its own way — swarm-ui binds a local
total (the idiom swarm-otel.nix already uses as `domainBase`), and the three
sites already gated on `deploy.swarm-ui.enable` gain `&& domain != null`, which
is stricter than a placeholder and puts no invented name into rendered config.

One behaviour change worth naming: `swarm.nix`'s certificate SAN list used to
receive the placeholder `swarm.invalid` on a null domain and now receives
nothing. `lib.optional` would otherwise have put a literal `null` in it.

Verified with `checks.module-eval` rather than `nix-instantiate --parse`: parse
proves syntax, and every failure this change could plausibly cause — a dead
binding, a renamed reference — is an evaluation error.
2026-08-31 19:00:52 +02:00
damocles
3f98bfb09d swarm: seed power intent to Up on first-deploy so the container actually starts 2026-08-31 18:57:18 +02:00
damocles
c48c8b1991 swarm-controller: fix broken intra-doc link in issue_report.rs 2026-08-31 18:50:38 +02:00
damocles
586f7f68c2 swarm-controller: stop narrating pr review history in doc comments 2026-08-31 18:50:38 +02:00
damocles
cc8e48a69b swarm-controller: report all assignees, not just one 2026-08-31 18:50:38 +02:00
damocles
957291e3a7 swarm-controller: split issue-report handlers into their own module 2026-08-31 18:50:38 +02:00
damocles
f1b729e6b5 swarm-controller: add a forge issue-report data source for swarm-ui 2026-08-31 18:50:38 +02:00
atlas
7172176b4c swarm: extract the name guards, so the module just says what is forbidden
Her review: too much text, and an assertion helper instead of that much code
where the file should just say "this option cannot contain that".

`lib/name-guards.nix` gets `mustNotEqual` / `mustNotContain`; both call sites in
swarm-otel become four fields each — the option, the names, the list, and the
sentence explaining the failure. The message plumbing (find every offender,
quote them, name the list) is one shape shared by both.

Offenders are printed after their label rather than before it, because no
English verb agrees with both one name and five.

Converted the pre-existing equality assertion too. Leaving one hand-rolled
beside one helper-built is the worse of the two outcomes, and it is the same
guard.

Verified by evaluating both guards directly rather than only parsing: clean
rosters pass, `foo-agent`/`beehive` fail containment, `swarm` fails equality,
and `forgeworks`/`operator-hq` still pass — the control that keeps the two
lists from being merged.
2026-08-31 18:50:15 +02:00
atlas
cdaf5824ed swarm: refuse hive names that CONTAIN a word identifiers are composed from
Hive-scoped identifiers are built by joining a hive name with fixed words —
`hive-<name>`, `hive-<name>-agent` — so a hive called `foo-agent` produces
exactly the id hive `foo`'s agent containers produce. The queue's responder
resolves it as the agents, and that hive silently receives an agent grant
instead of its own; a NATS denial reaches a client as a timeout, so nothing
names the cause. Equality cannot see this: the two NAMES differ, only the
composed strings collide.

Deliberately a second file rather than three more entries in
`reserved-names.nix`. That list is matched by equality against a whole name and
every entry is a value some component emits; applying containment to it would
refuse `forgeworks` and `operator-hq` for failures that do not exist. The two
lists differ in subject (hive-only vs both), matcher (substring vs equality)
and admission rule, and each file's header says so.

No Rust change: `HIVE_RESERVED_NAMES` answers "may an AGENT be called this",
which stays an equality question — an agent name is never composed into these
identifiers.

Measured against the predicate rather than assumed:

  pr1ma=ok alpha=ok forgeworks=ok operator-hq=ok
  foo-agent=REFUSED beehive=REFUSED swarm=REFUSED myswarmx=REFUSED

The four `ok` cases are the control: `forgeworks` and `operator-hq` are exactly
what a merged list would have refused.
2026-08-31 18:50:15 +02:00
atlas
dbda6fbde1 hive-forge: cut the webhook allow-list comment to what is not obvious
27 lines to 11. What went: the measured deny line (it is in the PR and the
commit that made the change), the "one value, two readers" argument for reading
the option the target URL is built from, and a note about `unique`.

What stayed is the part nobody can re-derive from the code: `external` must not
be dropped, both hyperhive hosts are private addresses the builtin denies, and
the entry is deliberately not gated on the per-host `deploy.swarm-ui.enable`
because a spread deployment has the forge and the swarm host on different
machines.
2026-08-31 18:31:47 +02:00
atlas
a44107c617 hive-forge: inline the webhook-host binding, used once
Her review: don't declare a `let` for a single use. The comment that justified
the binding moves to the use site and drops the claim it was carrying — that
`swarm.ui.domain` and `swarm.domain` can diverge — keeping only the part that
holds either way: this reads the same option swarm-controller builds its public
URL from, so the allow-list entry and the target it must match are one value
rather than two spellings.
2026-08-31 18:26:34 +02:00
atlas
b9919164e4 hive-forge: read the webhook host from the option the target is built from
`swarm.domain` is not the value swarm-controller addresses this hive with —
`swarm-controller.nix` builds `SWARM_CONTROLLER_PUBLIC_URL` from
`swarm.ui.domain`, which merely defaults to it and is documented as
overridable. Allowing the one and being delivered the other would work until
someone takes that option up, then fail as a refused delivery with nothing
naming the cause.

Also states why the entry is NOT gated on `deploy.swarm-ui.enable`, which is
what makes the controller register the hooks at all: in a spread deployment the
forge and the swarm UI are different hosts, so that per-host flag is false
exactly where the allow-list entry is needed.

The comment-block lint caught the prose at 35 lines; trimmed to the parts a
reader cannot re-derive.
2026-08-31 18:19:39 +02:00
atlas
584c99ce13 hive-forge: allow the swarm domain through forgejo's webhook SSRF list
`webhook.ALLOWED_HOST_LIST` named `external` plus the HIVE domain. The
swarm-controller's webhooks target the SWARM domain, which is a different host
on the same private gateway IP — so `external` denied it and the hive entry did
not cover it. Every swarm-controller delivery has been failing there.

Found from the sender's log, which names the host and the rule it broke:

  deny 'constellation.darkest.space(10.42.0.1:443)'
  webhook can only call allowed HTTP servers (check your
  webhook.ALLOWED_HOST_LIST setting)

This is the layer under the DNS fix: resolution now succeeds and reaches
10.42.0.1:443, and forgejo refuses to dial it. The two failures look identical
from the receiving end — no delivery, no log line — which is why reading the
sender was what separated them.

`optional` because a hive with no swarm has no such domain; `unique` because an
all-local deployment can legitimately set both to the same string.
2026-08-31 18:15:49 +02:00
atlas
ff7897f9bc CLAUDE.md: the mistyped label separator errors now, it does not pass
This warned that `--label area:ops` "matches nothing and silently returns
unfiltered results". That stopped being true when hive-forge learned to suggest
the nearest name: it now exits non-zero with `unresolved label name(s): area:ops
(did you mean "area/ops"?)` and lists the available labels.

Measured, not assumed — the old text is what sent me looking for a silent
failure that no longer exists, which is the specific cost of a doc that
describes a fixed bug.
2026-08-31 18:13:33 +02:00
atlas
196805bfc0 swarm-controller: retry webhook registration instead of waiting for a restart
Registration ran once at startup and, on failure, deferred to the next
process start. Nothing schedules one, so a controller that lost the boot
race kept running with no hooks registered — and the failure is silent at
both ends, since the forge has nothing to report about a call that never
arrived.

That race is the common case rather than an edge: the controller and the
forge come up together on a rebuild. Measured on a deploy where both
consecutive starts got 502 from the gateway because forgejo was not yet
serving; the forge was healthy two minutes later.

Bounded backoff, not a poll loop — it exists to outlast a slow forge, not
to re-register periodically. Unit ordering would not fix this: the forge
is a remote host in a spread deployment, where no After= can reach it.

Closes #3828
2026-08-31 15:48:42 +02:00
iris
b33bb7a3d3 Move dashboard settings into Y3R C4LL tab, drop standalone S3TT1NGS page
mara (issue #3817): remove the home link + page, put the setting toggle into the Y3R C4LL tab. Deleted settings.html/.js/.css; the browser-notification toggle (the only content there) now renders as a ◆ PR3F3R3NC3S ◆ section under Y3R C4LL's approvals/inbox, wired the same way (NOTIF.bind()/NOTIF.show() in common.js — no behavior change, just a new mount point). Updated build.mjs's entry lists and every doc/comment that pointed at the old page.
2026-08-31 12:57:31 +02:00
iris
146db58bab docs: trim M4TR1X-page LinksMenu paragraph to the pointer, not the impl
mara: strip impl details from docs. Dropped the nix-module name, struct literal, and gating-condition restatement — docs/swarm/ui.md::Quick links already covers that; this section only needs to say where the link lives now.
2026-08-31 12:55:40 +02:00
iris
19ed400cdd docs: M4TR1X page is reached via swarm-ui's LinksMenu, not opened directly
mara (PR#3820): the operator doesn't type /matrix/ into the browser — it's a swarm-level link (hive-matrix.nix registers it into services.hyperhive.swarm.controller.links, rendered by swarm-ui's LinksMenu). Corrected the M4TR1X page doc, and fixed the hive-matrix.nix comment that still described the removed H0M3 tile in present tense while I was in there.
2026-08-31 12:55:40 +02:00
iris
40be8222b8 docs: fix stale Matrix-tile reference in M4TR1X page section
argus (PR#3820 review): the M4TR1X page section still said the operator opens /matrix/ from the H0M3 hub's Matrix tile — that tile no longer exists after this PR removed it. Fixed to say the page is opened directly.
2026-08-31 12:55:40 +02:00
iris
2490181055 H0M3: remove Forge/Matrix tiles from hive home
mara (issue #3816): remove the swarm-service links (forge, matrix) from the hive home hub. Both surfaces are still reachable directly (forge's own public URL, /matrix/); they just no longer get a tile on H0M3. Dropped the now-dead reveal/href-fill logic in home.js and updated the two docs that described the old gating.
2026-08-31 12:55:40 +02:00
damocles
6ccd634afb raise mcp streamable-http session keepalive from 5m to 24h 2026-08-31 12:53:07 +02:00
atlas
d6f3af60ae hive-forge: resolve through the hive's dnsmasq
The forge container used the host's resolvers, where the swarm domain has
no records — so every outbound call to a swarm name failed with "no such
host". Forgejo's webhook deliveries died there: knowledge, config-pr and
vcs-activity alike, which is why the swarm-controller's receiver has never
logged a verified delivery and knowledge propagation has been running on
its hourly fallback.

An earlier fix taught the container authelia's name with a `networking.hosts`
entry. That covered one name and left the rest, so use the resolver every
other service container already uses and drop the override — hive-matrix
records why a second answer that can disagree with the first is worse than
none.

Closes #3824
2026-08-31 12:52:30 +02:00
atlas
36082f991f otel: ship the journals the dashboard can already show
The dashboard's log surface is an allow-list: the four harness units in
every agent container, the hive-ci / hive-forge / hive-matrix machines,
and three host units. `swarm.otel.journaldUnits` only named the host
three plus forgejo — so the units an operator can read in the UI were
absent from the store they are supposed to be queryable in, and nothing
reports that, because a name that matches nothing is not an error.

Adds the four agent units, the CI runner and the homeserver. The agent
units are declared by the hive-c0re module rather than by the
agent-modules that define them: those are a guest evaluation and cannot
reach a host option.

Closes #3818
2026-08-31 12:51:51 +02:00
atlas
2b35ffd250 swarm-controller: mark the instance webhook as a system webhook
`POST /admin/hooks` reads `is_system_webhook` out of the config map and
defaults it to false, which creates a forgejo *default* webhook — a
template copied into repos created later — instead of a live
instance-wide one. `GET /admin/hooks` returns only hooks with the flag
set, so `list_hook_urls` could never see what the create had just made:
every controller start listed zero hooks and created another default
webhook (12 in 26h), while the instance-wide push observation the scope
exists for never fired at all.

Send the key on the `Instance` arm only, via a per-scope
`extra_create_config()` so repo and org scopes stay unchanged.

Closes #3807
2026-08-31 10:56:08 +02:00
atlas
3939303287 docs/swarm/secrets: the store's contents are no longer a host-level path
The table still described the raft data as living at /var/lib/swarm-bao on the
host, bind-mounted in -- the design the previous commit removes. An operator
following that row would back up a directory that no longer holds the store,
and would not know that a container destroy now takes the data with it.

States what is still host-level, since that is the part the row was actually
useful for: the TLS material and the PKCS11 token, both of which a host unit
writes and the container only reads.
2026-08-31 00:33:37 +02:00
atlas
9f3f01450b swarm-bao: keep the raft state in the container, bind only the TLS material
The state directory was bind-mounted from the host so a nixos-container
destroy could not take the swarm's secrets with it. No sibling service does
that -- swarm-grafana keeps its sqlite database inside the container on
ephemeral = false -- and the bind is what broke the store: upstream pairs
StateDirectory= with DynamicUser=, systemd relocates the state to
/var/lib/private/openbao, and that rename fails EBUSY on an active mount
point, so the unit died at STATE_DIRECTORY before bao ever ran.

The TLS material still has to cross the boundary, because a host unit writes
it and the container reads it, so it moves to its own small bind at
/var/lib/swarm-bao-tls rather than riding along in the state directory. That
directory is 0755 and read-only inside: the certificate and client CA are
public and are read straight off the mount.

The private key is not. install -m 0600 leaves it root-owned and the service
runs as a DynamicUser, so the bind-mounted file is unreadable to it -- which
the old layout hid, because StateDirectory chowned the whole tree on the way
past. LoadCredential is systemd's mechanism for precisely this: PID 1 opens
the source as root and re-exposes it inside the unit owned by the service's
own account.
2026-08-31 00:33:37 +02:00
damocles
22adfd1451 remove the 1NFR4 dashboard panel and the now-writer-less audit log 2026-08-31 00:18:21 +02:00
damocles
c3cd36bc41 drop stale agent-restart comparison from 1NFR4 tooltip 2026-08-31 00:18:21 +02:00
damocles
7516a4e10e remove hive-level infra-container restart from web ui and agents 2026-08-31 00:18:21 +02:00
atlas
ccf9951e5d hive-c0re: seed an agent's proposed config from the forge when it exists
The swarm writes agent-configs/<agent> when it creates an agent, before
any hive is told to deploy it. setup_proposed authored a second copy of
those same bytes locally, so an agent's initial config had two sources
of truth, each unaware of the other and free to disagree. It now clones
that repo and falls back to the template only when there is nothing
there to take.

Preferred-source rather than a new-path-only variant because
provision_container is the Provision node for the swarm deploy and the
approval flow both, and cannot tell them apart. The approval flow
creates agent-configs/<agent> only after the first spawn
(forge_after_first_spawn), so it finds nothing and lands on the
template: the fallback becomes unreachable when hive-level create is
removed, rather than becoming something someone has to find and delete.

clone, not the neighbouring init+fetch. A failed fetch leaves an empty
.git behind, and that .git is exactly the byte setup_proposed reads to
decide whether seeding is still needed, so the fallback would have seen
a seeded repo. git removes a directory it created when a clone fails.
--branch main also makes an empty repo fail cleanly instead of cloning
to an unborn HEAD that would look seeded.
2026-08-31 00:17:41 +02:00
atlas
bfae9aa51a hive-c0re: a swarm deploy for an unknown agent provisions it
Per mara on the PR: the issue is about a *new* agent, there is no
approval because the operator clicked create at swarm level, and most of
what a hive does on create is already done by the controller.

`spawn_nodes` splits out of `spawn` the way `rebuild_nodes` already
splits out of `rebuild`: two callers want the same four nodes and
disagree only about what closes them. `first_deploy` is that subgraph
with no approval tail, and the absence is the point — that tail exists
because an operator used to approve the spawn at the hive, and asking
again after they clicked create at swarm level asks the same person the
same question twice.

The handler's predicate is "does a container exist", not "is one
running". `agents_for_meta_listing` is `nixos-container list`, so a
stopped agent still counts. `Coordinator::list_agents` looks like the
right check and is the registered-MCP-socket set — a stopped agent is
absent from it, and this would then try to create over an existing
container.

Enumeration failure drops the request rather than guessing: without the
list this cannot tell first deploy from rebuild, and guessing "new" is
the destructive direction.

Still missing, and the reason this is not the whole change: the hive
seeds its own config repo with `git init` instead of cloning the one the
controller already created.
2026-08-31 00:17:41 +02:00
atlas
b004ba3dc5 swarm: split the deploy subject per hive
Per mara on the PR: *"split by hive. its not a security thing, just so
hives dont get messages they dont care about."*

She agreed with the finding and still wanted the split, which is the
part worth recording. I measured that a per-hive subject gives no
confidentiality — `sub` is unrestricted, so a hive that wanted another's
messages could subscribe to them — and concluded it bought nothing.
"Nothing" is a claim over every axis and I had checked one. The axis I
never priced: every hive in the swarm being woken by every other hive's
deploys.

So `deploy_subject(hive)` replaces the single literal, and the payload
drops `hive` to carry only the agent — the subject names the hive, and
two places stating one fact are free to disagree. The hive subscribes to
its own subject and no longer filters.

The grant is a wildcard rather than a subject per hive because the
responder has no roster: it cannot enumerate hives, and a grant that had
to track one would be a second place to get the list wrong — the same
argument `hive_name`'s doc makes about admission.

The negative test gets stronger rather than merely adapted. Splitting
the family makes "another hive's subject" and "its own" separate strings
for the first time, so it now asserts a hive reaches neither, nor the
wildcard.
2026-08-31 00:17:41 +02:00
atlas
7519d9b904 hive-c0re: act on a deploy request addressed to this hive
The second subject on the connection this task already drains. Every
hive receives every message — that is what a swarm-wide subject buys —
so not being the addressee is the ordinary case and logs at `debug`. A
payload that will not decode is not: both ends share one type, so a
decode failure means they disagree about it.

The rebuild is the same insert the operator's own `rebuild` verb makes,
relock and all. "Deploy this agent" already means something here, and a
swarm-triggered rebuild that quietly did something narrower would be a
second definition of the word.

⚠️ The at-most-once argument in this function's docs does NOT transfer
to the new subject, and the docs now say so. A missed knowledge event is
repaired by the pull this daemon does at startup regardless; a missed
deploy event has no second path — nothing else would ever tell this hive
to build that agent. Closing that is the hive-side reconcile loop the
issue's other half calls for; until it exists this is a nudge with no
safety net. Not papered over with `JetStream` here: durability on one
subject would look like a fix while the desired state still lived only
in a message.

Swept the surrounding prose rather than only the lines I touched. Two
sections had gone quietly false: the summary said this listens on "the
knowledge-event subject" and named one event, and a whole section
argued "there is no payload, and that is deliberate" — true of the
knowledge event and the opposite of true for a deploy request, which is
addressed and has nowhere but the body to say so.
2026-08-31 00:17:41 +02:00
atlas
d4eb62434d swarm-controller: creating an agent now asks its hive to deploy it
`TriggerDeploy` had no producer — a node kind nothing enqueues is dead
code, and a publisher with no caller proves as little as a check nobody
runs.

It goes last in the creation chain, after `InitAgentConfigRepo` rather
than merely after the repo exists: the hive deploys by reading that
repo, so a deploy asked for any earlier would find nothing to build.
That edge is what makes creating an agent at swarm level actually put it
on a hive instead of leaving a provisioned name nobody runs.

The `hive` it carries is the string this handler already parsed as an
`Ident` and matched against the roster, so the node cannot name a hive
this swarm does not have.
2026-08-31 00:17:41 +02:00
atlas
38ccef987e swarm-controller: a node kind that asks a hive to deploy an agent
`TriggerDeploy` is the first `SwarmNodeKind` whose effect leaves this
host, so it is also the first to need the queue connection: `WorkerDeps`
grows a `queue` handle, cloned from the status reader whose own doc says
the connection living there is an accident of construction order rather
than a claim that events are a kind of status.

`publish_deploy` publishes and then flushes before reporting `Done`.
`publish` only hands the message to the client's write buffer, so a node
that reported success on that alone would be claiming a delivery it has
no evidence for — the ordering `webhook::announce_knowledge_change`
already documents.

The `WireNode::data` or-pattern did the job it was written for: its
comment says a new variant should fail to compile there rather than
silently render as an agent name, and `TriggerDeploy` is the first node
about an agent *and a hive*. A catch-all would have dropped the hive
from the viewer with nothing to notice it.
2026-08-31 00:17:41 +02:00
atlas
93c7454bf5 swarm: name the deploy event and grant the controller its publish
The subject and its payload live in `swarm-queue-client` for the reason
the knowledge event's already does: three crates have to agree on the
string, and the one that agrees hardest — the auth-callout responder,
which decides whether the publish is permitted at all — speaks neither
`jetstream` nor `kv`.

Swarm-wide rather than a `$SWARM.deploy.<hive>` family. That family
would look like isolation and provide none: this responder scopes
publish only, leaving `sub` unrestricted, so a hive could subscribe to
another's subject as easily as to its own. Until `sub` is scoped the
split costs a wider grant and buys nothing, so the addressing goes in
the payload and each hive filters on its own name.

Unlike the knowledge event the message is addressed, so it carries a
payload — a trigger, never the config. The hive already tracks the
agent's config repo; desired state on the wire would make this a second
source of truth for something git owns, and a hive that missed a
message would be wrong rather than late.

Both test arms mirrored from the knowledge event. The negative one
matters more here: a forged knowledge event makes a hive re-read a
repo, a forged deploy event makes it rebuild and restart a named agent.
2026-08-31 00:17:41 +02:00
damocles
5f197a38ee strip ansi escapes from systemd-managed daemon logs 2026-08-30 23:23:00 +02:00
atlas
b9d315dcd9 swarm-controller: separate the two paths that re-register a webhook
`ensure_hook` reaches its create call from two different places — the
list step failed, or it succeeded and matched nothing — and both were
logged at `debug!`. At the level the journal keeps, that made a repeated
registration indistinguishable from a first, correct one, and it is a
repeated registration that is being observed: the instance-scoped hook
logs the create arm on every process start while the repo-scoped one
correctly goes quiet.

The fold that keeps this harmless (`is_already_exists` swallowing a
duplicate create) is an assumption about the forge rather than a
guarantee, so the failed-list arm becomes a `warn!`, and the
matched-nothing arm logs the number of hooks it did see: `listed=0` is a
permission or scope problem, a non-zero count with no match means the
recorded url is not the one being compared.

No behaviour change — this makes the existing behaviour legible.
2026-08-30 22:24:50 +02:00
iris
5930efc29b Trim negative-space comments per mara's review
Don't state what a function/module doesn't do and where that
happens instead — just describe what it does. Cut the "not
something this function decides" / "not affected by this" /
"not a placeholder for a later commit" asides from the doc
comments touched in the last two commits.
2026-08-30 21:31:33 +02:00
iris
907567ef76 Fix stale default-open doc comments per argus's review
ClassifyCtx's tool_use-id correlation gates markdown-vs-plain body
format for a recv result, not open/collapsed state — that's always
the operator's uniform preference now. 5 backend comments still
described it as controlling "default-open" rendering, contradicting
the actual render path and this PR's own rewritten docs.

Also fixed useAgentState.ts's stale comment promising an
SSE-triggered refresh model "in a later commit" — that's permanently
off the table now that the terminal stream's kind tag is gone by
design (argus flagged this as a drive-by, not blocking, but it's a
one-line cause-and-effect of this same PR so fixing it here).
2026-08-30 21:28:34 +02:00