Five host options whose values hive-c0re renders into every agent's build move under services.hyperhive.swarm, so the swarm states each of them once instead of every hive carrying its own copy: c0re.dashboardPort -> swarm.dashboardPort c0re.claudeCodePackage -> swarm.claudeCodePackage github.enable -> swarm.github.enable otel.collector.port -> swarm.hiveOtel.collectorPort otel.extraResourceAttributes -> swarm.hiveOtel.extraResourceAttributes Every old path is a mkRenamedOptionModule alias, so a host that sets one keeps its value and gets an eval warning naming the new path. Types and defaults are unchanged, so the rendered meta flake, serve.json and hive-c0re's environment are the same as before on a host that sets none of them. The readers move with them: serve.json and its gc-root assertion, the hive-c0re environment, the hive collector's listen address and bridge port, the swarm collector's port-collision assertion, and the claude-login and review-classifier containers' claude pin. The swarm-services-switch fixture sets the swarm path and now also checks that serve.json carries the pinned claude. Part of #4857 (slice 2).
7 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: 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
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.
To turn it off for the whole swarm, set the swarm option:
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
The PAT is operator-supplied. In the swarm UI,
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.
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.
Implementation
How the agent's `gh`/`git push` actually authenticate, and how the notification poller works
How the agent uses it
When enabled, the container gets:
- A
ghwrapper onPATH(shadowing the rawgh) that exportsGH_TOKENfrom the token file at invocation, then execs realgh. Sogh pr create,gh api …, etc. just work —ghderives the identity from the token. - A git credential helper (
git-credential-hive-github), wired via a host-scoped/etc/gitconfigentry forhttps://github.com, sogit push https://github.com/<owner>/<repo>authenticates asx-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),
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. Once polling, it re-reads <state>/github-token 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 wantsBearer <t>plusAccept: application/vnd.github+json,X-GitHub-Api-Versionand aUser-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-remainingnear 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.