Commit graph

1,007 commits

Author SHA1 Message Date
iris
815f6561b7 screen.html + docs: migrate canvas-sizing + RFB design prose (#713 batch 3)
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.
2026-05-31 14:47:32 +02:00
iris
b07d74b51b agent.css + docs: migrate icon sizing + popover :not([hidden]) prose (#713 batch 2)
agent.css carried 17 #NNN cookies, mostly attribution refs to the
#394 vibec0re overhaul (mara's full-screen redesign). Most were
not substantive WHY-prose — just "this exists because of #394"
breadcrumbs. The two genuine WHY-explanations move to docs.

Moved to docs/web-ui.md::Per-agent page:
- **Agent icon paragraph**: rewrote with explicit-em-sizing
  rationale (intrinsic <img> dimensions push parent flex container
  open via align-items: stretch height feedback) + the 5em ≈ 6em
  min-height - 0.5em padding × 2 derivation + align-self:
  flex-start sticks-to-top.
- **Overflow button paragraph**: added the :not([hidden]) display
  scoping rationale (UA stylesheet sets display:none on [hidden],
  but author display:flex would override — scope to :not([hidden])
  so the popover stays hidden until JS unhides).

Collapsed in agent.css: 17 cookies scrubbed across the file:
- #360 (full-screen vibec0re overhaul — closed) ×2: section
  header preface + side-panel section header
- #394 (vibec0re header redesign — closed) ×7: header height,
  main column, agent icon, meta-nav, overflow trigger, two
  orphaned-style tombstones
- #411 (popover scoping + icon align-start — closed) ×2: icon
  sticks-to-top + overflow popover scoping
- #568 (OAuth code mask + reveal — closed) ×1: show/hide toggle
- #666 (ask→operator inline-answer slot — closed) ×1: slot
  styling (substance partly in PR #780 docs section)
- #559 (mark all read header row — closed) ×1
- #376 (inbox row layout — closed) ×1: long-message wrap
- #375 (tail pill z-index — closed) ×1: collision fix

agent.css: 17 → 0 refs (100% reduction, fully migrated).
2026-05-31 14:47:32 +02:00
damocles
ea5f70629c harness-base: generic bash-env.sh + _bashEnvFragments accumulator (mara on #779)
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.
2026-05-31 14:46:29 +02:00
damocles
5f61528133 harness-base: cargo --message-format short by default (#777)
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.
2026-05-31 14:46:29 +02:00
atlas
309879dba0 docs: extract 3 substantive harness-base.nix prose blocks (#718, first pass)
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.
2026-05-31 14:44:12 +02:00
iris
3f84b6492a agent/app.js + docs: migrate ask→operator inline-answer binding prose (#713 batch 1 cont.)
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.
2026-05-31 14:35:09 +02:00
iris
8366c3739b agent/app.js + docs: migrate stacking-context + OAuth + nav-link prose to docs (#713 batch 1)
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.
2026-05-31 14:35:09 +02:00
atlas
8d25b087b6 nix/hive-matrix: trim option-description prose to docs/gateway.md refs (argus #775 🔴)
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>`).
2026-05-31 14:29:40 +02:00
atlas
922057e81b docs/gateway.md: extract big-picture gateway docs, trim in-code comments (mara #775:9988)
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.
2026-05-31 14:29:40 +02:00
atlas
df549ed2a5 nix/hive-{matrix,gateway}: host fluffychat at matrix.<hive>/, 301 from <hive>/matrix/ (#772)
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.
2026-05-31 14:29:40 +02:00
iris
ea1c6e4d05 tabs.js + docs: migrate icon + pending-state prose to docs (#712 batch 11)
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.
2026-05-31 14:23:49 +02:00
iris
68c2b3284e tabs.js + docs: drop rebuild-queue cookies (#712 batch 10)
#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.
2026-05-31 14:16:30 +02:00
iris
c527f739ec tabs.js: drop scheduled-prompts cookies (#712 batch 9)
#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.
2026-05-31 14:12:44 +02:00
iris
272d8af620 dashboard: split queued vs running rebuild visual (#769)
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.
2026-05-31 14:10:37 +02:00
atlas
b106bb8c87 nix/hive-matrix: argus 🟡 on #764 — empty-gatewayHost assertion + federation port doc
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.
2026-05-31 14:08:40 +02:00
atlas
01cc664d76 nix/hive-{gateway,matrix}: matrix vhost at matrix.<hive-domain> + .well-known delegation (#747)
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.
2026-05-31 14:08:40 +02:00
damocles
8f9c77df06 hive-forge: list verb — filter issues/PRs by kind/state/assignee/author/label (#694 part 2) 2026-05-31 14:02:54 +02:00
iris
4ab4d99340 tabs.js + docs: drop #272 + #275 cookies (#712 batch 8)
#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.
2026-05-31 14:01:57 +02:00
lexis
47a97be958 docs(CLAUDE.md): forge.domain drives ROOT_URL + sub-domain vhost (follow-up to #754) 2026-05-31 14:01:44 +02:00
damocles
0af6ea1dd0 nix/hive-c0re: pin HIVE_FORGE_URL to 127.0.0.1 — agents can't resolve forge.<hive-domain> (#761) 2026-05-31 13:51:03 +02:00
atlas
9c27c4076f nix/hive-forge: unify on cfg.domain as full hostname, drop subdomain label (mara #754:9684)
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.)
2026-05-31 13:38:25 +02:00
atlas
de67002c94 nix/hive-forge: reject subdomain="" with assertion (argus #754 v2 🟡)
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.
2026-05-31 13:38:25 +02:00
atlas
f037056015 nix/hive-{forge,gateway}: move forge to forge.<hive-domain> sub-domain (#749, mara verdict)
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.
2026-05-31 13:38:25 +02:00
iris
3c2fffabdf tabs.js: drop #406 module-split + #408 forward-looking cookies (#712 batch 7)
#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.
2026-05-31 13:35:18 +02:00
damocles
266b8d243d docs(agent-hierarchy): drop missed manager-root mention from apply_set_parent refuses list (argus on #758) 2026-05-31 13:27:30 +02:00
damocles
2c3b62be55 topology: drop manager-root special case + notify three agents on reparent (#743) 2026-05-31 13:27:30 +02:00
damocles
9884ac013d docs(gotchas): drop missed Manager-fixed-at-8000 mention (argus on #756) 2026-05-31 13:25:13 +02:00
damocles
4526e40a49 lifecycle: drop manager port special case (#753) — manager hashes into 8100..8999 like every other agent 2026-05-31 13:25:13 +02:00
lexis
f9a492f7a9 docs: nix build attr path gotcha — nixosConfigurations not implicit (follow-up to #738) 2026-05-31 13:23:00 +02:00
lexis
7b0bb1b22f fix map direction: name→port not port→name (argus on #757) 2026-05-31 13:21:01 +02:00
lexis
ec0d1b38e5 docs: add agent_ports.rs to hive-c0re file map (follow-up to #748) 2026-05-31 13:18:07 +02:00
iris
7c9a1e690a docs/web-ui.md: scrub #262 + #486 cookies (mara on #752) 2026-05-31 13:13:07 +02:00
iris
8134b955e2 tabs.js: scrub #262/#486/#163 cookies from agent links + topology move + SSE resync (#712 batch 6)
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
2026-05-31 13:13:07 +02:00
damocles
e13c6f514c agent_ports: use MANAGER_NAME constant in filter test so assertion isn't trivial (argus on #748) 2026-05-31 13:07:38 +02:00
damocles
e197efd3a6 c0re: emit /var/lib/hyperhive/agent-ports.json on meta sync for gateway (#15) 2026-05-31 13:07:38 +02:00
iris
c43ff5d80b docs/web-ui.md: scrub #NNN cookies from SCH3DUL3S section (mara on #744)
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.
2026-05-31 13:03:48 +02:00
iris
9d9cac2f61 tabs.js: drop schedule + flow-split + TTL-ticker cookies (#712 batch 5)
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
2026-05-31 13:03:48 +02:00
lexis
a823fa4dd9 docs: get_agent_meta response gains hive_name + swarm_name (follow-up to #741)
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.
2026-05-31 13:03:12 +02:00
damocles
d0cc6e3643 nix/hive-gateway: serve /matrix/config.json with defaultHomeserver = hive domain (#736) 2026-05-31 12:55:55 +02:00
lexis
f611169ff0 docs: fix PR attribution in SPA fallback gotcha (argus nit) 2026-05-31 12:50:38 +02:00
lexis
6865c96697 docs: document Accept-header SPA fallback pattern (follow-up to #729) 2026-05-31 12:50:38 +02:00
damocles
a91cf4493f get_agent_meta: include hive_name + swarm_name in response (#710) 2026-05-31 12:50:19 +02:00
atlas
09c1abe5b4 nix/hive-gateway: switch to JSON port table per mara on #740 (#15 v0, option B)
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.
2026-05-31 12:49:53 +02:00
atlas
cb1a5cdbb8 nix/hive-gateway: per-agent /agent/<name>/ routing through gateway (#15 v0)
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.
2026-05-31 12:49:53 +02:00
iris
4c30b52e48 tabs.js: scrub #398/#432 cookies from container-card render path (#712 batch 4)
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
2026-05-31 12:35:08 +02:00
damocles
c3ec037105 lifecycle: prebuild attr path needs explicit nixosConfigurations.<name> (fix #721 regression) 2026-05-31 12:31:02 +02:00
atlas
68bac7986b nix/hive-gateway: SPA fallback via Accept-header map, not extension allowlist (#686)
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.
2026-05-31 12:27:52 +02:00
lexis
264c984398 docs: list all 5 system-prompt placeholders + set_status validation limits
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)
2026-05-31 12:23:59 +02:00
iris
10097bb9b6 tabs.js: collapse stale dangling sentence in per-card actions comment (argus #728)
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.
2026-05-31 12:16:27 +02:00
iris
69debe6b7d tabs.js + web-ui.md: drop 'no special-case manager' framing (mara on #728)
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.
2026-05-31 12:16:27 +02:00