Skip to content

Let members connect GitHub with OAuth through the GitHub App - #1720

Open
akwirick wants to merge 9 commits into
mainfrom
github-personal-oauth
Open

akwirick wants to merge 9 commits into
mainfrom
github-personal-oauth

Conversation

@akwirick

Copy link
Copy Markdown
Contributor

Stacked on #1719. Review and merge #1719 first; GitHub then changes this PR's base to main.

Summary

A member can now click Connect for GitHub in Settings → Credentials and authorize the org's GitHub App to act as them. They do not have to create or paste a personal access token. After connect, engrams knows their GitHub login, so the Reviews "Mine" filter from #1719 works at once.

  1. Ask OAuth token endpoints for JSON. GitHub answers the token request in form encoding unless the client asks for JSON, and the coordinator could not read that answer. The token request (code exchange, refresh, and server-side grants) now sends Accept: application/json. This does not change the result for providers that already answer in JSON.
  2. Personal OAuth for GitHub.
    • The github_app mint kind gets two optional fields, client_id and client_secret: the App's OAuth client. The admin enters them in the same GitHub setup form. Minting does not use them.
    • A mint connector has no org OAuth facet to reuse. So on a mint connector, userCredential.oauth is now a complete authorization-code flow of its own (oauthFacet). This replaces the parse error "user-to-server OAuth is not implemented".
      • Its token URL may be on an acquisition host, because GitHub exchanges codes on github.com. Adding github.com to hosts would open it to session injection. The org facet keeps the strict rule.
      • A personal flow may name no scopes. GitHub ignores scopes for App user tokens.
    • GitHub's flow asks GET /user: id becomes the account id and login the display name.
    • Settings → Credentials offers Connect only when the org has the App's client id and secret. Without them every member would see a button that can only fail. If the secret listing fails, the button stays, and the flow reports the real error.
    • The GitHub setup panel has an optional Step 5 that shows the callback URL to set on the App. The App manifest now sets callback_urls.
  3. Docs: the GitHub guide gets Step 5, the two ways to connect, and the Reviews "Mine" selection.

What sessions get

A profile that acts as the launching person now uses the App user token when the person connected with OAuth. That token has the App's permissions, limited to the person's own access, and does not carry the permissions of a token the person made by hand. GitHub App user tokens expire after 8 hours. The coordinator refreshes them on the same path that refreshes the other personal OAuth credentials.

Upgrade notes

  • For admins: to turn this on, open Settings → Integrations → GitHub. Set the App's callback URL to <origin>/api/v1/integrations/github/oauth/callback, generate a client secret, and enter the Client ID and client secret in the optional fields. An App made from the old manifest does not have the callback URL yet.
  • If the admin does nothing, the personal token path works as before.

Test plan

  • Coordinator oauth_redirect: the token request sends Accept: application/json. oauth_user_credentials passes.
  • engram-git-github: the descriptor test pins the two optional fields.
  • Workspace: cargo fmt --check, clippy -D warnings and hakari generate --diff pass. In nextest --no-fail-fast, 3223 of 3226 tests pass. The 3 that fail (engram-harness-codex dynamic-tool tests, engram-uffd-handler stalled_alt_source_…) also fail on the base commit in my environment.
  • Orchestrator: typecheck. New tests cover the mint-connector personal-flow parse (accepted; refused if true, a non-code grant, or a token host outside the declared hosts), GitHub authorize and callback through the personal flow, and Connect only when both client secrets are set. In the full suite, 2434 tests pass. connector-facets failed once on a 5.5 s timeout and passes alone.
  • Web: format, lint, build, and vitest for lib/githubSetup, components/integrations and components/settings
  • Site: pnpm build (leak and style lint, astro check)
  • Manual: on a deployment with the App's client id and secret set, connect GitHub from Settings → Credentials, confirm that the login shows, and confirm that Reviews → Mine filters to it. After 8 hours, confirm that a session still gets a working token after the refresh.

🤖 Generated with Claude Code

akwirick and others added 8 commits October 29, 2026 22:59
A personal token saved in Settings -> Credentials had no account data. So
engrams did not know whose token it was (for GitHub: the login).

A connector can now declare `userCredential.token.identity`: one bounded
"who am I" request to one of its hosts. The orchestrator sends it with the
token in `PutCredentialRequest.identity`. The coordinator runs it with the
token before the seal, and keeps the display and workspace names. The
account id stays empty, so a later OAuth connect to a different account
still replaces the token. If the provider refuses the token, the token is
not saved, and the route returns 400 with a plain message. GitHub declares
`GET /user` -> `login`.

