Per mara's ruling on hyperhive#4041 (Microsoft.We): keep the rule enabled, same treatment as the 'backend' rewrites. Traced all 21 genuine hits (4 gateway.md 'Let's Encrypt' hits are a substring-match false positive, left alone) to their actual referent: some name a specific component already established nearby in the same doc (forge_notify, hive-github-notify's poller, hive-forge, hive-agent/the harness, colors.css, the dashboard), others were pure filler that adds nothing once dropped.
145 lines
6.5 KiB
Markdown
145 lines
6.5 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, mirroring the dashboard side of the
|
|
[matrix account](matrix.md) flow: 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.
|
|
|
|
## Enabling
|
|
|
|
The integration is **on by default** for every agent (`hyperhive.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.
|
|
|
|
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 `hyperhive.github.enable = false`
|
|
into every agent, so no agent ships the `gh` wrapper or credential helper.
|
|
(`hyperhive.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)).
|
|
|
|
## 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:
|
|
|
|
```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).
|
|
|
|
## 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
|
|
`hyperhive.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>
|