hyperhive/docs/github.md
iris 052767c25b docs(github): move impl detail below the operator-facing sections
github.md mixed operator content (enabling, provisioning, security)
with deep implementation detail (the gh wrapper/credential-helper
mechanics, the notification poller's internals) in file order, so an
operator reading top-to-bottom hits internals before finishing the
part they actually need.

Pure reorder, no rewrite: Enabling -> Provisioning -> Security (all
operator-facing) now come first: How the agent uses it and
Notifications (both pure impl detail) move to the end, with a one-line
marker between them. Every word of existing content is unchanged, only
section order moved - lowest-risk shape for a file like this with no
subdirectory to split into (see hyperhive#1898).
2026-08-02 23:54:55 +02:00

6.4 KiB

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 flow: paste a PAT into the agent's credentials tab and it works. No per-agent nix declaration, no rebuild — the token is injected 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 a PAT is provisioned. There is nothing per-agent to declare — an agent gains GitHub simply by having a PAT written to its token file.

To turn it off for the whole hive, set the host option:

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 is written to <state>/github-token separately (see 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). There is also a CLI path for recovery/scripting:

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.

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.

Everything below this point is implementation detail (how the agent actually uses the token, and the notification poller's internals).

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 simply fail unauthenticated. The token path is baked into the scripts at build time (not read 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 is a separate binary and a separate systemd unit from the internal forge's poller (hive-forge-notify, see forge.md), 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 is also why this is not 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 is Restart = on-failure and never always — a clean exit on a PAT-less agent must not become a restart loop.

Forgejo's notifications API is modelled 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. The two real differences are absorbed by lenient deserializers — GitHub sends the thread id as a string where Forgejo sends a number, and says PullRequest where Forgejo says Pull. Todo keys are prefixed gh:<id> so a github thread id cannot 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 does not 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 our own tick) and rate-limits callers who ignore it, so the loop re-arms to the server's interval whenever that is slower than ours. A hint faster than our own tick is not 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 simply 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.