The parse of the probe moves out of `parseOauth` into shared helpers, so
the OAuth metadata probe and the token identity probe use one validation.

The credentials form now shows the server's message instead of the status
line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Mine" shows only pull requests whose author is the reader's GitHub login,
as their connected GitHub credential names it. It uses the existing server
author filter, and hides the Author filter while it is on. The rail uses
the same per-browser choice, so the switcher and the ledger list the same
pull requests.

If GitHub is not connected, or the token was saved before engrams recorded
the login, "Mine" says how to fix it instead of showing an empty list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RFC 6749 token endpoints answer in JSON, but GitHub answers in form
encoding unless the request asks for JSON. The coordinator could then not
read a GitHub token response ("token response was not JSON"). The token
request (code exchange, refresh, and server-side grants) now sends
`Accept: application/json`. This does not change the result for providers
that already answer in JSON.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A member could only paste a personal access token for GitHub. Now a
member can click Connect in Settings -> Credentials and authorize the
org's GitHub App to act as them. The user token has the App's permissions,
limited to the member's own access. It expires after eight hours and the
coordinator refreshes it, as it does for other personal OAuth credentials.

- The `github_app` mint kind gets two optional fields, `client_id` and
  `client_secret`: the App's OAuth client. The admin enters them in the
  same GitHub setup form. Minting does not use them.
- A mint connector has no org OAuth facet to reuse, so its
  `userCredential.oauth` is now a complete authorization-code flow of its
  own (`oauthFacet`). Its token URL may be on an acquisition host:
  GitHub exchanges codes on github.com, and adding github.com to `hosts`
  would open it to session injection. A personal flow can name no
  scopes, because GitHub ignores scopes for App user tokens.
- GitHub's flow asks `GET /user` for the account: `id` is the account id
  and `login` the name, so the Reviews "Mine" filter works after connect.
- Settings -> Credentials offers Connect only when the org has the app's
  client id and secret. Without them the button could only fail.
- The GitHub setup panel shows the callback URL to set on the App, and
  the App manifest sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The GitHub guide gets the optional fifth setup step (the App's callback
URL and OAuth client), the two ways a person connects their own GitHub
account, and the Mine selection on the Reviews page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The identity probe of a pasted token reported every failure as
"probe_failed", and the route answered each one with 400 "Check the
token". A GitHub outage or a 5xx then told the user that the token was
bad.

The probe now reports HTTP 401 and 403 as "probe_rejected". The route
answers a refusal with 400 and tells the user to check the token. It
answers any other probe failure with 503 and tells the user to try again.
The token is not saved in either case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	orchestrator/src/__tests__/me-oauth.test.ts
GitHub answers a dead refresh token with HTTP 200 and the error code
"bad_refresh_token", not the RFC's "invalid_grant". The token grant
reported that as "exchange_failed", which the refresh sweep treats as
transient. A GitHub App user credential whose refresh token expired or
was revoked then stayed "connected": the sweep retried each minute, and
sessions got an expired token with no reconnect prompt.

The grant now reports "bad_refresh_token" as "invalid_grant". The sweep
marks the credential broken, and keeps the existing check that reloads
the winner when a concurrent refresh moved the row version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nikhilunni
nikhilunni added this pull request to stack #1731 November 4, 2026 15:27
@nikhilunni
nikhilunni enabled stacked auto-merge November 4, 2026 15:28
nikhilunni
nikhilunni previously approved these changes Nov 4, 2026
@nikhilunni
nikhilunni disabled stacked auto-merge November 4, 2026 20:37
@nikhilunni
nikhilunni added this pull request to the merge queue Nov 4, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue because a pull request earlier in the stack was removed Nov 4, 2026
Base automatically changed from reviews-mine-filter to main November 4, 2026 21:42
@akwirick

Copy link
Copy Markdown
Contributor Author

@engrams-agent Are you able to fix the merge conflicts?

# Conflicts:
#	orchestrator/src/__tests__/connectors.test.ts
#	orchestrator/src/__tests__/me-oauth.test.ts
#	orchestrator/src/connectors/oauth-spec.ts
#	orchestrator/src/connectors/registry.ts
#	orchestrator/src/routes/me.ts

This branch has not been deployed

No deployments
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.

2 participants