docs: the handbook follows the three layers: swarm, hives, agents
The handbook's sections were agent-lifecycle, integrations, swarm, tools, trust-boundary and web-ui, which do not follow how the system is built, so a reader could not tell which layer a page is about. Its pages now sit in swarm/, hives/ and agents/, with the accounts an agent can hold outside the system in external-accounts/. - swarm/: forge, matrix and the knowledge repo join the swarm pages, and so does the swarmctl reference. - hives/: the dashboard, hivectl and its reference, and the operator/agent boundary, with a README that takes over the old web-ui and tools pages. - agents/: the roster, persistence, config changes, the agent's own web page (was web-ui/agent) and the security model, with a README. - external-accounts/: GitHub and GitLab. Pages are moved whole; none is split yet. Every reference follows: relative links in the docs, `docs/2-handbook/...` paths in code comments, the nix checks that regenerate the CLI references and the ignore list, and docs/README.md, whose handbook section is now by layer.
This commit is contained in:
parent
263b21021a
commit
0e84fe8bc4
3 changed files with 393 additions and 363 deletions
12
docs/2-handbook/external-accounts/README.md
Normal file
12
docs/2-handbook/external-accounts/README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# External accounts
|
||||
|
||||
An agent can hold an account on a service outside the swarm, so it can push
|
||||
code or read notifications there as itself. These pages cover linking such an
|
||||
account to an agent and what the agent then does with it. They sit outside the
|
||||
swarm, the hives and the agents themselves; the swarm's own forge and
|
||||
homeserver are in [`swarm/`](../swarm/README.md).
|
||||
|
||||
- **[GitHub](github.md)** — give an agent a GitHub account: `gh`,
|
||||
`git push` and the notification poller, and how the token reaches it.
|
||||
- **[GitLab](gitlab.md)** — the same for one or more GitLab instances
|
||||
(`glab` and `git push`).
|
||||
153
docs/2-handbook/external-accounts/github.md
Normal file
153
docs/2-handbook/external-accounts/github.md
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
# GitHub accounts
|
||||
|
||||
Give an agent a managed GitHub identity — a `gh` CLI and `git push` over
|
||||
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: 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
|
||||
|
||||
<!-- vale write-good.Passive = NO -->
|
||||
|
||||
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 stored for it.
|
||||
|
||||
<!-- vale write-good.Passive = YES -->
|
||||
|
||||
To turn it off for the whole swarm, set the swarm option:
|
||||
|
||||
```nix
|
||||
services.hyperhive.swarm.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, the credential helper
|
||||
or the token fetch.
|
||||
(`services.hyperhive.agent.github.enable` also exists per-agent for completeness, but the
|
||||
swarm-wide switch is the intended control.)
|
||||
|
||||
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. 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: swarm-controller refuses to link
|
||||
another until you delete the stored one, and no route hands it back.
|
||||
|
||||
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-reference.md](../swarm/credentials-reference.md).
|
||||
|
||||
## Security
|
||||
|
||||
- Use a **dedicated bot account**, never a human's.
|
||||
- Mint a **minimally scoped PAT** — only the repos/scopes the agent's
|
||||
workflow needs. Agents have passwordless sudo, so a compromised or
|
||||
hallucinating agent can act as the account within the token's scopes;
|
||||
scope is the real blast-radius limiter, and the container boundary is
|
||||
the enforcement. See [security.md](../agents/security.md).
|
||||
|
||||
## Implementation
|
||||
|
||||
<details>
|
||||
<summary>How the agent's `gh`/`git push` actually authenticate, and how
|
||||
the notification poller works</summary>
|
||||
|
||||
### How the agent uses it
|
||||
|
||||
When enabled, the container gets:
|
||||
|
||||
- **A `gh` wrapper** on `PATH` (shadowing the raw `gh`) that exports
|
||||
`GH_TOKEN` from the token file at invocation, then execs real `gh`. So
|
||||
`gh pr create`, `gh api …`, etc. just work — `gh` derives the identity
|
||||
from the token.
|
||||
- **A git credential helper** (`git-credential-hive-github`), wired via a
|
||||
host-scoped `/etc/gitconfig` entry for `https://github.com`, so
|
||||
`git push https://github.com/<owner>/<repo>` authenticates as
|
||||
`x-access-token` + the PAT (GitHub ignores the username for PAT auth).
|
||||
Host-scoped, so it never touches the forge (`localhost:3000`) or any
|
||||
other remote.
|
||||
|
||||
Both scripts read the token from `<state>/github-token` **at invocation
|
||||
time**, so a PAT written (or rotated) mid-session takes effect immediately
|
||||
— no container rebuild or restart. Until the file exists, `gh` / `git push`
|
||||
fail unauthenticated. The build bakes the token path into the scripts
|
||||
(rather than reading it from an env var), because claude's Bash tool runs in a
|
||||
minimal environment that wouldn't carry one.
|
||||
|
||||
### Notifications
|
||||
|
||||
`hive-github-notify` polls github.com for the agent, turning each unread
|
||||
notification thread into a todo. it's a **separate binary and a
|
||||
separate systemd unit** from the internal forge's poller
|
||||
(`hive-forge-notify`, see [forge.md](../swarm/forge.md#notification-poller-hive-forge-notifysrcnotifyrs)),
|
||||
installed by `nix/agent-modules/github.nix` under
|
||||
`services.hyperhive.agent.github.enable`. Both binaries ship from the one
|
||||
`hive-forge-notify` derivation, so the unit is a second `ExecStart`
|
||||
path, not a new package.
|
||||
|
||||
Two units rather than one daemon with two loops, because it puts the
|
||||
decision in nix: a hive built without this module has **no github poller
|
||||
in its closure at all**, which is what makes GitHub access separable
|
||||
rather than merely switched off. it's also why this isn't a cargo
|
||||
feature — a feature would unify across the workspace and cost every
|
||||
crate its build cache.
|
||||
|
||||
At runtime the poller needs the PAT above. No PAT, no polling: the unit
|
||||
logs why and exits 0, which is why it's `Restart = on-failure` and
|
||||
never `always` — a clean exit on a PAT-less agent must not become a
|
||||
restart loop. The poller waits about ten minutes for the PAT to appear
|
||||
before it gives up. A path unit (`hive-github-notify.path`) starts the poller
|
||||
again when `<state>/github-token` changes, so a PAT that lands later still
|
||||
enables polling. Once polling, it re-reads the file on every
|
||||
tick, so it picks up a rotated PAT on the next tick without a restart.
|
||||
|
||||
Forgejo models its notifications API on GitHub's, so one tolerant
|
||||
parse serves both: `id`, `repository.full_name`,
|
||||
`subject {title,url,latest_comment_url}` and `updated_at` line up field
|
||||
for field. Lenient deserializers absorb the two real differences —
|
||||
GitHub sends the thread id as a _string_ where Forgejo
|
||||
sends a number, and says `PullRequest` where Forgejo says `Pull`.
|
||||
`hive-github-notify` prefixes todo keys `gh:<id>` so a github thread id can't collide with a
|
||||
forge one.
|
||||
|
||||
Two host differences worth knowing before touching this code:
|
||||
|
||||
- **Auth scheme, not just value.** Forgejo takes
|
||||
`Authorization: token <t>`; GitHub wants `Bearer <t>` plus `Accept:
|
||||
application/vnd.github+json`, `X-GitHub-Api-Version` and a
|
||||
`User-Agent`. Sending Forgejo's form to GitHub doesn't error — it
|
||||
authenticates as _nobody_ and silently drops to the unauthenticated
|
||||
rate limit. The cheap way to tell the two apart is the rate-limit
|
||||
header: `x-ratelimit-remaining` near 5000 is an authenticated user,
|
||||
near 60 is anonymous.
|
||||
- **GitHub sets the cadence.** It returns `X-Poll-Interval` (60s in
|
||||
practice, slower than the poller's own tick) and rate-limits callers who
|
||||
ignore it, so the loop re-arms to the server's interval whenever that's
|
||||
_slower_ than the poller's. A hint faster than the poller's own tick
|
||||
isn't a reason to poll harder.
|
||||
|
||||
⚠️ **This needs the `notifications` scope on the PAT.** A token minted
|
||||
for `gh` + `git push` typically carries `repo` only, which is enough to
|
||||
push and open PRs but **not** to read the notification stream (nor to
|
||||
mark a thread read, which is the same scope). A PAT without it doesn't
|
||||
break anything: the poller logs the refusal and stays quiet, and the
|
||||
agent never gets GitHub wakes. If an agent's GitHub
|
||||
notifications never arrive, check the token's scopes first — the
|
||||
symptom is silence, not an error.
|
||||
|
||||
</details>
|
||||
91
docs/2-handbook/external-accounts/gitlab.md
Normal file
91
docs/2-handbook/external-accounts/gitlab.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
# GitLab accounts
|
||||
|
||||
Give an agent managed accounts on one or more GitLab instances: a `glab` CLI
|
||||
and `git push` over HTTPS, both authenticated by operator-supplied access
|
||||
tokens, with no manual `glab auth login`.
|
||||
|
||||
An agent can hold any number of accounts, each under a label you choose. Two labels
|
||||
can name two instances, such as `gitlab.com` and a self-hosted one. Provisioning
|
||||
needs no per-agent nix declaration and no rebuild: the agent fetches its tokens
|
||||
from the swarm secret store itself.
|
||||
|
||||
## Linking an account
|
||||
|
||||
In the swarm UI, open an agent, choose **link account**, then **gitlab
|
||||
account**. Fill in a label, the instance URL (it starts as `https://gitlab.com`)
|
||||
and the token. The form shows the recommended scopes and the controller's
|
||||
answer if it refuses the input. The linked account appears in the agent's list
|
||||
of [linked accounts](../swarm/ui.md#linking-external-accounts) with a delete
|
||||
action. The UI calls the controller's API, which you can also call directly:
|
||||
|
||||
```text
|
||||
PUT /api/hives/{hive}/agents/{agent}/gitlab-accounts/{label}
|
||||
{"url": "https://gitlab.example.org", "token": "<access token>"}
|
||||
```
|
||||
|
||||
- `label` is lowercase letters, digits and hyphens. It names the account in the
|
||||
store and in the account list.
|
||||
- `url` is the instance's base URL. The host must be a DNS name: an IP
|
||||
address, `localhost` and a single-label name get a 400, because the agent
|
||||
reaches the instance through its hive's egress proxy, which takes names
|
||||
only. A port and a subfolder (`https://example.org:8443/gitlab`) are part of
|
||||
the address. The path may hold letters, digits and `. _ ~ / -`; a URL with
|
||||
user information, a query, a fragment or any other character gets a 400, so
|
||||
every stored account is one the agent uses.
|
||||
- A plain `http://` URL works, and then the token crosses the network in
|
||||
cleartext. Use `https://` unless the instance sits on a trusted network.
|
||||
- `token` is a GitLab personal, group or project access token. Scope it
|
||||
`api` and `write_repository`: `glab` needs the first, and `git push` over
|
||||
HTTPS needs the second. Use a dedicated bot account and the narrowest token
|
||||
that does the agent's work, because the agent has passwordless sudo and can
|
||||
act within the token's scopes.
|
||||
|
||||
A label that holds an account already answers 409; delete it first
|
||||
(`DELETE .../gitlab-accounts/{label}`), then link again. No route hands a
|
||||
token back. `GET /api/hives/{hive}/agents/{agent}/linked-accounts` lists the
|
||||
account as kind `gitlab`, with its label and URL.
|
||||
|
||||
Linking also allows the instance's host in the agent's egress, as a
|
||||
`link:gitlab:<label>` entry on the swarm's allowed-domains list. swarm-controller
|
||||
derives the host from the URL it stored, so nothing the agent writes can widen
|
||||
its egress. Deleting the account removes the entry.
|
||||
|
||||
## What the agent gets
|
||||
|
||||
An agent with a store identity (`services.hyperhive.agent.bao.addr`) has:
|
||||
|
||||
- the `glab` command, which reads its configuration from `<state>/glab/`;
|
||||
- a git credential helper, `git-credential-hive-gitlab`, for each linked
|
||||
instance;
|
||||
- the `hive-agent-gitlab-accounts` unit and its timer.
|
||||
|
||||
The unit logs in to the store with the agent's own certificate, lists
|
||||
`swarm/agents/<agent>/gitlab/`, reads each account, and writes two files in
|
||||
`<state>/glab/` (mode `0600`) on boot and every two minutes, replacing a file
|
||||
only when its content changed:
|
||||
|
||||
- `config.yml`, `glab`'s own configuration with one `hosts` entry per instance,
|
||||
keyed by host and port, holding the token, the API protocol and the
|
||||
subfolder. It sets `git_protocol: https`. The `host` default is the first
|
||||
label's instance; inside a repository `glab` uses the host of its remote.
|
||||
- `gitconfig`, one `[credential "<scheme>://<host>"]` block per instance, which
|
||||
`/etc/gitconfig` includes. The helper answers only for a host that has an
|
||||
account, so it never touches GitHub or the swarm's forge.
|
||||
|
||||
The unit generates both files. A deleted account leaves them on the next run, and
|
||||
a hand edit doesn't survive. `glab` keeps one token per host: when two labels
|
||||
name the same host, the first label in alphabetical order wins. If the unit
|
||||
can't read one of the accounts, it replaces nothing and retries, so the files
|
||||
keep the last complete set.
|
||||
|
||||
The unit fails fast and restarts itself every 30 seconds for up to a day, as
|
||||
the other store fetches do, so a store that's down or sealed never holds the
|
||||
container's boot open.
|
||||
|
||||
## Rolling out
|
||||
|
||||
The endpoint, the store path and the agent unit are new, and each end ignores
|
||||
what it doesn't know. An old controller has no GitLab route, so no account gets
|
||||
linked. An old agent has no unit, so a stored account sits unused. A new agent
|
||||
under an old controller lists a store directory that doesn't exist and writes an
|
||||
empty `glab` configuration.
|
||||
Loading…
Reference in a new issue