feat(#1997): add prettier markdown formatter to treefmt

- .prettierrc: proseWrap=preserve (no prose reflow)
- .prettierignore: exclude hivectl-cli.md (auto-generated) + 11 docs
  with multi-line list-item continuations prettier would strip to col 0
  (CommonMark limitation in prettier's list handling)
- format 16 markdown files: cosmetic only (*→_, table alignment,
  heading normalisation) — verified no broken continuations, idempotent
This commit is contained in:
atlas 2026-07-02 19:55:26 +02:00 committed by mara
commit 8406a45275
17 changed files with 254 additions and 230 deletions

View file

@ -12,9 +12,9 @@ Topology lives in the hive-c0re-owned **meta repo**, alongside
```json
{
"ruth": null,
"ruth": null,
"alice": null,
"bob": "alice"
"bob": "alice"
}
```
@ -34,6 +34,7 @@ operator-driven:
All three converge on `topology::set_parent`, which delegates the
validation rules to a pure `apply_set_parent` helper. Refuses:
- unknown `child` / `new_parent` (typo guard),
- self-parenting,
- cycles (32-hop ancestor walk, mirroring `is_descendant_of`).
@ -93,15 +94,15 @@ where system-level facts live.
Once enforcement lands the rules collapse into:
| operation | who can do it |
|---|---|
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
| `request_init_config` (spawn a new child) | any agent, child added under self |
| `request_apply_commit` (any descendant's config) | any ancestor |
| `get_logs` (any descendant) | any ancestor |
| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor |
| `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
| operation | who can do it |
| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
| `kill` / `start` / `restart` / `update` (any descendant) | any ancestor |
| `request_init_config` (spawn a new child) | any agent, child added under self |
| `request_apply_commit` (any descendant's config) | any ancestor |
| `get_logs` (any descendant) | any ancestor |
| moderate questions / reminders (cancel any open thread of a descendant) | any ancestor |
| `send` / `recv` routing | parent ↔ same-parent siblings ↔ self ↔ descendants; explicit allow-list for anyone else |
| `request_update_meta_inputs` (bump meta lock) | root agents only (today: just `manager`) |
"Ancestor" walks `ContainerView.parent` chains; cycles are guarded by a
visited-set at dispatch time (a malformed topology.json can't lock the
@ -129,14 +130,14 @@ The `ManagerRequest::*` variants in `hive-sh4re/src/lib.rs` are
operations the manager flavour socket can make that sub-agent sockets
can't:
| variant | semantic | post-milestone |
|---|---|---|
| `RequestInitConfig` | seed an agent's proposed config repo | **topology** — existing direct child (re-init) or a brand-new name (child added under self on approval); a name owned by a different parent is refused |
| `RequestApplyCommit` | submit a commit sha for operator approval | **topology** — descendants only |
| `Kill` / `Start` / `Restart` / `Update` | container lifecycle on an existing agent | **topology** — descendants only |
| `RequestUpdateMetaInputs` | bump meta `flake.lock` | **per-agent cap** (root-only today; a future "let coder bump its own input" might grant it) |
| `GetLogs` | journalctl scrape of a sub-agent | **topology** — descendants only |
| `Wake` | inject a `from: <X>` message into self's inbox | **not really privileged** — the wire surface exists because daemon co-processes (e.g. `forge_notify`) need it. Sub-agents have the same via their own socket. |
| variant | semantic | post-milestone |
| --------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `RequestInitConfig` | seed an agent's proposed config repo | **topology** — existing direct child (re-init) or a brand-new name (child added under self on approval); a name owned by a different parent is refused |
| `RequestApplyCommit` | submit a commit sha for operator approval | **topology** — descendants only |
| `Kill` / `Start` / `Restart` / `Update` | container lifecycle on an existing agent | **topology** — descendants only |
| `RequestUpdateMetaInputs` | bump meta `flake.lock` | **per-agent cap** (root-only today; a future "let coder bump its own input" might grant it) |
| `GetLogs` | journalctl scrape of a sub-agent | **topology** — descendants only |
| `Wake` | inject a `from: <X>` message into self's inbox | **not really privileged** — the wire surface exists because daemon co-processes (e.g. `forge_notify`) need it. Sub-agents have the same via their own socket. |
### C — storage / mounts (`hive-c0re::lifecycle`)
@ -150,6 +151,7 @@ The manager container's nspawn bind set:
manager can read the system-wide deploy log
Tree-shape version:
- Each agent gets RW to `/agents/<descendant>/` for every descendant in
its subtree. The root agent (today: manager) gets RW to the full
forest as a special case of "the root has every other agent as a
@ -182,7 +184,6 @@ direct `$HYPERHIVE_STATE_DIR/<token>` read.
### F — drive-by checks across c0re
- `loose_ends.rs`: manager sees hive-wide loose-ends, sub-agents only
their own. **Topology** — every agent sees its own + its
descendants'.

View file

@ -1,12 +1,12 @@
# The operator/agent boundary
Design rationale for hyperhive's two-principal trust model. The
*implementation* work — container network isolation, the unifying
_implementation_ work — container network isolation, the unifying
gateway, core-daemon privsep — is tracked as `area:ops` issues on
the forge.
Today "the operator surface" and "the agent surface" are a
*convention*, not a boundary — nothing stops a container from
_convention_, not a boundary — nothing stops a container from
curling the core daemon on `localhost:<port>`, or another agent's
web UI. Network isolation, the gateway, and privsep together turn
that convention into an enforced boundary.
@ -19,7 +19,7 @@ that convention into an enforced boundary.
lifecycle POSTs) are served by the core daemon and only
reachable via the gateway.
- **Agent** — speaks only for itself, only over its per-agent
unix socket. The socket's identity *is* the agent (see
unix socket. The socket's identity _is_ the agent (see
`docs/conventions.md`, "identity = socket"). An agent must not
be able to reach the core daemon's HTTP surface, another
agent's socket, or another agent's web UI.
@ -30,7 +30,7 @@ that convention into an enforced boundary.
point.** They live on the core backend.
Worked example — answering an operator-targeted question is a
`POST /answer-question/{id}` on the core dashboard, *never* an
`POST /answer-question/{id}` on the core dashboard, _never_ an
`AgentRequest` variant. If it were a per-agent-socket request, an
agent could `curl` its own socket and spoof an operator answer.
The per-agent web UI POSTs cross-origin to the core for these
@ -44,7 +44,7 @@ container can reach `localhost:<core-port>`, the dashboard, and
every other agent's web port. Until that changes, the
operator/agent split is on the honour system — every boundary
claim above is aspirational. Network isolation is what makes the
boundary *real*; the gateway and privsep are ergonomics and
boundary _real_; the gateway and privsep are ergonomics and
defence-in-depth layered on top.
The `area:ops` issues followed this sequencing:

View file

@ -7,6 +7,7 @@ The `hive-ci` module runs a Forgejo Actions runner in a `hive-ci` nixos-containe
Set `services.hyperhive.forge.ci.enable = true` in the host NixOS config. That's it — no manual token provisioning.
**Requirements:**
- The internal forge is always present (mandatory), so the runner always has a hive-forge instance to register against — nothing extra to enable.
- Optional: tune `services.hyperhive.forge.ci.name` (runner name in forge admin panel), `concurrency` (parallel job capacity), `labels` (workflow targeting), `jobTimeout` (per-job wall-clock cap, default `"1h"`, Go duration string e.g. `"3h"` — a job that exceeds it is killed so a hung or runaway build can't hold the runner's single slot indefinitely).
@ -148,7 +149,7 @@ following to your host config:
```
**Remote builders:** if CI dispatches builds to a remote builder (e.g. via
`nix.buildMachines` / `ssh-ng://`), the build outputs land in *that host's*
`nix.buildMachines` / `ssh-ng://`), the build outputs land in _that host's_
store, so the same GC config should be applied wherever the builder runs —
GC on the coordinator host won't reclaim space on the builder.

View file

@ -32,17 +32,17 @@ somewhere."
### Queue kinds
| Kind | Description |
|------|-------------|
| `Rebuild` | Single-agent rebuild. Covers manual, approval-driven, auto-update, and meta-update cascade variants — all funnel through the same path. The start-after-rebuild is **deferred to a fast-lane `Start` follow-up** (`parent_id` = this entry) so the build lane is freed as soon as the profile-swap finishes instead of waiting out the container boot — see *Deferred start* under the rebuild path below. |
| `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. |
| `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. |
| `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. |
| `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent — prevents a restart racing a rebuild mid-flight. Sources: dashboard ↺ button, the `restart` MCP tool. |
| `PermChange` | Write a tool-group or capability change to the shared JSON file (`tool-groups.json` / `capabilities.json`), then rebuild the agent so the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes effect. Serialising the file write through the queue prevents concurrent dashboard batch-apply actions from racing on the shared file. After a successful file write, emits `CapabilitiesChanged` or `ToolGroupsChanged` SSE snapshot so the P3RM1SS10NS tab updates live. |
| Kind | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `Rebuild` | Single-agent rebuild. Covers manual, approval-driven, auto-update, and meta-update cascade variants — all funnel through the same path. The start-after-rebuild is **deferred to a fast-lane `Start` follow-up** (`parent_id` = this entry) so the build lane is freed as soon as the profile-swap finishes instead of waiting out the container boot — see _Deferred start_ under the rebuild path below. |
| `MetaUpdate` | `nix flake update` on the meta flake. The worker runs the lock bump itself, then enqueues a cascade of `Rebuild` entries with `parent_id` set to the meta-update's id. |
| `Spawn` | First-deploy of a new agent (approval-driven). Same serialisation as `Rebuild` from the operator's POV. |
| `Destroy` | For future use (`destroy --purge` does real I/O). Variant exists so the wire shape doesn't change later; not currently routed through the queue. |
| `Restart` | Stop + start a container without touching config (~5-10s). Routed through the queue so it serialises against in-flight rebuilds for the same agent — prevents a restart racing a rebuild mid-flight. Sources: dashboard ↺ button, the `restart` MCP tool. |
| `PermChange` | Write a tool-group or capability change to the shared JSON file (`tool-groups.json` / `capabilities.json`), then rebuild the agent so the updated `HIVE_TOOL_GROUPS` / `HIVE_CAPABILITIES` env var takes effect. Serialising the file write through the queue prevents concurrent dashboard batch-apply actions from racing on the shared file. After a successful file write, emits `CapabilitiesChanged` or `ToolGroupsChanged` SSE snapshot so the P3RM1SS10NS tab updates live. |
| `GracefulStop` | Quiesce then stop a container (the `?graceful=true` path on `/api/kill/<agent>`). Signals the harness (its next `Recv` returns `GracefulStop` — the inbound fence — so it runs a stop-checkpoint turn that flushes durable `/state`, then takes the normal post-turn compaction path if it crossed the watermark, then exits) and **immediately releases the build lane**, spawning a detached watcher that holds the `Stopping` transient across the drain (bounded by a 3-min timeout → hard-stop fallback) and then enqueues a fast-lane `Stop` (`parent_id` = this entry) for the actual `nixos-container stop`. Net: a whole-hive graceful stop signals every agent up front, drains overlap, and only the container teardowns serialise (on the fast lane). Queued so the signal can't race an in-flight rebuild for the same agent. |
**Intentionally not queued** (sub-second ops): the *hard* `start`, `stop`, `kill` via the direct API paths. (A *graceful* stop is the `GracefulStop` kind above — it takes a checkpoint turn, so it rides the queue.) The queue's fast lane does carry `Start` / `Stop` kinds, but only as **follow-ups** other entries enqueue for themselves — the graceful-stop teardown and the deferred start-after-rebuild — so the container op groups under its parent entry on the dashboard.
**Intentionally not queued** (sub-second ops): the _hard_ `start`, `stop`, `kill` via the direct API paths. (A _graceful_ stop is the `GracefulStop` kind above — it takes a checkpoint turn, so it rides the queue.) The queue's fast lane does carry `Start` / `Stop` kinds, but only as **follow-ups** other entries enqueue for themselves — the graceful-stop teardown and the deferred start-after-rebuild — so the container op groups under its parent entry on the dashboard.
### Dedup
@ -53,12 +53,12 @@ since the current run started).
### Sources
| Source | Meaning |
|--------|---------|
| `Manual` | Operator clicked rebuild / update-all / meta-update on the dashboard, or any other direct human action (CLI, an agent MCP tool). |
| `AutoUpdate` | Legacy startup-sweep source (flat, no parent). Replaced by `StartupSweep` for new boots. |
| `StartupSweep` | Child of a `StartupSweep` parent entry; boot-time per-agent rebuild with the sweep as the visual group header. |
| `Approval` | Triggered by an operator-approved `ApprovalKind::{Spawn, ApplyCommit}`. |
| Source | Meaning |
| -------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `Manual` | Operator clicked rebuild / update-all / meta-update on the dashboard, or any other direct human action (CLI, an agent MCP tool). |
| `AutoUpdate` | Legacy startup-sweep source (flat, no parent). Replaced by `StartupSweep` for new boots. |
| `StartupSweep` | Child of a `StartupSweep` parent entry; boot-time per-agent rebuild with the sweep as the visual group header. |
| `Approval` | Triggered by an operator-approved `ApprovalKind::{Spawn, ApplyCommit}`. |
### Cascade parent tracking
@ -83,6 +83,7 @@ in the queue as a terminal entry (`Done` / `Failed` / `Cancelled`) or no longer
the queue at all (evicted by `trim_history`, which only evicts terminals).
Use cases:
- Chain a `Rebuild` after an explicit prerequisite step without coupling them through
the `parent_id` cascade mechanism.
- Sequence a `PermChange` + `Rebuild` pair where the rebuild must not start until the
@ -243,10 +244,10 @@ agent container.
rebuild, so changes take effect on the next lifecycle op without
requiring a host rebuild.
| Option | Default | Description |
|--------|---------|-------------|
| `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. |
| `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. |
| Option | Default | Description |
| ---------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `services.hyperhive.c0re.agentCpuQuota` | `"200%"` | CPU cap per agent, as a percentage of one core (`"200%"` = 2 cores). Raise if agents hit CPU limits during builds or heavy tool use. |
| `services.hyperhive.c0re.agentMemoryMax` | `"4G"` | Memory cap per agent. Raise for agents that run large nix builds or hold big in-memory data. |
For a hive-wide cap across all containers together, set
`systemd.slices.machine.serviceConfig.CPUQuota` in your NixOS

View file

@ -14,16 +14,16 @@ Two scope sets live in `hive-c0re::forge`:
**`TOKEN_SCOPES`** (per-agent tokens):
| Scope | Why |
|-------|-----|
| `write:repository` | Create, clone, push, delete repos; merge PRs. |
| `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). |
| `write:user` | Edit own profile, create repos under own user. |
| `write:organization` | Create + manage orgs (lets agents share a forge namespace). |
| `read:user` | Token-owner endpoint used for self-identification at harness startup. |
| `write:misc` | Hooks, attachments, the rest of the long tail. |
| `read:notification` | Poll `GET /notifications` for unread events. |
| `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. |
| Scope | Why |
| -------------------- | ------------------------------------------------------------------------------------------------------- |
| `write:repository` | Create, clone, push, delete repos; merge PRs. |
| `write:issue` | Open / comment / review issues **and** pull requests (Forgejo namespaces PR conversation under issues). |
| `write:user` | Edit own profile, create repos under own user. |
| `write:organization` | Create + manage orgs (lets agents share a forge namespace). |
| `read:user` | Token-owner endpoint used for self-identification at harness startup. |
| `write:misc` | Hooks, attachments, the rest of the long tail. |
| `read:notification` | Poll `GET /notifications` for unread events. |
| `write:notification` | Mark notifications read via `PATCH /notifications/threads/{id}`. |
**`CORE_TOKEN_SCOPES`** (hive-c0re's own `core` user): everything in
`TOKEN_SCOPES` plus `read:admin` and `write:admin`. Site-admin
@ -102,7 +102,7 @@ resurfaces next tick.
**Size bound:** the per-poll prune retains only ids present in the
single `limit=UNREAD_FETCH_LIMIT` (50) fetch page, so the cursor never
exceeds that many entries — it tracks the unread *window*, not the
exceeds that many entries — it tracks the unread _window_, not the
all-time notification count. The fetch limit and the bound are the
same constant in `forge_notify.rs` (with a debug assertion), so a
future pagination change grows the ceiling visibly rather than
@ -165,7 +165,7 @@ silently (mark-read without delivery):
`user.login` matches `own_login`.
- **Self-authored creations** (an agent opening its own PR/issue) — the
already-fetched subject payload's poster `user.login` matches
`own_login`. Only *creations* are dropped; a later state change on the
`own_login`. Only _creations_ are dropped; a later state change on the
agent's own subject is driven by someone else and still surfaces.
`own_login` is fetched once at startup via `GET /api/v1/user`. On
@ -208,13 +208,13 @@ lookahead.
Five shapes, distinguished by the notification's classification:
| Trigger | Wrapper |
| --- | --- |
| Comment on issue / PR | `[comment on PR #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` |
| Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nreviewer: body\nassignee: ...` |
| New issue / PR | `[new PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` |
| Trigger | Wrapper |
| ----------------------------------- | --------------------------------------------------------------------------------- |
| Comment on issue / PR | `[comment on PR #N owner/repo] title\nurl: ...\n\nauthor: body\nassignee: ...` |
| Review submission | `[PR approved #N owner/repo] title\nurl: ...\n\nreviewer: body\nassignee: ...` |
| New issue / PR | `[new PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` |
| Later activity (open, not creation) | `[activity on PR #N owner/repo] title\nurl: ...\n\n<body excerpt>\nassignee: ...` |
| State change | `[PR merged #N owner/repo] title\nurl: ...\nassignee: ...` |
| State change | `[PR merged #N owner/repo] title\nurl: ...\nassignee: ...` |
Review labels come from the Forgejo `state` field: `APPROVED`
`approved`, `REQUEST_CHANGES``changes requested`, `COMMENT`
@ -232,7 +232,7 @@ original open notification and miss the review (#1637). So the `open`
state only earns the `new <kind>` label when the notification's event
time (`updated_at`) is within `NEW_ITEM_TOLERANCE_SECS` (120s) of the
subject's `created_at`. Anything later is labeled `activity on <kind>`
— neutral and non-misleading, since we can't cheaply say *what* the
— neutral and non-misleading, since we can't cheaply say _what_ the
activity was without an extra reviews fetch. Missing/unparseable
timestamps default to `new` (preserve prior behavior rather than mask a
genuine new item). Timestamps are parsed by a small dependency-free

View file

@ -201,7 +201,7 @@ Never use raw `curl` for forge access.
Agent containers bind-mount the host's nix-daemon socket. nspawn
containers don't get user-namespaces by default, so `nix build`
invocations *inside* the container can't set up the build sandbox
invocations _inside_ the container can't set up the build sandbox
and fail outright if the host daemon's
`nix.settings.sandbox-fallback` is `false` (nixpkgs default).
`nix/templates/harness-base.nix` does `lib.mkForce true` so builds
@ -277,7 +277,7 @@ connects to the compositor at `127.0.0.1:<vnc_port>`.
("Failed to connect to the bus" → binds `xdg_wm_base` then destroys it
= invisible window even though CDP works). The fix is **not** to wrap
each client in its own `dbus-run-session` (a private throwaway bus per
process — that's a *separate* session, defeating the one-session
process — that's a _separate_ session, defeating the one-session
model); it's this one shared bus, whose address is exported as
`DBUS_SESSION_BUS_ADDRESS=unix:path=/run/gui/bus` via
`systemd.globalEnvironment` so weston, the harness and every GUI client

View file

@ -15,13 +15,13 @@ of shared netns.
## v1 vs v2
| feature | v1 (this PR) | v2 (after netns isolation) |
|---|---|---|
| bridge interface | created on host, no slave NICs | per-agent veth pairs attach |
| dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) |
| agent container netns | shared host | private |
| agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver <bridge-ip>` |
| `address` rules target | `<bridge-ip>` (works in both modes) | unchanged from v1 |
| feature | v1 (this PR) | v2 (after netns isolation) |
| ------------------------ | ------------------------------------------------------- | ----------------------------------------------- |
| bridge interface | created on host, no slave NICs | per-agent veth pairs attach |
| dnsmasq binding | bridge IP (reachable via host loopback in shared netns) | bridge IP (reachable via veth in private netns) |
| agent container netns | shared host | private |
| agent `/etc/resolv.conf` | unchanged (host DNS) | `nameserver <bridge-ip>` |
| `address` rules target | `<bridge-ip>` (works in both modes) | unchanged from v1 |
The `address` rules ship pointing at the bridge IP from v1 so the
DNS contract is fixed before any container actually depends on it
@ -91,9 +91,10 @@ agent containers.
## Firewall posture
`networking.firewall.interfaces.<bridge>.allowedUDPPorts = [ 53 ]`
+ `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge
interface only. Other interfaces stay closed. The hive resolver
isn't an external-facing service.
- `allowedTCPPorts = [ 53 ]` opens the resolver on the bridge
interface only. Other interfaces stay closed. The hive resolver
isn't an external-facing service.
When `isolateContainers = true`, `allowedTCPPorts` is extended with
`[ 80 443 ]` so isolated agents can reach nginx (gateway container,
@ -131,14 +132,14 @@ agent containers from shared host netns to private netns. Set only after
### What the nix side does when `isolateContainers = true`
| effect | mechanism |
|---|---|
| IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` |
| Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC |
| Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks |
| Gateway access | `networking.firewall.interfaces.<bridge>.allowedTCPPorts = [ 80 443 ]` — lets isolated agents reach nginx on the host (shared netns) |
| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.<domain>` — agents resolve via dnsmasq, nginx proxies to forgejo |
| c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` |
| effect | mechanism |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| IP forwarding | `boot.kernel.sysctl."net.ipv4.ip_forward" = 1` |
| Internet NAT | `networking.nat { enable = true; internalInterfaces = [ bridgeName ]; }` — MASQUERADE on packets leaving via any external NIC |
| Loopback DROP | `networking.firewall.extraInputRules` — drops bridge-subnet → `127.0.0.0/8` traffic; defence-in-depth against routing table leaks |
| Gateway access | `networking.firewall.interfaces.<bridge>.allowedTCPPorts = [ 80 443 ]` — lets isolated agents reach nginx on the host (shared netns) |
| Forge URL | `HIVE_FORGE_URL` flips from `http://127.0.0.1:3000` to `http://forge.<domain>` — agents resolve via dnsmasq, nginx proxies to forgejo |
| c0re signal | `HIVE_NETWORK_ISOLATION=1`, `HIVE_NETWORK_BRIDGE`, `HIVE_NETWORK_SUBNET` in `systemd.services.hive-c0re.environment` |
`HIVE_NETWORK_SUBNET` is the host-side bridge IP + prefix (e.g.
`10.42.0.1/24`), **not** the canonical network address. The Rust side

View file

@ -15,10 +15,10 @@ An agent is **trusted code running inside its own nspawn container**. The
boundary that matters is the container: a sub-agent cannot see the host
netns, another agent's container, or another agent's state dir. Within its
own container the agent is privileged — it has **passwordless `sudo` by
default**. Isolating credentials *from the agent itself* is therefore **not a
default**. Isolating credentials _from the agent itself_ is therefore **not a
goal**: an agent can read its own tokens, its own `/home/<name>/.claude`, and
run arbitrary commands as root inside its container. (The narrow exception is
*cross-tenant* leakage — e.g. the unsandboxed-nix-build `0600` token policy
_cross-tenant_ leakage — e.g. the unsandboxed-nix-build `0600` token policy
below stops a build's nixbld user reading the agent's own forge token, and the
state-file endpoint stops one agent proxying another's files. Those harden the
boundary; they do not sandbox the agent from itself.)
@ -36,7 +36,7 @@ Each agent gets its own scoped credentials, never shared:
- **matrix token** → that agent's matrix account only.
So a compromised/confused agent's reach on the forge or matrix is bounded by
*its own* account's scope, not the swarm's. This is the main thing standing
_its own_ account's scope, not the swarm's. This is the main thing standing
between "one agent does something dumb" and "the whole hive is affected".
### Threat model: prompt injection → confused deputy
@ -44,7 +44,7 @@ between "one agent does something dumb" and "the whole hive is affected".
The realistic adversary **never needs to breach the container**. They supply
**untrusted input the agent reads and acts on**: a poisoned issue or PR
comment, a cloned repo's README/CI, a scraped webpage, a crafted matrix
message. The agent is the trusted, capable party; the *input* is the
message. The agent is the trusted, capable party; the _input_ is the
untrusted part. A successful injection turns the agent into a **confused
deputy** — it uses its legitimate capabilities (push, comment, deploy, run
shell) on the attacker's behalf.
@ -52,7 +52,7 @@ shell) on the attacker's behalf.
Mitigations are therefore about **bounding capability and inserting human
checkpoints**, not about sandboxing the agent from its own tools:
- **Operator merges, not the agent** — an agent may *push* branches, but a
- **Operator merges, not the agent** — an agent may _push_ branches, but a
**human (the operator) merges the PR**, keeping a person in the loop on the
highest-value action. On the **internal forge this is technically enforced,
not just convention**: agents can't create repos (`max_repo_creation = 0`),
@ -76,7 +76,7 @@ accepting**. The rule of thumb:
If an agent can deploy to prod, you are accepting the risk of a dropped
production database (via injection or plain error). If that's unacceptable,
the answer is *don't grant the capability* — not "grant it and hope the
the answer is _don't grant the capability_ — not "grant it and hope the
sandbox holds", because there is no sandbox between an agent and the tools you
handed it.
@ -101,7 +101,7 @@ before touching the filesystem:
(canonical host form or the in-container view `/agents/<n>/state/`)
- `/var/lib/hyperhive/shared/` — shared docs (`/shared/` in-container)
`/state/...` without an agent prefix is explicitly *not* accepted — it is
`/state/...` without an agent prefix is explicitly _not_ accepted — it is
ambiguous from the host's perspective.
Defense-in-depth layers (in order):
@ -181,22 +181,22 @@ for every operation that genuinely requires root.
**Narrow interface** — `PrivRequest` variants map 1:1 to specific
known operations; there is no arbitrary command pass-through:
| Operation | What it runs |
|-----------|-------------|
| `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill <name>` |
| `CreateContainer` / `UpdateContainer` | `nixos-container create/update <name> --flake <ref>` |
| `DestroyContainer` | `nixos-container destroy <name>` |
| `ListContainers` | `nixos-container list` |
| `ReadContainerJournal` | `journalctl -M <container> -n <n> [filters...]` |
| `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` |
| `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) |
| `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container |
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
| `DaemonReload` | `systemctl daemon-reload` |
| `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent/<name>/` socket directory |
| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` |
| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
| `RestartMatrixDaemon` | `systemctl --machine=h-<name> restart hive-matrix-daemon.service` |
| Operation | What it runs |
| ---------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `StartContainer` / `StopContainer` / `KillContainer` | `nixos-container start/stop/kill <name>` |
| `CreateContainer` / `UpdateContainer` | `nixos-container create/update <name> --flake <ref>` |
| `DestroyContainer` | `nixos-container destroy <name>` |
| `ListContainers` | `nixos-container list` |
| `ReadContainerJournal` | `journalctl -M <container> -n <n> [filters...]` |
| `ReloadGatewayNginx` | `systemctl -M hive-gateway reload/start/reset-failed nginx` |
| `WriteNspawnFlags` | write `/etc/nixos-containers/<container>.conf` (bind-mount list + network isolation vars) |
| `WriteResourceLimits` | write `CPUQuota=`/`MemoryMax=` systemd drop-in for agent container |
| `RemoveServiceDropin` | remove `container@<name>.service.d/` drop-in on destroy |
| `DaemonReload` | `systemctl daemon-reload` |
| `ChownSocketDir` / `ChmodSocketDir` | chown/chmod `/run/hive-agent/<name>/` socket directory |
| `RunForgeAdmin` | `nixos-container run hive-forge -- runuser -u forgejo -- forgejo admin <args>` |
| `WriteAgentForgeToken` / `WriteAgentMatrixToken` | write `0600` credential file into agent state dir |
| `RestartMatrixDaemon` | `systemctl --machine=h-<name> restart hive-matrix-daemon.service` |
**Container allowlist** — every request is validated against an
allowlist before any operation: only names matching the agent-name
@ -211,8 +211,8 @@ writes to only the paths `hive-priv` legitimately needs.
### Privilege boundary summary
| Component | Runs as | Privilege needed for |
|-----------|---------|----------------------|
| `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals |
| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes |
| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving |
| Component | Runs as | Privilege needed for |
| ---------------------------- | -------------- | ------------------------------------------------------------ |
| `hive-c0re` | `hive-core` | broker, HTTP dashboard, scheduling, approvals |
| `hive-priv` | `root` | container lifecycle, journal reads, bind mounts, cred writes |
| `hive-ag3nt` (per-container) | per-agent user | turn execution, MCP serving |

View file

@ -48,12 +48,12 @@ services.hyperhive.swarm.peers = {
The attrset key is the peer's DNS domain. Two independent, optional
trust knobs — pick by what you need to trust:
- **`certFingerprint`** (`"sha256:…"`) — pin the peer's TLS *leaf*
- **`certFingerprint`** (`"sha256:…"`) — pin the peer's TLS _leaf_
fingerprint. Scopes **only** to hive-c0re's own peer HTTPS checks
(the P33RS dashboard links + agent peer discovery below). It is
**not** consulted by matrix federation — tuwunel validates a peer's
federation certificate against the system CA bundle independently
(see *Matrix federation* below), so a fingerprint pin does nothing
(see _Matrix federation_ below), so a fingerprint pin does nothing
for a self-signed matrix cert.
- **`caCert`** (path to the peer's root CA PEM) — embeds that CA (at
build time, into the nix store — no runtime file on the host) and
@ -61,7 +61,7 @@ trust knobs — pick by what you need to trust:
alongside `hive-ca.pem` in every agent's
`security.pki.certificateFiles` (via the meta-flake renderer) **and**
in the matrix container's trust bundle, so tuwunel validates the
peer's *federation* TLS when it chains to that CA. Trust stays
peer's _federation_ TLS when it chains to that CA. Trust stays
**inside the hive** (agents + the matrix container), never the host
system trust store. **This is the knob that unblocks federation with
a self-signed peer hive** — use it instead of `certFingerprint` when

View file

@ -15,17 +15,18 @@ Submit a shell command for background execution (runs via `bash`).
Stdout and stderr stream to `harness/bash-tasks/<id>.{out,err}`.
When the task completes (or times out, or the process errors), the
harness fires a wake with `from: "bash-task-<id>"` and the exit code
+ last stdout lines in the body; handle it on a future turn.
- `timeout_secs` — kill the task after N seconds and mark it
- last stdout lines in the body; handle it on a future turn.
* `timeout_secs` — kill the task after N seconds and mark it
`timed_out`. Omit for no timeout (runs until natural exit).
- `wait_seconds` — inline poll before returning (capped at 30).
* `wait_seconds` — inline poll before returning (capped at 30).
When the task finishes within the window the full status is
returned immediately and no wake is fired; when the window expires
the task keeps running and the normal `task started: id=<id>`
response is returned. **Defaults to 3** — pass `wait_seconds: 0`
to disable inline waiting and always get the immediate response.
- `name` — optional caller-chosen task id. When set it replaces the
* `name` — optional caller-chosen task id. When set it replaces the
auto-generated hex id, so it surfaces in the wake `from`
(`bash-task-<name>`), in `status(<name>)` lookups, and in the
loose-ends list — a memorable label instead of an opaque id. A name

View file

@ -102,16 +102,16 @@ hive-forge repo-labels --json # full label ob
Several verbs read an issue/PR and look overlapping, but each has a
distinct output shape — pick by what you need:
| Need | Verb | Shape |
|------|------|-------|
| The full human view of one item — title, body, **and** every comment in order | `view <n>` | prose dump; **marks the thread read** (clears the read-before-comment guard). Top-level `view` auto-detects issue-or-PR by number; scoped `issue view <n>` / `pr view <n>` do the same when you already know the kind |
| Just the structured metadata of one item (state, labels, assignees, head sha…) | `issue <n>` / `pr <n>` | JSON object |
| Only the comments (skip the body), e.g. to catch up a long thread; for PRs, review bodies (approve/request-changes/comment summaries) are merged in too | `comments <n>` [`--tail N`] | comment + review-body list; **marks read** |
| One specific comment by its id | `comment-show <id>` | single comment |
| The audit trail — closes, label/assignee changes, commit refs, merges (no comment bodies) | `timeline <n>` | event list |
| The code change | `diff <n>` | unified diff |
| Is this PR ready to merge — mergeable + CI + reviews | `pr-status --pr <n>` | health view; exit code = merge-readiness |
| The review verdicts + inline review comments | `pr-reviews <n>` | review list |
| Need | Verb | Shape |
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The full human view of one item — title, body, **and** every comment in order | `view <n>` | prose dump; **marks the thread read** (clears the read-before-comment guard). Top-level `view` auto-detects issue-or-PR by number; scoped `issue view <n>` / `pr view <n>` do the same when you already know the kind |
| Just the structured metadata of one item (state, labels, assignees, head sha…) | `issue <n>` / `pr <n>` | JSON object |
| Only the comments (skip the body), e.g. to catch up a long thread; for PRs, review bodies (approve/request-changes/comment summaries) are merged in too | `comments <n>` [`--tail N`] | comment + review-body list; **marks read** |
| One specific comment by its id | `comment-show <id>` | single comment |
| The audit trail — closes, label/assignee changes, commit refs, merges (no comment bodies) | `timeline <n>` | event list |
| The code change | `diff <n>` | unified diff |
| Is this PR ready to merge — mergeable + CI + reviews | `pr-status --pr <n>` | health view; exit code = merge-readiness |
| The review verdicts + inline review comments | `pr-reviews <n>` | review list |
Rule of thumb: **`view` first** when you're about to act on a thread (it
shows everything and clears the unread guard so your follow-up `comment`
@ -172,7 +172,7 @@ hive-forge pr-status --pr 42
# last comment: 2026-06-05T19:13:28+02:00 by argus
```
Note: review verdicts come from *formal* Forgejo reviews (the
Note: review verdicts come from _formal_ Forgejo reviews (the
approve / request-changes API). Reviewers who post their verdict as a
plain comment show under `last comment`, not `reviews`.
@ -243,14 +243,14 @@ to discover valid label names before triaging or to audit the label set.
increment it). `--json` wraps the output.
- `ci-rerun` re-runs CI without pushing an empty commit (the old
retrigger path, which littered PR history). Forgejo has no token-usable
REST endpoint to re-run an *existing* run (the run-page rerun buttons
REST endpoint to re-run an _existing_ run (the run-page rerun buttons
are CSRF-gated web routes a token POST 404s), so this dispatches a
**fresh** run of the workflow via the workflow-dispatch API
(`POST …/actions/workflows/<workflow>/dispatches {"ref":"<branch>"}`).
Resolve the branch with exactly one of: `--pr <n>` (the PR's head
branch), `--run <n>` (the same run number `ci-log` / `artifact-get`
take — resolves the branch + workflow from that run), or `--branch
<name>` (directly). `--workflow <file>` picks the workflow file for
<name>` (directly). `--workflow <file>` picks the workflow file for
`--pr` / `--branch` (default `ci.yml`). Dispatch re-runs the whole
workflow — there is no single-job variant.
- Do NOT use raw `curl` for forge access -- the CLI handles auth,

View file

@ -76,13 +76,13 @@ agents after the approval resolves.
## Boundary summary
| Operation | Requires approval? | Scope |
|---|---|---|
| `kill` / `start` / `restart` / `update` | No | Direct children |
| `list_containers` | No | All descendants |
| `request_init_config` | Yes (InitConfig) | New direct child only |
| `request_apply_commit` | Yes (ApplyCommit) | Direct children |
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
| Operation | Requires approval? | Scope |
| --------------------------------------- | ------------------ | --------------------- |
| `kill` / `start` / `restart` / `update` | No | Direct children |
| `list_containers` | No | All descendants |
| `request_init_config` | Yes (InitConfig) | New direct child only |
| `request_apply_commit` | Yes (ApplyCommit) | Direct children |
| `request_update_meta_inputs` | Yes (MetaUpdate) | Meta flake (global) |
## See also

View file

@ -5,10 +5,10 @@ swap touches only the first:
1. **`colors.css`** — the 16 `--base00``--base0F` base16 slots. **This is
the entire theme swap contract.** A generator (e.g. one fed a stylix
base16 scheme, which is natively base00base0F) replaces *only this
file*.
base16 scheme, which is natively base00base0F) replaces _only this
file_.
2. **`theme.css`** — the semantic layer: `--bg`, `--fg`, `--purple`, …
*derived* from the base16 slots (`--bg: var(--base00)` etc.). This is
_derived_ from the base16 slots (`--bg: var(--base00)` etc.). This is
what the app references and it **never changes on a swap**.
Both files live in `frontend/packages/shared/src/`. Per-page stylesheets
@ -34,68 +34,73 @@ Each semantic var derives from a base16 slot (or, for the three that have
no clean slot, a `color-mix()` over base16 — pixel-identical under the
default palette). Default hexes shown are Catppuccin Mocha.
| Variable | Derives from | Default hex | Use |
|---|---|---|---|
| `--bg` | `base00` | `#1e1e2e` | page background |
| `--bg-elev` | `base01` | `#181825` | elevated surfaces: floating dropdowns, popovers |
| `--border` | `base02` | `#313244` | general borders, hover/active backgrounds |
| `--purple-dim` | `base03` | `#45475a` | subtle borders, terminal chrome, badge backgrounds |
| `--fg` | `base05` | `#cdd6f4` | primary text colour |
| `--red` | `base08` | `#f38ba8` | errors, fail state |
| `--amber` | `base09` | `#fab387` | warnings, pending / running state |
| `--yellow` | `base0A` | `#f9e2af` | flash messages, mild warnings |
| `--green` | `base0B` | `#a6e3a1` | success, ok state |
| `--cyan` | `base0C` | `#89dceb` | tool-use events, info accents |
| `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) |
| `--purple` | `base0E` | `#cba6f7` | accent — active tabs, links, highlights |
| `--pink` | `base0F` | `#f5c2e7` | thinking events |
| `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) |
| `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text |
| `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` |
| Variable | Derives from | Default hex | Use |
| -------------- | --------------------------- | ----------- | --------------------------------------------------------------- |
| `--bg` | `base00` | `#1e1e2e` | page background |
| `--bg-elev` | `base01` | `#181825` | elevated surfaces: floating dropdowns, popovers |
| `--border` | `base02` | `#313244` | general borders, hover/active backgrounds |
| `--purple-dim` | `base03` | `#45475a` | subtle borders, terminal chrome, badge backgrounds |
| `--fg` | `base05` | `#cdd6f4` | primary text colour |
| `--red` | `base08` | `#f38ba8` | errors, fail state |
| `--amber` | `base09` | `#fab387` | warnings, pending / running state |
| `--yellow` | `base0A` | `#f9e2af` | flash messages, mild warnings |
| `--green` | `base0B` | `#a6e3a1` | success, ok state |
| `--cyan` | `base0C` | `#89dceb` | tool-use events, info accents |
| `--blue` | `base0D` | `#89b4fa` | links, interactive accent (distinct from cyan) |
| `--purple` | `base0E` | `#cba6f7` | accent — active tabs, links, highlights |
| `--pink` | `base0F` | `#f5c2e7` | thinking events |
| `--crust` | `mix(base00 58%, #000)` | `#11111b` | terminal / code block background (below `--bg`) |
| `--muted` | `mix(base05 55.5%, base00)` | `#7f849c` | secondary / dimmed text |
| `--subtext0` | `mix(base05 77.7%, base00)` | `#a6adc8` | toolbar/status text; dimmer than `--fg`, lighter than `--muted` |
## Common mistakes
The Catppuccin colour names do **not** map 1:1 to the variable names.
Variables to avoid (undefined — they will silently resolve to transparent / inherited):
| Wrong | Correct |
|---|---|
| `--text` | `--fg` |
| `--mauve` | `--purple` |
| `--surface0` | `--bg-elev` (float bg) or `--border` (border/hover) |
| `--surface1` | `--border` |
| `--surface2` | `--purple-dim` |
| `--overlay0`, `--overlay1` | `--muted` |
| `--base`, `--mantle` | `--bg`, `--bg-elev` |
| Wrong | Correct |
| -------------------------- | --------------------------------------------------- |
| `--text` | `--fg` |
| `--mauve` | `--purple` |
| `--surface0` | `--bg-elev` (float bg) or `--border` (border/hover) |
| `--surface1` | `--border` |
| `--surface2` | `--purple-dim` |
| `--overlay0`, `--overlay1` | `--muted` |
| `--base`, `--mantle` | `--bg`, `--bg-elev` |
## Usage guide
**Floating menus and dropdowns** (e.g. agent context menu, tabbar overflow):
```css
background: var(--bg-elev);
border: 1px solid var(--purple-dim);
```
**Hover / active state backgrounds**:
```css
background: var(--border);
```
**Active tab text / accent elements**:
```css
color: var(--purple);
```
**Muted / meta text**:
```css
color: var(--muted);
```
**Error / warning / success badges**:
```css
color: var(--red); /* error */
color: var(--amber); /* warning / running */
color: var(--green); /* ok */
color: var(--red); /* error */
color: var(--amber); /* warning / running */
color: var(--green); /* ok */
```
## Theme swapping — the base16 contract
@ -104,31 +109,31 @@ color: var(--green); /* ok */
semantic names.** A theme generator (e.g. one reading a stylix base16
scheme) overrides only `colors.css`; the semantic layer in `theme.css`
derives everything else, so the whole UI re-themes with nothing else to
template or regenerate. The base16 slot → semantic mapping is *internal*
template or regenerate. The base16 slot → semantic mapping is _internal_
to `theme.css` (the "Derives from" column above) — a generator never
needs to know our var names, and `theme.css` + the page bundles stay
untouched.
`colors.css` base16 slot defaults (Catppuccin Mocha):
| Slot | Default | Standard base16 role | Mapped to |
|---|---|---|---|
| `base00` | `#1e1e2e` | default bg | `--bg`, (darkened) `--crust` |
| `base01` | `#181825` | lighter bg | `--bg-elev` |
| `base02` | `#313244` | selection/surface | `--border` |
| `base03` | `#45475a` | comments/dim surface | `--purple-dim` |
| `base04` | `#585b70` | dark foreground | *(unused; kept for completeness)* |
| `base05` | `#cdd6f4` | default foreground | `--fg`, (blended) `--muted`/`--subtext0` |
| `base06` | `#f5e0dc` | light foreground | *(unused)* |
| `base07` | `#b4befe` | lightest | *(unused)* |
| `base08` | `#f38ba8` | red | `--red` |
| `base09` | `#fab387` | orange | `--amber` |
| `base0A` | `#f9e2af` | yellow | `--yellow` |
| `base0B` | `#a6e3a1` | green | `--green` |
| `base0C` | `#89dceb` | cyan | `--cyan` (our *sky*; Catppuccin's `base0C` is teal) |
| `base0D` | `#89b4fa` | blue | `--blue` |
| `base0E` | `#cba6f7` | magenta | `--purple` |
| `base0F` | `#f5c2e7` | extra accent | `--pink` |
| Slot | Default | Standard base16 role | Mapped to |
| -------- | --------- | -------------------- | --------------------------------------------------- |
| `base00` | `#1e1e2e` | default bg | `--bg`, (darkened) `--crust` |
| `base01` | `#181825` | lighter bg | `--bg-elev` |
| `base02` | `#313244` | selection/surface | `--border` |
| `base03` | `#45475a` | comments/dim surface | `--purple-dim` |
| `base04` | `#585b70` | dark foreground | _(unused; kept for completeness)_ |
| `base05` | `#cdd6f4` | default foreground | `--fg`, (blended) `--muted`/`--subtext0` |
| `base06` | `#f5e0dc` | light foreground | _(unused)_ |
| `base07` | `#b4befe` | lightest | _(unused)_ |
| `base08` | `#f38ba8` | red | `--red` |
| `base09` | `#fab387` | orange | `--amber` |
| `base0A` | `#f9e2af` | yellow | `--yellow` |
| `base0B` | `#a6e3a1` | green | `--green` |
| `base0C` | `#89dceb` | cyan | `--cyan` (our _sky_; Catppuccin's `base0C` is teal) |
| `base0D` | `#89b4fa` | blue | `--blue` |
| `base0E` | `#cba6f7` | magenta | `--purple` |
| `base0F` | `#f5c2e7` | extra accent | `--pink` |
Notes for theme authors:

View file

@ -3,7 +3,6 @@
> Part of [Web UI](../web-ui.md). See also:
> [Dashboard layout](dashboard.md) · [Per-agent page](agent.md)
- `GET /``index.html` from the bundled frontend dist (see
`frontend/`). Both binaries' routers declare their dynamic
endpoints first and then `fallback_service(ServeDir::new(...))`
@ -169,8 +168,8 @@ subscribes to `streamUrl` (live tail). Both endpoints return
`.no-anim` class so they don't stagger in like live events. The
optional `streamFactory(url)` callback lets the dashboard hand
the factory a `SharedWorker`-backed `EventSource` facade (so
multiple tabs share one upstream connection — see *SSE
multiplexing* above); when omitted, the factory falls back to a
multiple tabs share one upstream connection — see _SSE
multiplexing_ above); when omitted, the factory falls back to a
plain `new EventSource(url)`.
**`linkify` (text-node based).** Bare `http(s)://` URLs in row
@ -298,10 +297,10 @@ without its last path segment.
That makes the page work under any prefix the agent ends up mounted
at without rebuilding the dist. The cases that matter:
| served at | `api/state` resolves to |
|---|---|
| `/` (own port, today's shape) | `/api/state` |
| `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` |
| served at | `api/state` resolves to |
| ------------------------------------------------ | ----------------------- |
| `/` (own port, today's shape) | `/api/state` |
| `/agent/iris/` (gateway-prefixed) | `/agent/iris/api/state` |
| `/agent/iris/stats` (subpage, no trailing slash) | `/agent/iris/api/state` |
The gateway upstream config strips the prefix before forwarding to
@ -316,4 +315,3 @@ the relative-path resolution stays correct: `static/app.js` from
replaced), not `/stats/static/app.js`. Adding a trailing slash to
those routes would break the resolution; either keep them
slash-less or use `<base href>` injection at serve time.