Expanded the /screen endpoint description in docs/web-ui.md to
cover the substantive design rationale that lived in screen.html
comments. screen.html shrinks correspondingly.
Moved to docs/web-ui.md::Per-agent endpoints (GET /screen):
- Deliberate thinness — minimal RFB renderer; noVNC vendoring
path called out for production-grade replacement.
- Fit-mode flex-item min-width:auto clamp: a flex item's automatic
minimum size resolves to the canvas's intrinsic framebuffer
resolution and clamps CSS max-* back up, making fit mode a
silent no-op. The fix (flex: none + min-width: 0 + min-height: 0
+ explicit px sizing via relayoutCanvas()) is now documented.
- localStorage persistence for fit-mode (key screen-fit; default
on).
- Pointer rescale in sendPointer keeps clicks accurate.
- ExtendedDesktopSize pseudo-encoding (-308 rect) gates the
match-size button.
Collapsed in screen.html: 8 #NNN cookies scrubbed across all
inline comments. #133 (canvas-sizing fit bug — closed) ×5,
#52 (noVNC vendoring — closed) ×1, #14 (relative URL — closed
my piece) ×1, plus one CSS-block #133. Each comment shrinks to
a brief pointer.
screen.html: 8 → 0 #NNN cookies (100% reduction). Net ~26 lines
of substantive prose moved into docs/web-ui.md.
mara: 'if we replace it with one thing, that should be named more
generic so we dont have to change it for future additions'.
extract the BASH_ENV plumbing into a shared shape:
- new internal option `hyperhive._bashEnvFragments` (types.lines)
accumulates shell snippets across feature modules.
- file path is now `/etc/hyperhive/bash-env.sh` (was the
cargo-specific bash-cargo-short.sh).
- the file + BASH_ENV + interactiveShellInit are gated on
`_bashEnvFragments != """ so a fully feature-disabled agent has
no overhead.
cargo function moves to a `lib.mkIf cargo.shortMessages` contribution
to `_bashEnvFragments` — same behaviour, no rename when the next
hook (nix-env helper, claude-cmd helpers, whatever) lands.
closes#777. saves tokens by collapsing per-crate progress lines into
warning/error summaries when claude (or the operator) runs cargo
inside an agent container.
implementation: /etc/hyperhive/bash-cargo-short.sh defines a 'cargo'
bash function that injects '--message-format short' on compile
subcommands (build/check/clippy/test/run/doc/bench/install/rustc/fix).
loaded via BASH_ENV in non-interactive shells (claude's Bash tool
runs 'bash -c') and via programs.bash.interactiveShellInit in
interactive shells (operator SSH inside the container).
handles the '+toolchain' selector (cargo +nightly build), skips
injection when the caller already passes --message-format (any
form), leaves third-party cargo-* subcommands alone.
new option: hyperhive.cargo.shortMessages (default true) — agents
that parse cargo json output should set false.
iris's #718 scope: move substantive design context from `#` comment
blocks in `nix/` to corresponding `docs/` files, leave short
references in code. iris handed it back to me on #10114 since
nix/ is my lane + #775 established the pattern.
First pass — three highest-density blocks in harness-base.nix:
1. **First-boot agent-user migration** (~70 lines → `~20 lines code +
short ref` in the activation script). Substantive prose moves to
new `docs/persistence.md::First-boot agent-user migration (post-#658)`
section explaining the 4 steps the script performs + the eventual
removability of the marker-guarded body.
2. **nix-daemon `sandbox-fallback = true`** (10-line block → 5-line
ref). New `docs/gotchas.md::Containerized nix-daemon needs
sandbox-fallback = true` section covers the user-namespaces
rationale + nixpkgs-default override.
3. **Matrix daemon + token-arrival trigger** (~50 lines across two
systemd units → ~10 lines code + short refs). New
`docs/persistence.md::Matrix per-agent daemon + token-arrival
trigger` covers the socket-path rationale, the runtime-dir
ownership story, and the first-boot ordering pattern.
Net: harness-base.nix -84 lines, docs +74 lines. Substantive design
context moves to durable docs; in-code refs follow iris's pattern
from her #712 batches (`see docs/<file>::<section>`).
Follow-ups: hive-c0re.nix, hive-forge.nix, hive-matrix.nix (already
trimmed via #775 but a couple of remaining blocks could go), and
the smaller files in #718's scope table. Shipping this first to get
the pattern reviewed before larger batches.
Verified: `nix eval` on agent-base toplevel still resolves.
Moves the #666 ask→operator inline-answer wiring rationale from
app.js into a new docs/web-ui.md::Per-agent page sub-paragraph
**Ask → operator inline-answer binding**. Substantial block —
the slot-registry / reconcileAskBinds / buildAnswerForm trio has
real design rationale (async question id, text-match pairing,
resolved-vs-cancelled-vs-expired neutrality, defensive prune,
no-regression-fallback to side-panel answer) that belongs in
docs, not three JSDoc blocks scattered through app.js.
Moved to docs (~30 lines of substantive prose):
- pendingAskBinds slot-registry rationale
- reconcileAskBinds matching algorithm (text-match, first-unbound
to first-unclaimed pairing stability)
- defensive prune for disconnected slots
- [resolved] vs ✓ rationale (answered / cancelled / TTL-expired
ambiguity — neutral label)
- fallback to side-panel answer when slot stays unbound
Collapsed in app.js: 3 JSDoc blocks + inline comments → ~10-line
total pointer. Drops #666 ×3, #559, #668 cookies en passant
(substance now lives in docs).
app.js: 24 → 19 refs in this commit; 27 → 19 across batch 1
(30% reduction so far). Net ~57 lines of substantive prose
migrated from app.js to docs/web-ui.md across both commits in
this PR.
First batch of #713 (agent per-container UI prose migration).
Moves 3 substantive WHY-explanations from app.js into
docs/web-ui.md::Per-agent page, replaces each in-code mention
with a brief pointer.
Moved to docs:
- **Terminal-wrap pill anchor** (~11 lines): why the `↓ N new`
pill is anchored in `.agent-main` rather than the default
`.terminal-wrap` parent — backdrop-filter creates a stacking
context, anchoring inside it traps the pill's z-index below the
composer.
- **OAuth code input** (~14 lines across two blocks): masked
password + reveal toggle + `autocomplete="one-time-code"`
rationale (shoulder-surfer / screenshot exposure + WHATWG
semantic value + suppresses browser save-password prompt).
- **NavLink kind resolution + XSS-safe DOM-build** (~8 lines):
Container → same-origin, Forge → `http://<host>:3000<url>`,
External → already absolute; agent-declared strings never reach
innerHTML.
Collapsed in app.js: each block becomes a 3-4 line pointer to the
matching docs section. Drops #262 / #568 / #592 cookies en passant
since their substance now lives in docs. The `#14` cookie on
`historyUrl` / `streamUrl` is also scrubbed — the path-relative
shape is the convention, not an open issue. `#375` (agent.css ID
ref) drops as part of the pill-anchor block collapse since it was
sole-purpose pointing inside that comment.
app.js: 27 → 24 refs. Net ~35 lines of substantive prose moved out
of app.js into docs/web-ui.md::Per-agent page, where it belongs.
argus on #775 v3: "the `gatewayHost` option description's
server_name-vs-gatewayHost essay + federation SRV note are also
candidates for [docs/gateway.md] section."
Cuts the gatewayHost option's description from ~40 lines (with
inline duplication of the discovery flow, when-to-set-which, and
federation port caveat) down to ~8 lines pointing at
`docs/gateway.md`. The brief `server_name vs gatewayHost` clarifier
stays in code because it disambiguates two SIMILAR-LOOKING options
on the same module — operators reading option docs need the
distinction inline, not behind a doc link.
Also trimmed `matrix.gui.enable` + `matrix.gui.package` descriptions
to similar shapes — point at docs/gateway.md for the architecture,
keep the override-shape hints in code.
Push includes the rebase onto current main (#764 + 0af6ea1 + others
landed since #775 was opened; cherry-picked commits get skipped
cleanly).
Net: matrix.nix loses ~70 lines of inline prose. No behavioral
change (verified gatewayHost still resolves to `matrix.<hive>`).
mara on PR #775: "this is too much docs in code - move bigger picture
stuff to md files and put refs in code"
New `docs/gateway.md` consolidates the gateway architecture story
that was spreading across long inline comments in `hive-gateway.nix`,
`hive-matrix.nix`, and `hive-forge.nix`:
- vhost map (which URL serves what, which upstream, which option)
- matrix discovery flow (.well-known → sub-domain delegation
sequence)
- Accept-header SPA fallback pattern (#686 / #729 design history)
- local-dev `localHostsEntry` story
- sub-domain rationale (mara verdict tracking) + when sub-path is
right (hyperhive-internal apps)
- per-vhost tuning knobs (forge LFS, matrix long-poll, agent SSE)
- sequencing history (which PR added which routing piece)
In-code comments in the two nix modules get trimmed to short refs
into the doc — keeps the *why* in the markdown while the *what*
stays alongside the code:
- hive-gateway.nix: top-of-file comment, `agentPortsTable`,
`appendHttpConfig`, every location block + vhost
- hive-matrix.nix: `fluffychat-web-fixed`, `fluffychat-web-imaging`,
the dart compile postInstall
README.md gets a new row in the docs table pointing at gateway.md.
Verified `nix eval` still resolves the same vhost + location layout
after the comment trim — no behavioral change, just less in-code
prose.
mara on #764:9897: "host the fluffy chat app at / as follow up?"
Moves fluffychat-web from the bare-domain sub-path
(`<hive>/matrix/`) to the matrix sub-domain root
(`matrix.<hive>/`). Follow-up to #764 (matrix vhost itself), per
mara's gateway-architecture verdict (sub-domain for external standard
apps, sub-path for hyperhive-internal). Stacked on
`atlas/747-matrix-behind-gateway` — depends on #764 landing first.
## Mechanics
**hive-matrix.nix:**
- Drop `flutterBuildFlags = [ "--base-href" "/matrix/" ]` from
`fluffychat-web-fixed`. Upstream default `--base-href "/"` is correct
at sub-domain root.
- Update option docs to reflect new mount point.
**hive-gateway.nix:**
- `$matrix_spa_target` map target flips from `/matrix/index.html` →
`/index.html` (sub-domain root now).
- New `<hive>/matrix/*` location: `rewrite ^/matrix/(.*)$
matrix.<hive>/$1 permanent;` — 301 redirect preserves bookmark +
deep-link compatibility for `<hive>/matrix/#/rooms/...` URLs during
the transition.
- `<hive>/matrix/config.json` location removed (moved to `/config.json`
on the matrix vhost).
- Matrix vhost (#764) gains `/` location: serves fluffychat dist as
static files with the Accept-header SPA fallback (`/_matrix/`
proxying to tuwunel keeps working via nginx longer-prefix-wins
precedence). When `gui.enable = false`, `/` returns 404 cleanly.
- Matrix vhost gains `= /config.json` for the FluffyChat boot-config
pre-fill (#736).
## Verified
```
vhosts: ["_", "forge.test.local", "matrix.test.local"]
bare locations: ["/", "/matrix/", "= /.well-known/matrix/client",
"= /.well-known/matrix/server"]
matrix vhost locations: ["/", "/_matrix/", "= /config.json"]
/matrix/ extraConfig: "rewrite ^/matrix/(.*)$ http://matrix.test.local/$1 permanent;"
matrix vhost / alias: /nix/store/...fluffychat-web-2.6.0/
```
Full container toplevel builds clean.
## Risk
Medium. Two breaking changes for operators:
1. **Bookmark migration**: `http://<hive>/matrix/#/rooms/...` 301s
to `http://matrix.<hive>/#/rooms/...`. Browser bookmarks +
shared links keep working via the redirect; can be cleaned up
once it's been in the wild long enough.
2. **fluffychat-web dist hash changes**: dropping the
`--base-href "/matrix/"` flag changes the derivation hash, so
`gui.package` rebuilds even though the source is the same.
Operators on substitute caches will fetch the new dist; building
from source takes the same time as before.
The `.well-known/matrix/{client,server}` delegation (already
advertising `matrix.<hive>` per #764) means matrix clients
auto-discover the new location — no client config change needed.
## Sequencing
**Depends on #764** — needs the matrix vhost to host the new `/`
location. Merge after #764 lands + soaks. If #764 changes shape
during review I'll rebase + force-push.
Closes#772.
Mara on #774: previous batches were cookie-stripping rather than
prose migration. This batch actually moves substantive comment
prose from tabs.js into docs/web-ui.md.
Moved to docs/web-ui.md:
- Container row → **Icon layout + load strategy** sub-paragraph
(new): absolute-positioning rationale (so img load state can't
reflow row), fire-and-forget load pattern (no pre-check
reachability), favicon fallback chain, icon-unreachable class,
the immediate-fallback-when-stopped optimisation.
- Container row → **Pending-state derivation** paragraph (new):
three-source priority order (transient → in-flight queue → none),
why ContainerStateChanged isn't enough, the opRunning flag's role
in driving the pending-running class + spinner.
Collapsed in tabs.js:
- Icon block (~14 lines of WHY comments + pointer to docs) →
4-line pointer + behavioural one-liner. Drops #177 / #195 / #202
cookies en passant since their substance now lives in docs.
- Pending-state block (~22 lines split across two paragraphs) →
4-line pointer. Drops #769 self-cookie (the queued vs running
split lives in docs::Container row now).
- SharedWorker EventSource (~7 lines) → 5-line pointer. Drops
#448 cookie (the SSE multiplexing paragraph in docs already
has the design + Firefox throttling rationale; the in-code
comment was duplicating).
- M4TR1X tab gating (~4 lines) → 2-line pointer. Drops #607 cookie
in both tabs.js + docs/web-ui.md::Tab strip (the substance was
already in docs, just had the cookie attached).
tabs.js: 11 → 6 refs (92% reduction from baseline 73). Net effect:
~47 lines of substantive prose moved out of tabs.js into
docs/web-ui.md, where it belongs.
#437 (multi-step progress on rebuild_queue entries — closed) ×2:
in tabs.js current-step comment + in docs/web-ui.md prose
referencing the phase annotation.
#501 (PR — merged for #437) ×1: paired with #437 in the same
tabs.js current-step comment.
#575 (X button to cancel pending builds — closed) ×2: in tabs.js
cancel-form comment + in docs/web-ui.md cancel-button prose.
#436 (route approval execution through rebuild_queue — closed)
×1 in docs/web-ui.md: scrubbed the parenthetical "; #436" next
to the approval source-chip description (small freebie since I
was editing that paragraph anyway). The approval-as-source-chip
is the implementation of #436; the cookie was just history.
Pre-scanned docs for matching cookies; caught all 3 doc cookies
in the same PR.
#459 (dashboard tab + creation form for scheduled prompts —
closed) ×2 (section header + tab-activation re-fetch comment) +
#444 (make it possible to schedule prompts — closed) ×1 (Backend
endpoint enumeration preamble) + #535 (schedules-as-table —
closed) ×1 (renderSchedulesList layout rationale). The
section-header comment now reads as a pure mile-marker; the
backend-endpoints preamble enumerates what the API surface is
today; the table-layout comment describes the rendered shape.
No docs/web-ui.md matches for any of #444 / #459 / #535.
Container row's pending highlight fired for both queued + running
rebuild-queue entries, so a long queue painted half the SW4RM tab
amber. Mara on #769: don't highlight while queued, show running as
spinner on agent icon.
- tabs.js: derive opRunning (transient OR op.state === 'running')
separately from pending; add a pending-running row class.
Operator-initiated transients still count as running so the
rebuild-now-clicked → backend-picks-it-up window stays visually
consistent.
- dashboard.css: move the amber border + tint from .pending to
.pending-running (queued rows now keep their normal look, with
the badge text still saying "rebuild queued"). Add a 1s linear
rotating amber arc as .pending-running > .container-icon::after,
with overflow:visible so the ring sits just outside the icon and
composes with the mauve selected ring.
- docs/web-ui.md::Container row: describe the queued vs running
split in prose. Drop the orphaned (#398) cookie while I'm at it
— small #712 freebie.
Folds both 🟡 notes from argus's #764 review:
1. **Empty-string assertion on `cfg.gatewayHost`**: same footgun as
the forge.domain rejection from #754 — empty would render `.<hive>`
shaped garbage in both nginx server_name (wildcard catch-all,
surprising) and /etc/hosts (invalid entry). Fail loud at toplevel
build with a message pointing at `null` as the right opt-out.
2. **Federation port-8448 caveat in `gatewayHost` docs**: when the
gateway listens on 80, `.well-known/matrix/server` advertises
`${gatewayHost}` with no port suffix → matrix federation spec
falls back to port 8448 → no listener on 8448 → cross-hive
federation requires either `_matrix._tcp.${gatewayHost}` SRV
record OR `services.hyperhive.matrix.openFirewall = true`.
Hyperhive is mostly closed/internal so this rarely bites, but
the option docs now flag it for the federation-curious operator.
Verified: `gatewayHost = ""` triggers the new assertion at toplevel
build with the expected message; default still resolves to
`matrix.<hive>` cleanly.
mara on #747:9722: "this still seems to be an issue in current version"
(after #751 closed without merge). Mirroring the forge sub-domain
pattern just merged as #754 for matrix per mara's #749:9609 verdict
(sub-domain over sub-path for forge + matrix, "not user-visible for
matrix because the .well-known/matrix/{client,server} redirect routes
clients through automatically").
## Mechanics
**New `services.hyperhive.matrix.gatewayHost`** — nullable str, defaults
to `matrix.<services.hyperhive.domain>` when hive-domain set, else
null. Full hostname (`matrix.darkest.space`, `homeserver.internal.lan`)
for bespoke shapes per mara's #754:9684 "specify full domain in
options instead" pattern.
**Gateway:** new `server { server_name = matrixCfg.gatewayHost; }`
block proxying `/_matrix/...` → `http://127.0.0.1:<httpPort>/_matrix/...`
with matrix-spec CORS + tuned for long-poll `/sync` (1h timeout) +
typical media uploads (50M body cap). `/` returns 404 — nothing
else lives at the matrix vhost. Matches the forge vhost shape from #754.
**`.well-known/matrix/{client,server}`** (already served at bare hive-
domain since #660): now points at `matrixCfg.gatewayHost` (no port
suffix when gateway is on the canonical port 80) instead of the
direct `<hive-domain>:<httpPort>` shape. Falls back to direct shape
when `gatewayHost = null` (no hive-domain, or operator nulled it).
**`localHostsEntry` extension**: `/etc/hosts` (when set) now adds the
matrix sub-domain → 127.0.0.1 alongside hive-domain + forge.domain.
`lib.unique` collapses any duplicate (edge case if operator sets
gatewayHost equal to hive-domain).
## Verified via `nix eval`
```
vhosts: ["_", "forge.test.local", "matrix.test.local"]
gatewayHost: "matrix.test.local"
client wellknown: m.homeserver.base_url = "http://matrix.test.local"
server wellknown: m.server = "matrix.test.local"
/etc/hosts: ["test.local", "forge.test.local", "matrix.test.local"]
```
## What this fixes for #747
mara's HAR showed `GET /.well-known/matrix/client` and
`GET /_matrix/client/versions` both failing on `pr1ma.darkest.space`:
1. **`.well-known/matrix/client`** was advertising
`http://pr1ma.darkest.space:8008` — that URL only works if tuwunel's
port 8008 is firewall-open to the operator's browser (it isn't by
default — `services.hyperhive.matrix.openFirewall` defaults to false
since #651). Now advertises `http://matrix.pr1ma.darkest.space/`
which goes through the gateway on the (already-open) port 80.
2. **`/_matrix/client/versions`** was hitting the bare-domain `"_"`
vhost, which has no `/_matrix/` location — fell through to `/` →
c0re's dashboard upstream → 404. Now hits the new `matrix.<hive>`
vhost which proxies the request to tuwunel cleanly.
server_name + serverName unaffected — matrix identifiers (`@alice:<hive>`)
still embed the bare hive-domain per #660; only the wire-level transport
URL moves to the sub-domain.
## Risk
Medium. Existing matrix tokens / sessions stay valid because:
- `serverName` (the identifier domain) doesn't change
- tuwunel's `/_matrix/` endpoints serve the same requests, just reached
via the new sub-domain instead of the direct port
Operators with `services.hyperhive.matrix.openFirewall = true` and
external clients reaching `:8008` directly keep working too — the
sub-domain vhost is additive, doesn't take away the direct port.
## Sequencing
This is a parallel matrix-side mirror of #754 (forge). Both follow
the same mara-verdict pattern; once both have soaked, the gateway-
behind-everything story is done for v0.
Closes#747.
#272 (show approval requested-at — closed) ×2 in tabs.js (live
ApprovalAdded fallback note + amber stale chip comment) + ×1 in
docs/web-ui.md (Approval card identity header). #275 (select all
for meta inputs — closed) ×1 in tabs.js (bulk-select rationale)
+ ×1 in docs/web-ui.md (M3T4 1NPUTS tree control note). The
amber-stale chip line in tabs.js now points at
docs/web-ui.md::Approval card for the rendered spec instead of
the closed issue.
mara on PR #754: "would it be better to specify full forge domain in
options instead?"
Drops the awkward `cfg.subdomain` label option. Now `cfg.domain` is
the single source of truth for both the forgejo `DOMAIN` setting
(existing semantics) AND the gateway vhost server-name (new).
## Before / after
```nix
# before: separate label + cfg.domain juggling
services.hyperhive.forge.subdomain = "forge"; # → forge.<hive>
services.hyperhive.forge.domain = "localhost"; # unused for vhost
# after: full domain, single option
services.hyperhive.forge.domain = "forge.darkest.space"; # ← used for ROOT_URL + vhost
```
## Default
`cfg.domain` default auto-derives:
- `forge.<services.hyperhive.domain>` when hive-domain is set
- `"localhost"` otherwise (pre-#749 direct-on-port shape)
So the common case (hive-domain set) gets `forge.<hive>` for free,
operators with a bespoke shape (`git.example.com`) set the full
hostname directly.
## Assertions
- `cfg.domain != ""` — empty would render `.<hive>` shaped garbage
in both server_name + /etc/hosts.
- `cfg.behindGateway → gateway.enable` — can't route through a
gateway that isn't running.
(The previous "subdomain = empty" assertion is dropped — that
edge case is gone with the rename.)
## Verified
- default with `hyperhive.domain = "test.local"` → `forge.test.local`,
`ROOT_URL = http://forge.test.local/`, vhost present
- `forge.domain = "git.example.com"` → `git.example.com`,
`ROOT_URL = http://git.example.com/`, vhost = `["_", "git.example.com"]`
- `gateway.enable = false` → `forge.domain` falls back to `localhost`,
`ROOT_URL = http://localhost:3000/`, no gateway vhost
(`behindGateway = false`)
- `/etc/hosts` (when `localHostsEntry = true`) → unique entries for
hive-domain + forge.domain (de-duped via `lib.unique` for the
edge case where forge.domain = hive-domain)
- full container toplevel builds clean
## PR title
(Will fix the PR title separately — still says "/forge/" which is
wrong since the rewrite to sub-domain shape.)
argus on PR #754 v2 review:
> `subdomain = ""` edge case: when `cfg.subdomain = ""`, the
> `localHostsEntry` appends ".${domain}" (invalid hostname; bare
> domain is already covered) and the virtualHosts key becomes
> ".${domain}" (nginx treats this as a wildcard catch-all, not a
> bare-domain server block). docs call this "advanced: collides
> with dashboard server block" — the actual nginx behavior is
> more surprising than that.
Fix: reject `""` at assertion time rather than ship the surprising
behaviour. Bare-domain landing is what the dashboard already
serves; there's no use case for `""` that null doesn't already
cover. Updated option description + dropped the now-dead branch
from the `subdomain` let-binding.
Verified: `services.hyperhive.forge.subdomain = ""` triggers the
new assertion at toplevel build with a clear message pointing at
`null` as the right opt-out. Default + `null` paths still build
clean.
mara on #749:9609: "we will go with sub domains for forge and matrix
(redirected in well known in the latter case, not user visible). close /
fix PRs you have open that dont match this."
Reshapes the v1 sub-path (`<host>/forge/`) approach into a sub-domain
vhost (`forge.<host>/`) per the mara verdict. matrix gets the same
treatment in damocles's #751 follow-up.
## Why sub-domain
- forgejo's default `ROOT_URL = http://<host>/` works without any
`X-Forwarded-Prefix` gymnastics — sub-domain hosting is the
canonical Forgejo deploy shape, matches every upstream-doc example.
- Cookie / storage isolation between the dashboard and forge (XSS blast
radius shrinks; a future forge XSS can't reach dashboard session).
- matches the matrix-spec pattern that #751 wires up for the
homeserver.
## Mechanics
**forge options:**
- `services.hyperhive.forge.subdomain` — nullable str, default `"forge"`
→ rendered sub-domain is `forge.<hive-domain>`. Set to `null` to opt
out (forge stays direct on `httpPort`); set to `""` for bare-domain
landing (advanced, collides with dashboard).
- `services.hyperhive.forge.rootUrl` — nullable str override. When
null, auto-derived: `http://<subdomain>.<hive>/` when gateway is on
+ subdomain set, else `http://<domain>:<httpPort>/` (direct).
- **Asserts** rootUrl ends with `/` (argus 🟡 on #754: forgejo's
ROOT_URL contract requires trailing slash, else emits
`https://forge.example.com.user.id` shaped garbage). Asserts
`subdomain != null` requires `hyperhive.domain` set.
**gateway:**
- New `virtualHosts."<subdomain>.<hive-domain>"` server block —
separate from the `"_"` catch-all. Proxies all `/` →
`http://127.0.0.1:<forge.httpPort>/` so forgejo handles requests at
root (no prefix translation needed; matches the upstream-default
ROOT_URL shape).
- Git-tuned: `client_max_body_size 1G`, `proxy_read_timeout 1h`,
`proxy_send_timeout 1h`, `proxy_buffering off`,
`proxyWebsockets = true`. SSH stays direct on `cfg.sshPort`.
- `networking.hosts` (when `localHostsEntry = true`) now also adds
`forge.<hive-domain> -> 127.0.0.1` for the dev loop.
## Verified
- `nix eval ROOT_URL` → `http://forge.test.local/` (default with
gateway on)
- `nix eval ROOT_URL` with `gateway.enable = false` → `http://localhost:3000/`
(current direct shape preserved)
- `nix eval virtualHosts attrs` → `["_", "forge.test.local"]`
- `nix eval networking.hosts` with `localHostsEntry = true` →
`{"127.0.0.1": ["test.local", "forge.test.local"], ...}`
- bad rootUrl (no trailing /) triggers assertion at toplevel build
with the spelled-out forgejo failure mode
- full container toplevel builds clean
(`nixos-system-hive-gateway-26.05pre-git`)
## Migration
ROOT_URL change is a one-way migration on rebuild:
- Existing agent `git remote origin` URLs (`http://localhost:3000/...`)
**keep working** — forgejo accepts any inbound URL; the URL on the
agent side is unchanged.
- New clone-link copy-paste from forge UI uses `forge.<hive>/...` —
operators copying clones after this lands need to go through the
new sub-domain.
- Direct browsing on `:3000` shows pages with `forge.<hive>` links →
works if hosts entry / DNS resolves, broken otherwise. Operators
should switch to `http://forge.<hive>/`.
## Out of scope
- TLS termination (mara explicit on #15: no TLS v0)
- SSH-over-HTTPS / wildcard cert provisioning
- matrix sub-domain (damocles's #751, sibling work)
Closes#749. Addresses argus 🟡 on #754.
#406 (split app.js → tabs/flow/common) closed; the migration is
done — the 'moved to ./flow.js' / 'pre-step-2 this wiring lived
in the broker-terminal IIFE' commentary is git-history, not
present-state docs. #408 (split flow messages from main sse) also
closed; the 'will give /index.html its own stream' line was
forward-looking design that didn't ship that way. Comments
tightened to describe what the code does today; the section
headers, behaviour notes, and reconnect-rationale all stay
intact.
Three small clusters of cookies removed:
- #262 (×2): agent-declared dashboard links via /api/agent/<name>/links.
Both comment blocks describe the same architectural decision
(agent backend = source of truth, DOM-built so link strings can't
reach the HTML parser). The cookies just attributed the decision
to a PR; the prose stands without them.
- #486 (×2): M0V3 topology affordances + addBulkButton perAgentBodyFor
hook. Substantive prose moved to docs/web-ui.md::Selection bar in
#695 already; in-code comments now reference the docs.
- #163 (×3): snapshot re-sync + SSE catchup idempotence. The cookies
flagged 'this is why the guard exists'; replaced with the present-
tense 'post-disconnect SSE catchup can carry duplicate rows' which
reads as the actual reason without needing the issue context.
tabs.js: 31 → 24 #NNN refs (-7). Net +1 line (the rewrites are
sometimes slightly longer when 'issue #N' is replaced with the
substantive description; net is still ahead). 67% reduction since
milestone start.
refs #712
Same theme as the tabs.js scrubs in this batch — drop the 7 #NNN
cookies (#444, #459, #460, #467, #474, #535, #564) the SCH3DUL3S
section was carrying. The substantive prose stands on its own;
the citations were just attribution tracking that belongs in the
git log + issue threads, not the user docs.
Mara on #744: 'no docs md changes needed?' — yes, this.
Four clusters of cookies in tabs.js, all pointing at prose that's
either already in docs/web-ui.md::SCH3DUL3S tab or self-evident
from the code:
- #564 (×4): inline-create row + carry-state for schedules. Layout
rationale is in the docs; the in-code comments describe what the
state does, which stands on its own.
- #474 (×3): inline edit form + targets multi-select for
schedules. Same pattern — docs cover the UX, code comments
describe the implementation pieces.
- #399 (×4): null-guard for tabs.js sections that may not exist on
/flow.html. The 'pre-split this code lived in flow.js' historical
paragraph dropped entirely; replaced with the present-tense
no-op-when-target-absent convention statement.
- #335 (×3): question-TTL chip ticker. Cookies dropped; substantive
text stays.
Plus #466 (interval composer shared use) and some adjacent cleanup
from the same passes — small additional drops folded in.
tabs.js: 48 → 31 #NNN refs (down from 73 at start of milestone).
Net -8 lines. Functional code unchanged; build clean.
refs #712
turn-loop.md: add optional hive_name / swarm_name fields to get_agent_meta
response shape; note they are omitted when the host options are unset.
CLAUDE.md: update hive-gateway.nix entry to mention per-agent routing
and .well-known; drop stale #609 cookie.
mara on PR #740 comment 9295: "we decided to go with the json" (issue #15 comment 9270:
"nginx container lives in system config, so it cannot be just rebuilt
from meta flake. go for the json file the c0re writes").
Drops:
- `cfg.agents` listOf str option
- Replicated FNV-1a hash + char-code table + manager-port special case
- Drift-hazard comment (no more rust↔nix constant sync)
Adds:
- `cfg.agentPortsFile = "/var/lib/hyperhive/agent-ports.json"` (default,
nullable to disable) — path to a JSON map of `{ "<name>": <port> }`
written by hive-c0re on every topology change.
- `agentPortsTable` reads the file at eval time via
`builtins.fromJSON (builtins.readFile path)`, guarded by
`builtins.pathExists` so a missing file gracefully defaults to `{}`.
- Per-agent locations generated via `lib.mapAttrs'` over the table —
one location block per entry; empty table → empty attrset → no
per-agent blocks, pre-#15 shape.
Rust-side dependency: hive-c0re needs to emit the JSON file on every
topology change. Coordinating with damocles via a separate ping — the
nix side ships now with safe defaults (missing file = no routes, no
behavior change vs main).
Verified:
- nix eval with `/tmp/test-agent-ports.json` → 4 per-agent blocks at
correct ports (8178 iris, 8267 argus, 8304 atlas, 8549 damocles)
- nix eval with nonexistent file → only `/` location (graceful default)
- full container toplevel builds clean with matrix on
Empty file case mirrors the previous empty-list default — purely
additive, old `<host>:<port>/` direct reach untouched, no per-agent
blocks until c0re writes the JSON. Operator can also `null` the
option to disable entirely.
Per mara on #14 (comment 9081): focused, purely additive to what's
there, no TLS / no manager special cases, old `<host>:<port>/` path
keeps working. Builds on iris's #731 (agent UI now serves
document-relative URLs so it works under any nginx prefix).
Mechanics:
- New `services.hyperhive.gateway.agents` option (`listOf str`,
default `[]`) lists sub-agent names to expose at
`/agent/<name>/` through the gateway.
- For each name, generate one `location /agent/<name>/` block that
`proxy_pass`es to `http://127.0.0.1:<port>/`, where `<port>`
is computed from the same FNV-1a hash hive-c0re uses internally
(`lifecycle::agent_web_port`).
- Trailing-slash pair on location + proxy_pass strips the
`/agent/<name>` prefix on the upstream side — agent server
receives `GET /`, `GET /api/state`, `GET /screen/ws`, etc. as if
reached directly on its port.
- `X-Forwarded-Prefix` set so the harness can build correct absolute
URLs for cases where document-relative isn't enough.
- `proxyWebsockets = true` + `proxy_buffering off` keeps SSE
+ WS endpoints working transparently.
- Empty `cfg.agents` (default) → no per-agent blocks generated.
- Manager not included — already gets `/` via the c0re upstream.
FNV-1a hash replicated in nix to match `lifecycle::agent_web_port`
line-for-line. Verified against rust output for 8 representative
agent names:
agent | nix | rust | match
iris | 8178 | 8178 | ✓
atlas | 8304 | 8304 | ✓
argus | 8267 | 8267 | ✓
damocles | 8549 | 8549 | ✓
manager | 8000 | 8000 | ✓ (special case)
dmatrix | 8266 | 8266 | ✓
triage | 8737 | 8737 | ✓
bitburner | 8658 | 8658 | ✓
Drift hazard documented in the let-block comment: if the rust
constants change (MANAGER_PORT, WEB_PORT_BASE, WEB_PORT_RANGE, or
the FNV-1a parameters), the nix copy needs a lockstep bump or
gateway will proxy to wrong ports. Tracked in the option's
description as a follow-up to single-source via
`/var/lib/hyperhive/meta/topology.json` lib.importJSON OR runtime
nginx-include written by c0re.
Char-code lookup table covers `[a-z0-9_-]` — the current
`hyperhive.user.name` alphabet. Names with other chars produce an
eval-time error rather than a silent wrong hash.
Verified:
- `nix eval` on the locations attrset for [iris atlas argus damocles]
→ correct ports (matching rust impl) on each `/agent/<name>/` block
- empty `cfg.agents` default → no per-agent blocks (`[ "/" ]` only)
- full container toplevel builds cleanly with 7 agents + matrix on
(`nixos-system-hive-gateway-26.05pre-git`)
Sequencing per mara: this is #15 v0 (gateway-side per-agent routing,
purely additive). #14 netns isolation follows once this soaks.
Out of scope: TLS, manager special-case routing, per-agent unix
sockets (mara: "at some point the agent servers will be domain
sockets"), CORS workaround removal at `POST /answer-question/{id}`,
gateway auth.
Closes#15 v0.
Seven cookies removed across the container-card rendering code (4
#398, 3 #432). All point at existing docs/web-ui.md::Container row
prose (rebuild_queue building badge, stopped-container state
clearing). Each comment shrinks to either a short docs ref or just
drops the cookie where the substantive content already explains
itself.
Net ±0 lines in tabs.js (rewrites are similar length); the win is
each comment now reads as 'what this code does' rather than 'why
PR #N decided this'. tabs.js is now at 48 #NNN refs (down from
55 at start of this batch, 73 at start of the milestone).
refs #712, builds on #722 + #724 + #728
mara on PR #729: "this still feels hacky - is there a proper way to do this?"
damocles: agreed, "Accept-header map is meaningfully better than the
allowlist [...] one map definition that encodes browser semantics directly,
vs ~20 extensions to keep synced with whatever fluffychat (and any future
hyperhive-served SPA) decides to ship".
The previous shape (#684 catch-all regex, then this PR v1's
extension allowlist) leaned on heuristics to distinguish "missing
asset → 404" from "unknown SPA route → fall back to index.html".
Both shapes were fragile against a SPA shipping a new extension,
and the allowlist became dead code the moment a route ended in
`.html-ish-suffix`.
The proper distinction lives at the HTTP layer: top-frame browser
navigations send `Accept: text/html,...` (chrome/firefox/safari are
consistent on this). Asset fetches from script tags / img / fetch() /
XHR send asset-typed Accepts (`image/*`, `application/javascript`,
`*/*`) without `text/html`.
Mechanics: an `nginx http`-context `map` keyed on `$http_accept`
emits either `/matrix/index.html` (navigation) or a sentinel
nonexistent path (`/__matrix_spa_no_html_fallback`); the location's
`try_files $uri $uri/ $matrix_spa_target =404;` does the right thing
for both cases. No extension list, no regex narrowing, no `if` block,
no named-location fallback.
The `map` lives in `services.nginx.appendHttpConfig` (only added
when the matrix GUI is on, otherwise no `map` directive at all).
The location's `extraConfig` is now a single `try_files` line.
Verified via `nix eval` on both the rendered `appendHttpConfig` and
the location's `extraConfig`. Full closure build pending operator
deploy.
Closes#686.
turn-loop.md:
- expand stale '{label} and {operator_pronouns}' note to all 5
current placeholders (#730 added hive_identity + swarm_identity)
- add 200-char + single-line validation note to set_status (#723)
When I rewrote the original 'Per-card action buttons … moved into
the sticky #selection-bar (see renderSelectionBar) which appears
when the operator has at least one agent selected via the icon
click' comment to use the docs ref, I shrunk part of it but left
the second sentence as a dangling fragment glued onto the new
short ref. Collapse cleanly: docs ref + the still-useful
`needs update ↻` chip note.
mara's review nit: explaining the absence of a manager special-case
implicitly endorses the idea that special cases would be normal.
Default is no special case; the doc shouldn't dwell on it.
- ST0P comment block removed entirely (the bulk-button line above
is self-explanatory; the substantive 'c0re survives manager-down'
rationale lives in the host docs, not here).
- M0V3 helper comment reframed: trim the 'no special-case' framing
but keep the substantive note that the backend refuses moves it
can't satisfy and the failure surfaces in the bulk roll-up.
- docs/web-ui.md::Selection bar bullet list: drop the 'manager
included; no special-case' qualifiers; just describe the action.
M0V3→ROOT keeps the rationale that backend refusals surface in
the roll-up but stops calling out manager specifically.