Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/docs/integrations/github.md
atlas 8ad2af735e swarm-controller: refuse linking over an existing account
The matrix, forge and github link routes wrote their credential
unconditionally, so linking a name that was already linked replaced the
working account. For matrix that lost the device the agent's crypto store
belongs to (#4838).

Each route now reads the account's store path first and answers 409,
naming the existing account, when something is stored there. Nothing is
written. Replacing an account takes the delete from #4899, then a link.

The matrix route checks before password mode's login, so a refused link
mints no new device at the homeserver.

The check is a read then a write, not an atomic step; two concurrent
links to one name can still both pass it.

Closes #4856
2026-10-03 13:49:03 +02:00

149 lines
6.9 KiB
Markdown

# 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 hive, set the host option:
```nix
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, 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 — 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.md](../swarm/credentials.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](../trust-boundary/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](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.
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>