Watch
0
0
Fork
You've already forked hyperhive
0

Merge remote-tracking branch 'forge/main' into docs/networking-scheduler-pass

# Conflicts:
#	docs/networking/gateway.md
This commit is contained in:
atlas 2026-10-02 19:00:42 +02:00
commit f63ab954c9
57 changed files with 1042 additions and 1070 deletions

View file

@ -5,9 +5,9 @@ HTTPS, both authenticated by an operator-supplied personal access token
(PAT) — so it can run GitHub API calls and push commits without any manual
`gh auth login`.
Provisioning is UI-driven: paste a PAT into the agent's credentials tab
and it works. No per-agent nix declaration, no rebuild — hive-c0re
injects the token into the agent's state dir out of band.
Provisioning is UI-driven: link a PAT to the agent in the swarm UI and it
works. No per-agent nix declaration, no rebuild — the agent fetches the token
from the swarm secret store itself.
## Enabling
@ -15,8 +15,7 @@ injects the token into the agent's state dir out of band.
The integration is **on by default** for every agent (`services.hyperhive.agent.github.enable
= true`), inert until the operator provisions a PAT. No per-agent declaration is
needed — an agent gains GitHub by having a PAT written to its token
file.
needed — an agent gains GitHub by having a PAT stored for it.
<!-- vale write-good.Passive = YES -->
@ -27,29 +26,31 @@ services.hyperhive.github.enable = false;
```
hive-c0re's meta-flake renderer then injects `services.hyperhive.agent.github.enable = false`
into every agent, so no agent ships the `gh` wrapper or credential helper.
into every agent, so no agent ships the `gh` wrapper, the credential helper
or the token fetch.
(`services.hyperhive.agent.github.enable` also exists per-agent for completeness, but the
hive-wide host switch is the intended control.)
github.com only. The token **value** never touches nix — it's written to
`<state>/github-token` separately (see [Provisioning](#provisioning)).
github.com only. The token **value** never touches nix — the agent writes it
to `<state>/github-token` at runtime (see [Provisioning](#provisioning)).
## Provisioning
The PAT is operator-supplied. The primary path is the **dashboard
credentials tab** (github sub-tab): paste the PAT for an agent and submit
(`POST /api/github-account`). A CLI path also exists for
recovery/scripting:
The PAT is operator-supplied. In the [swarm UI](../swarm/ui.md#linking-external-accounts),
open the agent on `/agents`, choose **link github account** and paste the PAT
(`PUT /api/hives/{hive}/agents/{agent}/github-account`). swarm-controller
stores it at `swarm/agents/<agent>/github-token` in the swarm secret store;
no hive writes it. One token per agent: linking again replaces it,
and no route hands it back.
```sh
hivectl github set-token <agent> --token-stdin # paste the PAT on stdin (preferred)
hivectl github set-token <agent> --token <pat> # inline (visible in shell history)
```
Either path has hive-c0re delegate the write to hive-priv, which stores the
file `0600` owned by the agent (so the container can read it) — the same
credential-injection path as forge/matrix tokens. See
[hivectl → GitHub](../tools/hivectl.md#github).
The agent's `hive-agent-github-token` unit reads that path under the
agent's own store certificate and writes `<state>/github-token` (`0600`,
owned by the agent), on boot and every two minutes, replacing the file only
when the token changed. It needs a store identity
(`services.hyperhive.agent.bao.addr`); an agent without one gets no token.
The unit never deletes the file: a `github-token` already in place stays
when the store holds none or doesn't answer. Where the token lives and who
reads it: [credentials.md](../swarm/credentials.md).
## Security

View file

@ -14,7 +14,7 @@ You rarely switch it on yourself. `gateway.enable` defaults to off, and every mo
| --- | --- | --- |
| `<swarm>/` | swarm-ui dist (static), behind an authelia subrequest | `swarm-ui.nix`, `deploy.swarm-ui.enable` |
| `auth.<swarm>/` | authelia (`9091`) | `swarm-authelia.nix`, `deploy.authelia.enable` |
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.behindGateway` |
| `forge.<swarm>/` | forgejo (`3000`) | `hive-forge/`, `deploy.forgejo.enable` |
| `chat.<swarm>/_matrix/*` | tuwunel (`8008`) | `hive-matrix.nix`, `swarm.matrix.gatewayHost != null` |
| `chat.<swarm>/` | fluffychat-web static (404 with the GUI off) | `hive-matrix.nix`, `deploy.matrix.gui.enable` |
| `chat.<swarm>/config.json` | inline JSON (FluffyChat boot config) | `hive-matrix.nix`, `deploy.matrix.gui.enable` |
@ -170,7 +170,7 @@ services.hyperhive.deploy.forgejo.openFirewall = false; # default
`sshPort` serves `git clone/push/pull` over SSH (`git@<domain>:owner/repo.git` with `-p 2222`). SSH goes straight to Forgejo, not through nginx.
`openFirewall` (default `false`) opens `httpPort` and `sshPort` on the host. Agents don't need it — they come through the gateway. Set it for a browser reaching `http://<host>:<httpPort>/` directly, or for external git clients pushing over SSH. The forge vhost behind the gateway (`deploy.forgejo.behindGateway`, default `true`) needs only the gateway's own `openFirewall`.
`openFirewall` (default `false`) opens `httpPort` and `sshPort` on the host. Agents don't need it — they come through the gateway. Set it for a browser reaching `http://<host>:<httpPort>/` directly, or for external git clients pushing over SSH. Forge's gateway vhost doesn't need `openFirewall` — the gateway's own `openFirewall` option covers that path.
### `rootUrl` override
@ -178,14 +178,7 @@ services.hyperhive.deploy.forgejo.openFirewall = false; # default
services.hyperhive.swarm.forge.rootUrl = "https://forge.example.com/";
```
`rootUrl` (default `null`) overrides the Forgejo `ROOT_URL` derived from `forge.domain` and the gateway:
| Shape | Derived `ROOT_URL` |
|---|---|
| `deploy.forgejo.behindGateway = true` | `https://<forge.domain>/` (no port suffix when `gateway.httpsPort == 443`) |
| `deploy.forgejo.behindGateway = false` | `http://<forge.domain>:<httpPort>/` |
Set it when `forge.domain` differs from the public URL, or for a bespoke shape such as an external reverse proxy on another host or path. It must end with `/` (an assertion enforces this).
`rootUrl` (default `null`) overrides the Forgejo `ROOT_URL` that's autoderived as `https://<forge.domain>/`, with `:<gateway.httpsPort>` appended when that port isn't 443. The gateway always terminates TLS, so the forge is always advertised over `https://`. Set `rootUrl` explicitly when `forge.domain` differs from the public URL, or for a bespoke shape such as an external reverse proxy on another host or path. It must end with `/` (an assertion enforces this).
## Security headers
@ -218,7 +211,7 @@ When enabled, every vhost adds `Strict-Transport-Security: max-age=…[; include
`services.hyperhive.gateway.localHostsEntry = true` maps to `127.0.0.1` in the host's `/etc/hosts`:
- the hive domain;
- every name a module on this host contributes to `gateway.localNames` — each swarm service this host runs adds its own (`forge.<swarm>` when behind the gateway, `chat.<swarm>`, `auth.<swarm>`, the swarm UI's apex, …).
- every name a module on this host contributes to `gateway.localNames` — each swarm service this host runs adds its own (`forge.<swarm>`, `chat.<swarm>`, `auth.<swarm>`, the swarm UI's apex, …).
`services.hyperhive.deploy.singleHostSwarm` turns it on. Leave it off with real DNS.

View file

@ -119,7 +119,7 @@ build can't hold the runner's single slot indefinitely).
## Container design
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It can't reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI. Requires `deploy.forgejo.behindGateway = true`.
- **Private netns, bridge-attached**: the container runs in its own network namespace (`privateNetwork = true`, `hostBridge`) and reaches hive-forge through the gateway at `http://<forge.domain>` (resolved to the bridge IP via `networking.extraHosts`). It can't reach host-loopback services — the core dashboard at `127.0.0.1:7000` and the raw forge port are unreachable from CI.
- **Non-ephemeral**: runner credentials persist across restarts (written to container's stateDir on first registration, reused thereafter).
- **Sandbox fallback**: nspawn containers can't create user-namespaces, so nix's sandboxing would always fail. Module sets `nix.settings.sandbox-fallback = true` in the container — nix builds run unsandboxed (safe because the container is already isolated). See `docs/process/gotchas.md`.
- **Credential isolation**: the forge admin token (`forge-core-token`) never enters the container. hive-c0re holds it and performs all forge API calls (runner validation + registration-token mint, in `forge/ci_runner.rs`); via hive-priv it writes only the runner registration token to the host env-file `/run/hive-ci/runner-token`, which the container bind-mounts read-only.

View file

@ -42,10 +42,6 @@ to the store under its own name, pulling what it needs when it needs it. No
process reads a secret on another principal's behalf: the principal that
needs a value is the principal that authenticates for it.
One per-agent credential file — the github token — sits outside this page:
it's operator-supplied and never passes through the store, so the table
below doesn't govern it.
**Per secret, the target specifies minter, reader, and renewal strategy.**
Those three are the contract, and the reader is a process pulling a store
path at runtime — not a path on disk, and not a unit whose job is to turn a
@ -69,6 +65,7 @@ of the cell says how.
| `swarm/agents/<agent>/queue` | `swarm-controller`, at agent creation | `hive-agent` in the agent container, under the agent's own certificate, held in memory — the identity it presents to the swarm queue, naming that one agent rather than its hive | ✅ `swarm-controller`'s five-minute pass re-mints a live agent's secret once it's 45 days old by `minted_at` on the stored object; a secret with no `minted_at` gets one stamped, value unchanged. The pass skips agents declared `Destroyed` — declaring an agent destroyed deletes every version of the path instead, the undo of the mint rather than another one | ✅ `hive-agent` reads the path before its first connect and again on every reconnect attempt, so a reconnect after a re-mint presents the new secret. An open connection keeps the secret it connected with; after a revocation the agent keeps retrying under the queue client's backoff |
| `swarm/agents/<agent>/forge-token` | `swarm-controller`, at agent creation and in a pass every 5 minutes over every agent with a store identity | the agent container itself, under its own certificate, fetched to `/run/hive-agent-forge-token/token` | ✅ the controller re-mints when the stored token is missing or no longer matches the forge (last eight characters and scopes) | ✅ the agent re-fetches on a 10-minute timer |
| `swarm/agents/<agent>/forge/<label>` | `swarm-controller`, when an operator links an external forge account in the swarm UI | `hive-agent-forge-accounts` in the agent container, under the agent's own certificate, into `<state>/forge-<label>-token` and `forge-<label>.json` | ❌ an operator's token; replaced only by linking the label again | ✅ the agent re-fetches on a 2-minute timer |
| `swarm/agents/<agent>/github-token` | `swarm-controller`, when an operator links a GitHub account in the swarm UI | `hive-agent-github-token` in the agent container, under the agent's own certificate, into `<state>/github-token` | ❌ an operator's token; replaced only by linking it again | ✅ the agent re-fetches on a 2-minute timer |
| `swarm/hives/<hive>/matrix/appservice-token` | one minter, on the authelia host | the hive process that presents the token to its homeserver, under the hive's own certificate | must be stated | must be stated |
| `swarm/hives/<hive>/matrix/sender-token` | `swarm-controller`, with the swarm's appservice token, for every hive in its directory in a five-minute pass | `swarm-controller` under its own certificate, before it decides whether to mint, and hive-c0re's `stored_sender_token()`, under the hive's own certificate | ✅ the controller's pass re-mints when the stored token is missing, unknown to the homeserver, or someone else's | ✅ hive-c0re's matrix sweep reads the store every run and overwrites its token file when the store's token differs |
| `swarm/hives/<hive>/queue/agent` | authelia | `swarm-bao-queue-agent` on the hive's host, under its own per-hive certificate; no agent's policy reaches it | must be stated | must be stated |

View file

@ -7,13 +7,13 @@ domain, covers host-level detail for one hive.
## What it shows
| route | what |
| ------------------------- | ------------------------------------------------------------------------------------------- |
| `/` | the hive directory, each hive with its last reported status |
| `/agents` | every agent: status, config PR, wanted state; create agents, link forge and matrix accounts |
| `/agents/<name>/terminal` | one agent's live terminal |
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
| `/issues` | a cross-repo issue report |
| route | what |
| ------------------------- | --------------------------------------------------------------------------------------------------- |
| `/` | the hive directory, each hive with its last reported status |
| `/agents` | every agent: status, config PR, wanted state; create agents, link forge, matrix and GitHub accounts |
| `/agents/<name>/terminal` | one agent's live terminal |
| `/jobs` | the controller's job graph — where agent creation and credential mints show progress |
| `/issues` | a cross-repo issue report |
Everything it shows comes from [`swarm-controller`](../../swarm-controller/README.md).
An agent created here or with `swarmctl agent create` starts `paused`; set it
@ -21,8 +21,8 @@ An agent created here or with `swarmctl agent create` starts `paused`; set it
### Linking external accounts
Each agent on `/agents` opens two dialogs that write a credential for it
into the swarm secret store through swarm-controller. Both are blind
Each agent on `/agents` opens three dialogs that write a credential for it
into the swarm secret store through swarm-controller. All three are blind
set/update actions: no route lists linked accounts or hands a token back.
- **link a matrix account** — `PUT /api/hives/{hive}/agents/{agent}/matrix-accounts/{account}`,
@ -36,6 +36,14 @@ set/update actions: no route lists linked accounts or hands a token back.
`hive-forge -f <label>` reads. The unit never deletes a pair: linking
the same label again overwrites both files, and a pair whose label the
store doesn't list stays untouched.
- **link a github account** — `PUT /api/hives/{hive}/agents/{agent}/github-account`
with a personal access token, stored at `swarm/agents/<agent>/github-token`.
One token per agent: linking again replaces it. The agent's
`hive-agent-github-token` unit fetches it into `<state>/github-token`,
the file its `gh` wrapper, git credential helper and GitHub notification
poller read. A `github-token` already in place stays when the store holds
none. What the token needs and how the agent uses it:
[GitHub accounts](../integrations/github.md).
Where each credential lives and who reads it:
[`credentials.md`](credentials.md).

View file

@ -9,8 +9,6 @@ This document contains the help content for the `hivectl` command-line program.
* [`hivectl forge reconcile-config`↴](#hivectl-forge-reconcile-config)
* [`hivectl matrix`↴](#hivectl-matrix)
* [`hivectl matrix invite`↴](#hivectl-matrix-invite)
* [`hivectl github`↴](#hivectl-github)
* [`hivectl github set-token`↴](#hivectl-github-set-token)
* [`hivectl gateway`↴](#hivectl-gateway)
* [`hivectl gateway create-user`↴](#hivectl-gateway-create-user)
* [`hivectl gateway delete-user`↴](#hivectl-gateway-delete-user)
@ -64,7 +62,6 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
* `forge` — Reconcile an agent's config between this hive and the forge
* `matrix` — matrix-tuwunel invites
* `github` — GitHub account provisioning
* `gateway` — Gateway htpasswd user management
* `agent` — Lifecycle actions on ONE managed agent container. Needs the hive-c0re daemon running
* `list-agents` — Show all managed agents with their status and technical state
@ -156,39 +153,6 @@ Invite a matrix user to the hive Space, or a specific room with `--room`. Idempo
## `hivectl github`
GitHub account provisioning.
Store an operator-supplied personal access token (PAT) for an agent so its `gh` and git can authenticate. Creates no account — the PAT is for an existing GitHub account.
**Usage:** `hivectl github <COMMAND>`
###### **Subcommands:**
* `set-token` — Store a GitHub PAT for `<agent>` so its `gh` and git can authenticate
## `hivectl github set-token`
Store a GitHub PAT for `<agent>` so its `gh` and git can authenticate.
Prefer `--token-stdin` — an inline `--token` is visible in shell history.
**Usage:** `hivectl github set-token [OPTIONS] <AGENT>`
###### **Arguments:**
* `<AGENT>` — Logical agent name (the container/agent name)
###### **Options:**
* `--token <TOKEN>` — The PAT value inline. Mutually exclusive with `--token-stdin`
* `--token-stdin` — Read the PAT from stdin (trailing newline stripped). Mutually exclusive with `--token`
## `hivectl gateway`
Gateway htpasswd user management.

View file

@ -65,30 +65,6 @@ group; nobody has built that sync yet.
invite power (it owns the hive Space, so that case always works).
Idempotent — already-member / already-invited is a no-op.
## GitHub
<!-- vale write-good.Passive = NO -->
Write an operator-supplied GitHub personal access token (PAT) into an
agent's token file so its `gh` wrapper + git credential helper can act as
the bot account. Unlike matrix there is no account creation — the PAT
is for an existing GitHub account. A CLI alternative to the dashboard
credentials tab; the [GitHub integration](../integrations/github.md) is on by default
(`services.hyperhive.agent.github.enable`), so no per-agent config is needed.
<!-- vale write-good.Passive = YES -->
```bash
hivectl github set-token damocles --token-stdin # paste the PAT on stdin (preferred)
hivectl github set-token damocles --token <pat> # inline (visible in shell history)
```
- `set-token`: writes `<state>/github-token` (`0600`, agent-owned) via
hive-priv — the same credential-injection path as forge/matrix tokens.
The `gh` wrapper / git credential helper read it live, so a freshly set
or rotated PAT takes effect with no rebuild or restart. Refuses an empty
token. See [github.md](../integrations/github.md) for the full flow + security notes.
## Gateway
Manage users in the gateway's HTTP Basic auth htpasswd file
@ -296,6 +272,6 @@ note, not an error.
A surface has no URL when it isn't browser-reachable: `home` needs
`services.hyperhive.domain`; `forge` needs
`services.hyperhive.deploy.forgejo.behindGateway = true`; `matrix` needs
`services.hyperhive.swarm.forge.publicUrl` (set by default); `matrix` needs
`services.hyperhive.deploy.matrix.gui.enable = true`. In those cases the command
exits with a hint naming the option to set.

View file

@ -295,7 +295,6 @@ known operations; there is no arbitrary command pass-through:
| `ControlInfraContainer` | `systemctl <action> container@<container>.service` — the `InfraContainer` enum is the allowlist, and serde rejects unknown names at the wire boundary (`hive-c0re` has no variant, so no request can name it) |
| `SyncAgentTmpfiles` | unlink `/etc/tmpfiles.d/hyperhive-agents.conf` and return `Ok` |
| `SetAgentPaused` | create / remove the `<state>/<name>/harness/paused` marker that parks an agent's turn loop |
| `WriteAgentGithubToken` | write `0600` `github-token` into agent state dir |
| `RegisterCiRunner` | write `/run/hive-ci/runner-token` (host path, root-owned) then `systemctl --machine=hive-ci restart gitea-runner-hive.service`. Only the registration token crosses; the forge admin token never enters the container |
| `EnsureAgentSubvolume` | `btrfs subvolume create <state>/<name>` for a new agent — no-op when the path exists or the filesystem isn't btrfs |
| `UpgradeAgentSubvolume` | migrate an existing plain state dir into a subvolume: create, `cp -a --reflink=auto`, atomic swap. Operator opt-in, and the caller stops the agent first |

View file

@ -12,7 +12,7 @@ what you actually do here.
Everything starts at the **H0M3 hub**, served at `/` — a grid of tiles
linking to every surface (Dashboard, Flow, Logs, Builds, Stats,
Settings, Core, Credentials). Every page links back to H0M3, so you're
Settings, Core). Every page links back to H0M3, so you're
never more than one step from the hub.
The **dashboard** itself (`/dashboard.html`) is where you'll spend most
@ -29,11 +29,8 @@ Everything else lives on its own page instead, all reachable from the
H0M3 hub: **Flow** (`/flow.html`, the raw live message stream across
the whole swarm), **Logs** (`/logs.html`, per-agent and host
journals), **Stats** (`/stats.html`, swarm-wide usage stats),
**Builds** (`/builds.html`, the rebuild queue and build history),
**Core** (`/core.html`, tombstones and container resource use), and
**Credentials** (`/credentials.html`, pasting a GitHub PAT for an
agent's dedicated bot account — matrix and external-forge accounts are
swarm identities, linked from the swarm UI). Your local
**Builds** (`/builds.html`, the rebuild queue and build history), and
**Core** (`/core.html`, tombstones and container resource use). Your local
browser preferences (notifications) live in the dashboard's Y3R C4LL
tab.
@ -83,11 +80,6 @@ journald view for any agent + service; the per-agent `⋮` menu's
recurring or one-shot prompts you schedule for one or more agents, and
reminders agents have set for themselves.
**Set an agent's GitHub token.** The Credentials page pastes a PAT for
the agent's dedicated bot account, without editing that agent's config
repo. Link matrix and external-forge accounts from the swarm UI
instead.
## More depth
Both the dashboard and the per-agent pages are SPAs sharing one

View file

@ -30,7 +30,7 @@ from the dashboard tab strip.
— sits below the tab strip.
- **Server-warnings banner** — a generic, sticky top-of-page strip shown
on **every** page (dashboard + every stand-alone page — FL0W, L0GS,
H0M3, C0R3, BU1LDS, CR3D3NTIALS, ST4TS), injected at the top
H0M3, C0R3, BU1LDS, ST4TS), injected at the top
of `<body>` by `renderServerWarnings` in `common.js`. Driven by
`state.server_warnings` — a list of `{ kind, level, message }` — and
coloured by `level` (`warn` amber / `crit` red). hive-c0re owns the
@ -283,35 +283,6 @@ badge with a ticking elapsed-time chip; expanding streams output via
(suspends on manual scroll-up). Deep-link: `?id=N#buildlogs` opens the
entry with that id pre-expanded.
## CR3D3NTIALS page (`/credentials.html`)
Operator surface to provision per-agent credentials without editing the
agent's config repo. Standalone page reached from the **Credentials** tile
on the H0M3 hub, same minimal chrome as `/logs.html` (a `← home` back-link
+ a sub-tab strip, via the shared `@hive/shared/tabs.js` tab strip) rather
than `/core.html`'s plain title. Its own esbuild bundle
(`credentials.js`); no SSE — it reads `/api/state` once for the (shared)
agent picker and otherwise works off purpose-built endpoints per tab.
One sub-tab, GITHUB.
### GITHUB tab
Provision a single per-agent GitHub personal access token (see
[`docs/integrations/github.md`](../integrations/github.md) for the injection + `gh`/git-push
mechanics). No login flow — the operator pastes an existing PAT for a
dedicated bot account, with a security-warning banner (dedicated account +
minimally scoped token) and a link to
[github.com/settings/tokens](https://github.com/settings/tokens).
Status reads `GET /api/github-account?agent=<name>` →
`{ present: bool }` — whether the agent's `github-token` file exists.
There's no live/heartbeat concept for a static PAT, so this is just a
"token stored ✓" / "not set" line.
Provisioning posts `POST /api/github-account` (form-encoded `agent`,
`token`) → `200 { ok: true }` on success; failures come back as RFC 9457
`application/problem+json` with the message in `detail`. The token is never
echoed back in either direction.
## P3RM1SS10NS tab
Per-agent permission configuration. Two sections, each rendered as a
@ -575,7 +546,7 @@ re-renders the terminal row. The operator addresses the root agent as `@root`.
The H0M3 hub is the primary landing page (served at `/` by default). A
responsive grid of link tiles — Dashboard, Flow, Logs, Builds, Stats,
Core, Credentials, API — each pointing to their respective
Core, API — each pointing to their respective
surfaces, all unconditionally shown (no gating). The page is a pure
portal with no tab-bar or SSE subscriptions. Typography + colours inherit
from the shared theme (Catppuccin Mocha via `common.css` + `theme.css`).

View file

@ -20,8 +20,8 @@
CSS bundle per page (`colors.css` + `theme.css` + `common.css`,
loaded by every page, plus a page-specific bundle —
`dashboard.css` / `flow.css` / `logs.css` / `home.css` /
`stats.css` / `core.css` / `builds.css` /
`credentials.css`); `common.css` inlines `@hive/shared`'s
`stats.css` / `core.css` / `builds.css`);
`common.css` inlines `@hive/shared`'s
`base.css` + `terminal.css` + `tabs.css` + `chrome.css` +
`pill.css` via esbuild's `@import` resolution.
`terminal.js` exports `{ create, linkify }` as ES module