Sixth batch of the ongoing write-good.Passive pass (hyperhive#4042):
read all 52 hits across knowledge.md/github.md/matrix.md/forge.md in
context and rewrote 39 with a clearly nameable actor -- mostly
hive-c0re, forge_notify, or a specific fn named right there or a
sentence or two earlier. forge.md's notification poller is the
densest yet (19/20 hits rewritten): forge_notify is established as
the section's sole actor early and reused throughout, the shape
that's produced the highest catch rates all along.
Left 13 alone: the "no X is needed" negative-capability idiom (x2),
a container-lifecycle state descriptor ("when container is stopped"),
a false-positive tokenization ("read-only" split across a line wrap,
vale matches "is read" inside it -- not a real passive at all), the
"X can't be Yed" idiom, a generic "before the ids are minted" timing
clause with no natural actor to name, a room-join policy-state
descriptor, an "is enabled"/"is trusted" pair describing a config/
trust state (predicate-adjective-copula bucket, same family as
"is privileged" from an earlier batch), three "**X is required**"
bolded requirement-list labels (structural convention, not really
mid-sentence passives), and a contrastive "are shared" clause
mirrored against an active sibling clause exactly like
claude-invocation.md's "everything else is shared" from the
turn-loop batch -- left alone there for the same reason.
One sibling-inconsistency catch worth flagging: forge.md's merge-
racing-comment paragraph had two passive clauses ("is left off",
"is dropped") sitting next to a third, already-active clause
("appends nothing") in the same three-item parallel list -- rewrote
all three under one active subject (forge_notify) for consistency.
Verified via vale before/after: 52 -> 13 write-good.Passive hits,
exactly the 13 left alone above; error count and other warning
categories unchanged (still on TooWordy since #4097 hasn't merged to
this branch yet). Re-read every changed line in full surrounding
context after editing, matching the diff to intent before running
the final vale check.
145 lines
6.4 KiB
Markdown
145 lines
6.4 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 our own tick) and rate-limits callers who ignore
|
|
it, so the loop re-arms to the server's interval whenever that's
|
|
_slower_ than ours. A hint faster than our 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>
|