feat(github): a multi-org GitHub App, and git that reaches every org (v0.435.0) - #807
Merged
Conversation
… 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
vikasprogrammer
force-pushed
the
feat/gh-multi-org
branch
from
September 10, 2026 11:31
0a5d555 to
d97b6d6
Compare
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
This was referenced Sep 10, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phases 1–3 of
docs/github-multi-org-plan.md(#800).The defect
GithubIdentity.ensureBotTokenpersisted a singlegithub_installation_idand, when it was unset, resolved it asinstallations[0]— whichever installGET /app/installationshappened 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_installationsrecords the whole set;github_installation_idkeeps its exact meaning as the primary. Both are plainsettingsrows — no migration, and a one-org tenant behaves bit-for-bit as before.GithubIdentitygainsinstallations/installationFor(case-insensitive) /primaryInstallation/orgs/refreshInstallations.github_bot_token:<id>;loadBotToken(org?)andensureBotToken(nowMs?, by?, org?)resolve per org. A token in the legacy bare slot migrates onto the primary's key on first read.undefinedand mints nothing — never another org's credential.refreshInstallationsleaves a live primary alone and replaces only a vanished one, so reinstall churn can't quietly move which org the fleet pushes to.AOS_GH_ORG+AOS_GH_ORGS;GET /api/stateexposesinstallations+primaryInstallationId. Clearing the private key now drops every installation's token and the registry.Phase 3 —
gitreaches every orgconfigureGitCredentialsturns oncredential.useHttpPathfor a multi-org bot run, so git hands the helperpath=<org>/<repo>on every request. The helper reads the org off that and — only when it isn't the org$GH_TOKENalready covers — fetches that installation's token fromPOST /api/agent/github/credential, the same session-secret loopback channel the gate hook uses.$GH_TOKEN: unreachable route, uninstalled org, restarted server. The worst case is exactly the old behaviour, never broken git.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.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.ghis still primary-only — it readsGH_TOKENand 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 fullnpm run test:governancegate all clean.scripts/github-multi-org-test.cjs— 50 checks, added totest:governance. The phase-3 half drives realgit credential fillagainst the real route, covering the primary/second-org/uninstalled/unreachable/member-identity paths plus the route's 404/403/no_orgedges.scripts/github-per-member-test.cjsstill 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 withprintfnow), and a test harness usingexecFileSyncdeadlocks against an in-process server, so the helper always appeared to fall back.🤖 Generated with Claude Code
https://claude.ai/code/session_01UfJRSqtHhuk7x51Cqy4NB5