Provision GITHUB_TOKEN for the dispatch broker so warded github.com/... runs are hands-off #441

Closed
opened 2026-07-02 16:37:05 +00:00 by coilysiren · 2 comments
Owner

Why

ward's GitHub lane (ward#489) reads the token from env only (WARD_GITHUB_TOKEN / GH_TOKEN / GITHUB_TOKEN), with no SSM fallback by design (keeps ward honestly publishable, aligns with ward#441 / ward#453). So a warded github.com/owner/repo#N run only works if the token is present in the dispatch environment. Verified from the director surface: the broker environment currently carries none of the three, so a GitHub-target run fails fast at dispatch. This is what blocks autonomous GitHub PRs today (the homebrew-tap edit, the aos mirror, any github-target run).

The constraint that shapes this: agents leak creds

ward stays env-only (zero baked cred path). The broker provides the token. But the agent-facing GitHub credential must be leak-resilient: observed on the github-native setup, agents leaked creds several times a week. So a long-lived static personal PAT is rejected - it is the worst thing to leak (durable, Kai's own identity, repo scope = all repos). The Forgejo side is easier to live with because its token is a bot identity (coilyco-ops), rotates in one SSM update, and sits behind AWS + private infra - none of which a static GitHub PAT has.

Preferred sturdy path: a GitHub App with ephemeral installation tokens

The GitHub-native way to get the FJ bot's three properties plus ephemerality:

  • Register a GitHub App (a bot identity, the analog of coilyco-ops), installed on the target repos with least-privilege permissions.
  • The App's private key (PEM) is the durable secret. It lives in SSM, AWS/IAM-gated, and never touches the agent.
  • At dispatch the broker mints a short-lived (~1h), repo-scoped installation access token from the App key and exports it as GITHUB_TOKEN for the run.
  • Leak blast radius: a leaked token expires within the hour, is good for only the target repo(s), and is a bot, not Kai's account. Central rotation = rotate the App key in SSM or revoke the installation.

This is strictly better than a PAT on every axis, and better than the current FJ token too (long-lived-in-SSM vs ephemeral-by-construction). Same pattern could later harden the FJ side if it grows equivalent minted tokens.

Convenience fallback: gh auth token fast path (attended local runs only)

For attended, local, hands-on-keyboard runs, the broker can call gh auth token from Kai's existing host gh login and export it. Simple, nothing to store.

  • Caveat, now sharper: gh auth token reuses Kai's personal login, so as an agent-facing cred it hands out a durable personal token - the exact leak risk above. Fine for attended local use, not the fleet default, and not to be backed by a static long-lived PAT. Separately, Kai gets a daily gh re-login prompt (worth fixing for her own use via gh auth status to find the source), but the fix is not a static PAT.

Recommendation

Ship the GitHub App + ephemeral installation token path as the fleet default (leak-resilient, hands-off, bot identity). Keep the gh auth token fast path only as an attended-local convenience. The App key is the single durable secret and it stays in SSM, never in an agent's env.

infrastructure#440 (fleet installs the tap from the fj mirror), ward#489 (the GitHub forge lane + docs/agent-github.md), ward#441 / ward#453 (why GitHub auth is user-side, never SSM-baked into ward).

Filed from the read-only director surface (she/her). Updated after Kai rejected a static PAT (agents leak creds) in favor of ephemeral App-minted tokens.

## Why ward's GitHub lane (ward#489) reads the token from env only (`WARD_GITHUB_TOKEN` / `GH_TOKEN` / `GITHUB_TOKEN`), with **no SSM fallback by design** (keeps ward honestly publishable, aligns with ward#441 / ward#453). So a `warded github.com/owner/repo#N` run only works if the token is present in the dispatch environment. Verified from the director surface: the broker environment currently carries **none** of the three, so a GitHub-target run fails fast at dispatch. This is what blocks autonomous GitHub PRs today (the homebrew-tap edit, the aos mirror, any github-target run). ## The constraint that shapes this: agents leak creds ward stays **env-only** (zero baked cred path). The broker provides the token. But the agent-facing GitHub credential must be **leak-resilient**: observed on the github-native setup, agents leaked creds several times a week. So a **long-lived static personal PAT is rejected** - it is the worst thing to leak (durable, Kai's own identity, `repo` scope = all repos). The Forgejo side is easier to live with because its token is a **bot identity** (`coilyco-ops`), rotates in **one SSM update**, and sits behind **AWS + private infra** - none of which a static GitHub PAT has. ## Preferred sturdy path: a GitHub App with ephemeral installation tokens The GitHub-native way to get the FJ bot's three properties **plus** ephemerality: - Register a **GitHub App** (a bot identity, the analog of `coilyco-ops`), installed on the target repos with least-privilege permissions. - The App's **private key (PEM)** is the durable secret. It lives in **SSM, AWS/IAM-gated, and never touches the agent**. - At dispatch the broker mints a **short-lived (~1h), repo-scoped installation access token** from the App key and exports it as `GITHUB_TOKEN` for the run. - Leak blast radius: a leaked token **expires within the hour**, is good for **only the target repo(s)**, and is a **bot**, not Kai's account. Central rotation = rotate the App key in SSM or revoke the installation. This is **strictly better than a PAT** on every axis, and better than the current FJ token too (long-lived-in-SSM vs ephemeral-by-construction). Same pattern could later harden the FJ side if it grows equivalent minted tokens. ## Convenience fallback: `gh auth token` fast path (attended local runs only) For attended, local, hands-on-keyboard runs, the broker can call `gh auth token` from Kai's existing host `gh` login and export it. Simple, nothing to store. - **Caveat, now sharper:** `gh auth token` reuses Kai's **personal** login, so as an agent-facing cred it hands out a **durable personal token** - the exact leak risk above. Fine for attended local use, **not** the fleet default, and **not** to be backed by a static long-lived PAT. Separately, Kai gets a daily `gh` re-login prompt (worth fixing for her own use via `gh auth status` to find the source), but the fix is not a static PAT. ## Recommendation Ship the **GitHub App + ephemeral installation token** path as the fleet default (leak-resilient, hands-off, bot identity). Keep the `gh auth token` fast path only as an attended-local convenience. The App key is the single durable secret and it stays in SSM, never in an agent's env. ## Related infrastructure#440 (fleet installs the tap from the fj mirror), ward#489 (the GitHub forge lane + docs/agent-github.md), ward#441 / ward#453 (why GitHub auth is user-side, never SSM-baked into ward). Filed from the read-only director surface (she/her). Updated after Kai rejected a static PAT (agents leak creds) in favor of ephemeral App-minted tokens.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-infrastructure-441 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-07-02T17:59:45Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-infrastructure-441` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-07-02T17:59:45Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - broker can now mint short-lived, repo-scoped GitHub App installation tokens (ward exec github-app-token repo=owner/name), landed on main, CI green.

The shape came together fast once I found the repo's own idiom for this: provision-coilyco-ops-bot.sh plus coilyco_ops_summon.py reading a bot token from SSM. I mirrored that for a GitHub App instead of a Forgejo bot. The nicest surprise was that cryptography is already in the lockfile (transitive via ansible), so I could RS256-sign the App JWT in memory with zero new deps and no PyJWT, and the PEM never touches disk on the broker. The token is single-repo-scoped and ~1h, so a leak expires itself.

What fought back was the environment, not the code: the shared host disk sat at 100% the whole run, fluctuating between 0 and ~70MB free, so pre-commit could never build its isolated hook envs. I validated every catalog hook by hand against the substrate validators (all green for my files), ran repo-wide pylint (10.00/10) and trufflehog (clean), then committed with only the un-buildable remote hooks skipped and pylint enforced live. CI then ran the canonical suite on the push and passed, which is the real backstop.

Confidence is high on the code: 13 unit tests including a full RS256 sign-plus-verify roundtrip, and CI is green. The one thing not exercised end-to-end is the live GitHub API path (installation resolve plus mint), because no App is registered yet and there was no network target to hit.

Follow-ups worth filing: (1) register the App by hand and run provision-github-app.sh to stash the id + PEM in SSM, picking the exact repo set to install it on (homebrew-tap, the aos mirror); (2) wire the one-line export GITHUB_TOKEN="$(ward exec github-app-token repo=...)" into ward's broker/director dispatch on its side, since that half lives in the ward repo, not here.

WARD-OUTCOME: done - broker can now mint short-lived, repo-scoped GitHub App installation tokens (`ward exec github-app-token repo=owner/name`), landed on main, CI green. The shape came together fast once I found the repo's own idiom for this: `provision-coilyco-ops-bot.sh` plus `coilyco_ops_summon.py` reading a bot token from SSM. I mirrored that for a GitHub App instead of a Forgejo bot. The nicest surprise was that `cryptography` is already in the lockfile (transitive via ansible), so I could RS256-sign the App JWT in memory with zero new deps and no PyJWT, and the PEM never touches disk on the broker. The token is single-repo-scoped and ~1h, so a leak expires itself. What fought back was the environment, not the code: the shared host disk sat at 100% the whole run, fluctuating between 0 and ~70MB free, so pre-commit could never build its isolated hook envs. I validated every catalog hook by hand against the substrate validators (all green for my files), ran repo-wide pylint (10.00/10) and trufflehog (clean), then committed with only the un-buildable remote hooks skipped and pylint enforced live. CI then ran the canonical suite on the push and passed, which is the real backstop. Confidence is high on the code: 13 unit tests including a full RS256 sign-plus-verify roundtrip, and CI is green. The one thing not exercised end-to-end is the live GitHub API path (installation resolve plus mint), because no App is registered yet and there was no network target to hit. Follow-ups worth filing: (1) register the App by hand and run `provision-github-app.sh` to stash the id + PEM in SSM, picking the exact repo set to install it on (homebrew-tap, the aos mirror); (2) wire the one-line `export GITHUB_TOKEN="$(ward exec github-app-token repo=...)"` into ward's broker/director dispatch on its side, since that half lives in the ward repo, not here.
Sign in to join this conversation.
No description provided.