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:
parent
442efa76f2
commit
8406a45275
17 changed files with 254 additions and 230 deletions
|
|
@ -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'.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 base00–base0F) replaces *only this
|
||||
file*.
|
||||
base16 scheme, which is natively base00–base0F) 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:
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue