Skip to content

feat(github): a multi-org GitHub App, and git that reaches every org (v0.435.0) - #807

Merged
vikasprogrammer merged 2 commits into
mainfrom
feat/gh-multi-org
Sep 10, 2026
Merged

feat(github): a multi-org GitHub App, and git that reaches every org (v0.435.0)#807
vikasprogrammer merged 2 commits into
mainfrom
feat/gh-multi-org

Conversation

@vikasprogrammer

@vikasprogrammer vikasprogrammer commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Phases 1–3 of docs/github-multi-org-plan.md (#800).

The defect

GithubIdentity.ensureBotToken persisted a single github_installation_id and, when it was unset, resolved it as installations[0] — whichever install GET /app/installations happened to return first — then cached one token under one vault key.

With the App installed on two orgs the bot acted on one of them and 404'd every repo in the other. Nothing failed at launch: the token is valid, it just doesn't cover that repo, so it reads to a human as "the agent can't see our repo". The stale-id retry re-picked [0] too, so a reinstall could flip which org worked with nobody touching a setting.

Phase 1 + 2 — the registry and a token per installation

  • github_installations records the whole set; github_installation_id keeps its exact meaning as the primary. Both are plain settings rows — no migration, and a one-org tenant behaves bit-for-bit as before. GithubIdentity gains installations / installationFor (case-insensitive) / primaryInstallation / orgs / refreshInstallations.
  • Each installation caches under github_bot_token:<id>; loadBotToken(org?) and ensureBotToken(nowMs?, by?, org?) resolve per org. A token in the legacy bare slot migrates onto the primary's key on first read.
  • An org the App isn't installed on yields undefined and mints nothing — never another org's credential. refreshInstallations leaves a live primary alone and replaces only a vanished one, so reinstall churn can't quietly move which org the fleet pushes to.
  • Launch exports AOS_GH_ORG + AOS_GH_ORGS; GET /api/state exposes installations + primaryInstallationId. Clearing the private key now drops every installation's token and the registry.

Phase 3 — git reaches every org

configureGitCredentials turns on credential.useHttpPath for a multi-org bot run, so git hands the helper path=<org>/<repo> on every request. The helper reads the org off that and — only when it isn't the org $GH_TOKEN already covers — fetches that installation's token from POST /api/agent/github/credential, the same session-secret loopback channel the gate hook uses.

  • A primary-org repo costs no round trip.
  • Any failure falls back to $GH_TOKEN: unreachable route, uninstalled org, restarted server. The worst case is exactly the old behaviour, never broken git.
  • The member guard, both sides. The helper isn't installed for a run carrying a linked member's token, and the route refuses one (member_identity). A per-repo bot token would otherwise silently re-author that human's commits as the App bot the moment they touched a second org.
  • Mints are audited (github.bot_token.minted, via: credential-helper); lookups aren't — git calls its helper on every fetch/push and the token is cached for the hour.

gh is still primary-only — it reads GH_TOKEN and ignores git credential helpers. The agent's prompt now says exactly that rather than implying every non-primary org is unreachable. github_token({ org }) is phase 4.

Verification

  • npm run typecheck, cd web && npm run build, and the full npm run test:governance gate all clean.
  • scripts/github-multi-org-test.cjs — 50 checks, added to test:governance. The phase-3 half drives real git credential fill against the real route, covering the primary/second-org/uninstalled/unreachable/member-identity paths plus the route's 404/403/no_org edges.
  • scripts/github-per-member-test.cjs still 86/86 — the single-org path is unchanged.

Two things the real-git runs caught that review would not have: an inlined -d "{\"session\"...}" in the helper silently truncates the curl argument (the body is built with printf now), and a test harness using execFileSync deadlocks against an in-process server, so the helper always appeared to fall back.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UfJRSqtHhuk7x51Cqy4NB5

… org (v0.433.0)

`ensureBotToken` persisted a single `github_installation_id` and, when unset,
resolved it as `installations[0]` — whichever install GitHub returned first — then
cached one token under one vault key. With the App on two orgs the bot acted on one
and 404'd every repo in the other, with nothing failing at launch: the token is
valid, it just doesn't cover that repo, so it reads to a human as "the agent can't
see our repo". The stale-id retry re-picked `[0]` too, so a reinstall could flip
which org worked with no setting touched.

Phases 1+2 of docs/github-multi-org-plan.md:

- `github_installations` records the whole set; `github_installation_id` keeps its
  exact meaning as the PRIMARY. Both are plain settings rows — no migration, and a
  one-org tenant behaves as before.
- Each installation caches its own token under `github_bot_token:<id>`;
  `loadBotToken(org?)` / `ensureBotToken(nowMs?, by?, org?)` resolve per org. A
  token in the legacy bare slot migrates onto the primary's key on first read.
- An org the App isn't installed on yields nothing — never, silently, another org's
  credential. `refreshInstallations` leaves a live primary alone and replaces a
  vanished one, so reinstall churn can't quietly move which org the fleet pushes to.
- Launch exports `AOS_GH_ORG` + `AOS_GH_ORGS` and, past one installation, the prompt
  names the orgs and says a call against a non-primary one fails as though the repo
  did not exist — so an agent asks a human instead of concluding the repo is missing.

Reaching a non-primary org from a session is phases 2–4. The per-member OAuth lane
already spans every org that human can reach and is deliberately untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfJRSqtHhuk7x51Cqy4NB5
Phase 3 of docs/github-multi-org-plan.md. A bot token is scoped to ONE
installation, so the ambient GH_TOKEN could only ever reach the primary org — a
push anywhere else 404s from a perfectly valid credential.

On a multi-org bot run `configureGitCredentials` now turns on
`credential.useHttpPath`, which makes git hand the helper `path=<org>/<repo>` on
every request. The helper reads the org off that and, only when it isn't the org
$GH_TOKEN already covers, fetches that installation's token from a new
session-secret loopback route (POST /api/agent/github/credential — the same
AOS_SECRET channel the gate hook uses). A primary-org repo costs no round trip,
and ANY failure — unreachable route, uninstalled org, restarted server — falls
back to $GH_TOKEN, so the worst case is today's behaviour rather than broken git.

The member lane is guarded on both sides: the helper isn't installed for a run
carrying a linked member's token, and the route refuses one (`member_identity`).
A per-repo bot token would otherwise silently re-author that human's commits as
the App bot the moment they touched a second org.

`gh` ignores git credential helpers, so it stays on the primary org; the agent's
prompt now says that precisely instead of implying every org is unreachable.

Verified by driving real `git credential fill` against the real route — the shell
snippet is bash 3.2 / BSD clean, and its JSON body is built with printf because an
inlined `-d "{\"org\"...}"` silently truncates the argument.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfJRSqtHhuk7x51Cqy4NB5
@vikasprogrammer vikasprogrammer changed the title feat(github): the company bot stops collapsing a multi-org App to one org (v0.433.0) feat(github): a multi-org GitHub App, and git that reaches every org (v0.435.0) Sep 10, 2026
@vikasprogrammer
vikasprogrammer merged commit 911aa5a into main Sep 10, 2026
1 check passed
@vikasprogrammer
vikasprogrammer deleted the feat/gh-multi-org branch September 10, 2026 11:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant