Commit graph

4,218 commits

Author SHA1 Message Date
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
iris
cebf3c6ced Drop classifyEvent.ts, render TermMsg directly
Per review: StreamRow was meant to match what the server sends in
TermMsg, not be a separate model needing a translation step.

- classifyEvent.ts and streamRow.ts deleted; termMsg.ts holds the wire
  types (TermMsg/TermEnvelope) plus TermRow, a TermMsg with just the
  key/fromHistory bookkeeping Preact needs for list rendering.
- Row.tsx renders a TermRow directly: level -> CSS class, empty
  summary + markdown body -> flat row, everything else with a body ->
  expandable details gated by the operator's preference. No separate
  classification step.
- useLiveStream.ts drops ClassifyCtx (a single incrementing key
  counter didn't need a whole context object) and maps envelopes to
  rows inline.
- docs/terminal-rendering.md trimmed substantially — was documenting
  more implementation detail than useful; points at stream_enrich.rs
  for the per-tool specifics instead of duplicating them in prose.
2026-08-30 21:23:28 +02:00
iris
5eefaa951d Simplify terminal message shape to a uniform TermMsg
Move terminal-row classification server-side into a new
hive-agent/src/term_msg.rs, replacing the old JSON-mutation
enrich()/stamped-field approach in stream_enrich.rs with one
uniform wire shape: {icon?, level: debug|info|warn|error, summary,
body?, body_format?: markdown|diff, coalesce_key?}. No more per-row
`kind` tag or raw claude-JSON passthrough — every row is the same
shape, with structural identity carried by icon + summary text
instead of a CSS class per row kind.

hive-agent/src/web_ui/stream.rs's history + SSE endpoints now both
call term_msg::classify() and serve TermEnvelope{ts, seq?, msgs}
frames; events that classify to zero rows (agent-state changes,
drop-noise) never reach the wire.

Frontend: classifyEvent.ts collapses from a large per-tool dispatch
tree to a thin TermMsg -> StreamRow adapter. streamRow.ts/Row.tsx
drop the now-dead meta/childText fields. terminal.css switches from
a dozen-odd per-row-kind classes to four level-based color rules.
Expand/collapse of a bodied row is now a uniform client-side
decision (the operator's preference), no server-side per-tool
override.

docs/terminal-rendering.md rewritten to match.
2026-08-30 21:11:25 +02:00
atlas
daa6eb96f8 deploy: move the hive CA's knobs to deploy.hive-controller.tls
`services.hyperhive.tls.{stateDir,caValidityDays,leafValidityDays}` sat at
the top of `services.hyperhive`, which is meant to be everything about
hyperhive rather than the settings of one hive. Where the hive CA lives,
how long it lasts and how long the leaves it signs last are decisions of
the host holding the key — `deploy.*`, by the same rule as the switches
that moved before them.

`hive-controller` is hive-c0re's new name (mara on the issue), so the
knobs hang off the daemon that owns the CA rather than off a bare `tls`
at the root. mkRenamedOptionModule entries carry existing configs.

⚠️ Unlike the two switch renames, these names are NOT unique, so this was
swept by ALIAS BINDING rather than by identifier: hive-tls.nix alone holds
two options spelled `stateDir` — its own `cfg.stateDir` and the swarm CA's
`swarmCaCfg.stateDir`, four sites that must not move. Nine files bind an
alias to this config; the rename followed those bindings.

Two sites were invisible to the obvious check, and an unanchored sweep for
`hyperhive\.tls\b` is what found them: the option declaration (`= {` after
the path, so no trailing `.` or `;`) and the alias convention documented in
a comment in lib/hive-ca-trust.nix.

Also renamed the `<tls.stateDir>` shorthand in four docs and two Rust doc
comments, anchored on its delimiters — the new path contains the old one
as a substring, so an unanchored replace would have doubled the prefix.
2026-08-30 20:52:00 +02:00
atlas
04f161a95b nix/docs: stop naming a specific remote builder in a comment
The narrowed-source comment justified itself with "the muede-pc2 remote
builder can reuse its cached result". mara, closing the issue I filed
about that builder being unreachable: "this is expected behavior …
depending on what network i am on different builders are available".

So the machine is not a fact about this repo. The reasoning holds for
whichever builder happens to be reachable, and a hostname in a comment is
the thing that rots — the same class as the forge address that has been
wrong twice.
2026-08-30 20:20:40 +02:00
iris
f14056996e terminal: catch the remaining stale ask/answer references
argus, reviewing PR#3793: a third stale  ask mention survived in the
same file/table the first pass touched (docs/terminal-rendering.md's
icon legend) — tool_icon() has no ask/answer arm at all, confirmed by
reading the function directly. Swept the whole tree this time instead
of trusting the earlier narrow grep: found three more —
docs/web-ui/dashboard.md's S3TT1NGS section still documented the
expand-tool-output toggle as living on the dashboard, which moved to
the per-agent page's own SettingsMenu popover in #3780 and was never
followed up here; markdown.ts/streamRow.ts/terminal.css had the same
send/ask/answer/recv phrasing as the original two comments.
2026-08-30 20:13:49 +02:00
iris
6233538f29 terminal: drop stale ask/answer references in comments and docs
mara, on #3791: "also ask/answer was removed, so any references are
stale and can be removed afaik" — confirmed: no code anywhere
references those tool names anymore (the rich-markdown classification
is generic, gated on _category/_body_type stamped server-side, not a
per-tool-name check), just two leftover comment/doc mentions.
2026-08-30 20:13:49 +02:00
atlas
4bd5c51e2e deploy: drop the per-option restatement of what allSwarmServices asserts
mara: "deploy.nix still says it everywhere". Seven option descriptions
each told the reader that this service derives from the swarm-services
switch — the same duplication just removed from docs/swarm/services.md,
one layer over, and rendered into the generated options doc.

Each keeps only what is true of that service and nothing else: the
metrics store is paired with grafana, the log store has clients rather
than second instances, the homeserver can be placed on its own host, a
swarm has one SSO provider. The store's paragraph about where it runs
being a separate question from that it runs survives, because that is
about the store, not about the switch.

swarm-ui keeps its positive derivation (the controller) and loses only
the "rather than from allSwarmServices" half.

Deliberately not touched, as a different question rather than an
oversight: the *mode* option's description in local-defaults.nix, whose
whole job is to name what it asserts; the module comments in
swarm-authelia.nix / swarm.nix, which are read by someone editing that
module rather than by an operator picking an option; and ui.md's
contrast, which she has not ruled on.
2026-08-30 20:12:16 +02:00
atlas
d7a471043b docs/swarm: state the swarm-services rule once, not per service
mara on the PR: "remove the re stating of the 'follows allSwarmServices'
from the docs everywhere. services md is the central place to explain
what 'all swarm services' means (without listing them)".

Four sections each restated that their service derives from the switch —
authelia, the metrics pair, the log store, the swarm collector. The rule
now lives once at the top of services.md, in the form that carries the
information the four copies did: every optional once-per-swarm service
takes its enable from it.

The intro's enumeration goes with them. Naming the four services in
prose is a hand-maintained list of the same kind, one that a fifth
service does not update.

That also makes an exception legible: swarm-ui's line saying it derives
from the controller rather than from this switch now reads as a real
difference instead of one paragraph disagreeing with four others.
2026-08-30 20:12:16 +02:00
atlas
97a7b518ea deploy: rename enableAllLocalDefaults to deploy.singleHostSwarm
Same defect as the switch below it, one tier up: it sat at the TOP of
`services.hyperhive`, a namespace that is meant to be everything about
hyperhive rather than the settings of a single hive. Whether this box is
the whole deployment is as per-host as a decision gets.

The name follows mara's sentence for what it means — "everything in the
swarm is running on this host" — rather than naming its mechanism.
"Defaults" was doing no work: it is not a defaults toggle, it is a claim
about where the swarm lives, and the pair now reads as the containment it
already was, singleHostSwarm implying allSwarmServices plus this hive.

One site was a setter rather than a reference: module-eval's `allLocal`
fixture passes an attrset merged into `services.hyperhive`, so its key
carries the path and had to become `deploy.singleHostSwarm`. A rename by
bare identifier is right for the twelve prose mentions and wrong for
exactly this one, which is worth knowing before the next rename.
2026-08-30 20:12:16 +02:00