# Set up personal GitHub integrations

> Register a GitHub OAuth App, choose scopes, and supply the six GitHub settings so members of a self-hosted TulipFarm can connect GitHub accounts.

Source: https://tulipfarm.ai/docs/self-hosting/github/

Use this guide to configure TulipFarm's personal GitHub OAuth accounts on your
server. This is not a GitHub App installation. For how members connect and use
their accounts, see [Connect GitHub](https://tulipfarm.ai/docs/integrations/github/index.md).

## 1. Register the OAuth Apps

1. In the intended GitHub owner account, open **Settings**, **Developer settings**,
   **OAuth Apps**, then **New OAuth App**.
2. Register a production app with a recognizable public name.
3. Set **Homepage URL** to your public web origin, such as `https://tulipfarm.example.com`.
4. Set **Authorization callback URL** to your web origin followed by
   `/github/callback`, such as `https://tulipfarm.example.com/github/callback`.
5. Select **Enable Device Flow** if you plan to qualify MCP bindings in
   [step 3](#3-choose-fixed-api-coverage-or-qualify-mcp-bindings). For an existing
   app, open its settings, enable the option, and save. Leave the callback unchanged.
   TulipFarm's user integration still uses browser authorization with S256 PKCE.
   If **Expire user access tokens** is offered,
   leave it disabled for this implementation. Expiring tokens require manual
   reconnect because automatic refresh is not implemented.
6. Register the app and generate its client secret. Transfer the client ID and
   secret directly to your [secret store](https://tulipfarm.ai/docs/self-hosting/secrets/index.md). Do not put either
   credential into a PR, chat, terminal argument, browser build, or screenshot.
7. For local consent tests, register a separate development OAuth App. Use
   `http://localhost:5173` as its homepage and
   `http://localhost:5173/github/callback` as its callback. Keep development and
   production credentials separate even when GitHub permits multiple callbacks.

The callback belongs to the **web app**, not the API. The web callback clears its
query and posts the code and state to the
authenticated `POST /v1/github/oauth/callback` API. Do not register that API route
as the provider callback. Keep the initiating browser session signed in.

If you deliberately use `http://127.0.0.1:5173` locally, register
`http://127.0.0.1:5173/github/callback` and set `WEB_ORIGIN` to that exact origin.
Do not mix `localhost` and `127.0.0.1`. Production requires HTTPS. The callback
cannot contain credentials, a query, or a fragment.

See GitHub's guide to
[create an OAuth app](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app).

## 2. Prepare the scopes and preserve the shared keyring

1. Set `GITHUB_OAUTH_SCOPES` to this space-separated value:

   ```text
   read:user read:org repo workflow delete_repo project write:discussion
   ```

   This is the full set in `githubOperationScopes`
   ([`packages/github/src/configuration.ts`](https://github.com/TulipFarm/tulipfarm/blob/main/packages/github/src/configuration.ts)).
   `repo` covers private repositories. `workflow` permits workflow-file changes.
   `delete_repo` is required for repository deletion. `project` includes project
   reads and writes, while `write:discussion` includes discussion reads and writes.
   These scopes do not override repository permissions, organization OAuth App
   restrictions, SSO requirements, or branch protection.

2. Request these scopes through TulipFarm's configuration, not a GitHub App
   permission screen. Check the actual consent grant. A reduced grant requires
   reconnect rather than silently enabling incomplete operation coverage.
3. Preserve the existing `INTEGRATION_CREDENTIAL_KEY_ID`,
   `INTEGRATION_CREDENTIAL_KEY`, and `INTEGRATION_CREDENTIAL_PREVIOUS_KEYS`.
   API and worker must use the same keyring. Do not generate a new key as
   part of GitHub setup. Replacing a key or discarding historical keys can make
   stored credentials unreadable for GitHub and other integrations.

The keyring parser ([`packages/integrations/src/credentials.ts`](https://github.com/TulipFarm/tulipfarm/blob/main/packages/integrations/src/credentials.ts)) requires an active
ID and a 64-hex-character key. Previous keys are a JSON object keyed by key ID and
must not repeat the active ID. Local parsing defaults absent previous keys to `{}`.
See [Encryption keys](https://tulipfarm.ai/docs/self-hosting/configuration/index.md#encryption-keys).

Changing any OAuth setting, including the client secret or scope set, changes the
account configuration hash. Plan for fresh consent and selection after such a
change. See the [OAuth scope reference](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps).

## 3. Choose fixed API coverage or qualify MCP bindings

For fixed API coverage, set `[]` as the JSON value for **both**
`GITHUB_MCP_READ_BINDINGS` and `GITHUB_MCP_WRITE_BINDINGS`. This selects implemented
REST or GraphQL routes before dispatch. It is not evidence that hosted MCP works.
Do not omit either variable in production. Continue to step 4 with those arrays
if you are not qualifying MCP now.

To enable supported official MCP mappings, complete these additional steps from a
source checkout, after `pnpm install --frozen-lockfile` and a build of
`@tulipfarm/github`:

1. Confirm **Enable Device Flow** is saved on the production OAuth App. Choose the
   operator-controlled GitHub identity and record its numeric GitHub user ID.
   Before a TulipFarm integration exists, open
   `https://api.github.com/users/YOUR_LOGIN` in your browser after replacing
   `YOUR_LOGIN` with that username. Use the numeric `id`, not `node_id`, the login,
   an OAuth client ID, or a TulipFarm membership ID. For an existing integration,
   use the **GitHub ID** shown in Integrations.
2. Export `GITHUB_OAUTH_CLIENT_ID` and `GITHUB_OAUTH_SCOPES` from your secret store.
   Replace `123456` in the commands below with that actual numeric GitHub ID.
   Use a private terminal without session recording. Choose a new output filename
   for every run, including retries:

   ```sh
   node tooling/github-qualification.mjs --device --account-id 123456 --inspect --output /private/operator/github-catalog.json
   ```

   This needs only the client ID and scopes. It needs no client secret and
   does not retrieve TulipFarm's stored user credentials.
3. Open the printed `https://github.com/login/device` URL, check the intended
   GitHub account and OAuth App, enter the user code, and authorize the requested
   scopes. Do not share the code. The CLI polls within GitHub's interval and expiry,
   obtains the token in memory, and checks the authenticated numeric user ID before
   contacting MCP. A different identity fails before catalog discovery.

   The command writes `untrusted-catalog` with mode 0600 and refuses to overwrite
   an existing file. Treat descriptions and schemas as untrusted provider data.
   It contacts `https://api.githubcopilot.com/mcp/` but permits only initialization
   and `tools/list`. It never calls a tool or writes a setting.
   Neither the OAuth access token nor the device code is printed
   or persisted. The short-lived user code appears only for browser consent.
4. Review the returned input, output, and execution contracts manually. Select only
   operations supported by the read matrix ([`packages/github/src/read-matrix.ts`](https://github.com/TulipFarm/tulipfarm/blob/main/packages/github/src/read-matrix.ts))
   and write binding schema ([`packages/github/src/write-provider.ts`](https://github.com/TulipFarm/tulipfarm/blob/main/packages/github/src/write-provider.ts)). Use the
   exact permitted tool `name` and observed 64-character `contractDigest`.
   Do not use a fixture digest or map an arbitrary catalog tool.
5. Create a private review JSON object with `reviewedBy`, an actual ISO UTC
   `reviewedAt`, `readBindings`, and `writeBindings`. Each selected binding must
   contain `operation`, `name`, and `contractDigest`. Keep unselected arrays empty.
6. Verify that explicit selection against a fresh authenticated catalog. Use the
   same app, scopes, numeric account ID, and a different new output filename:

   ```sh
   node tooling/github-qualification.mjs --device --account-id 123456 --review /private/operator/github-review.json --output /private/operator/github-reviewed-bindings.json
   ```

   Complete browser consent again. Inspection alone never approves bindings.
7. Require the `reviewed-bindings` result. Transfer only its `readBindings` array
   into `GITHUB_MCP_READ_BINDINGS` and its `writeBindings` array into
   `GITHUB_MCP_WRITE_BINDINGS`. Do not store the enclosing artifact as either runtime value.
8. Keep the review output private. On denied or
   expired consent, verify the app settings and account, then rerun for a fresh
   code and new output filename. On catalog drift, review the new contracts before
   creating a new explicit selection. Do not overwrite the old output.
   A digest match proves a reviewed catalog, not a successful read,
   write, approval, or recovery journey.

Read discovery uses `X-MCP-Readonly: true`. Full discovery omits that filter so
write contracts remain visible, but still cannot execute them. Configured mappings
win before dispatch. Catalog drift or execution failure does not fall back to an
API retry. Unmapped operations continue using fixed APIs.

Do not substitute a PAT or a token from another app for OAuth-derived qualification.
Exiting the CLI does not revoke the OAuth grant. Provider revocation can affect
existing integrations for the same account and app, so coordinate it rather than
using it as routine cleanup.

## 4. Supply the six GitHub settings

Give the API and the worker these six settings from your
[secret store](https://tulipfarm.ai/docs/self-hosting/secrets/index.md). No user access token is one of them.
User tokens are encrypted in workspace storage.

| Setting | Value |
| --- | --- |
| `GITHUB_OAUTH_CLIENT_ID` | The OAuth App client ID from step 1. |
| `GITHUB_OAUTH_CLIENT_SECRET` | The OAuth App client secret from step 1. |
| `GITHUB_OAUTH_REDIRECT_URI` | The exact web callback from step 1. |
| `GITHUB_OAUTH_SCOPES` | The scope string from step 2. |
| `GITHUB_MCP_READ_BINDINGS` | The read bindings JSON array from step 3. |
| `GITHUB_MCP_WRITE_BINDINGS` | The write bindings JSON array from step 3. |

Restart the API and the worker after you change a value. Runtime startup rejects
partial OAuth configuration, missing operation scopes, missing production arrays,
malformed bindings, and an incorrect callback origin. Leaving
`GITHUB_OAUTH_CLIENT_ID` unset disables GitHub.

For local development, use these same six keys with the development
app's credentials and local callback. Never inject backend keys into the web app build.

## 5. Recover without repeating uncertain writes

1. For a failed status read, use the visible retry or status-check action. Loading
   is not an empty account list. Stale authorized data may remain visible after a
   transient failure, while decisions stay disabled. Access loss conceals it.
2. For expired or rejected credentials, use **Reconnect** in Integrations with the
   same GitHub identity. OAuth App tokens need not expire, but any returned expiry
   is enforced. There is no automatic refresh-token renewal.
3. To stop local access, use **Disconnect**. To revoke the provider grant as well,
   open GitHub **Settings**, **Applications**, **Authorized OAuth Apps**, select
   the app, and revoke it. Provider revocation can affect other workspaces that
   use the same account and app. Disconnect alone does not revoke that grant.
4. For an `unknown` action, read status and use provider reconciliation in the
   approval control. Reconciliation performs reads only. It confirms success only
   when retained evidence attributes the original effect, not merely when the
   current state happens to match.
5. If evidence remains insufficient, leave the outcome unresolved. Reads remain
   available, but new write batches for that membership remain blocked, including
   attempts through another account. There is no supported human override to mark
   an unknown effect successful, no general manual-resolution endpoint, and no
   automatic retry. Do not change command IDs, edit receipts, or create replacement
   intent to bypass the block.

Each provider family has different positive reconciliation evidence
([`packages/github/src/batches.ts`](https://github.com/TulipFarm/tulipfarm/blob/main/packages/github/src/batches.ts)). A 404 does not prove a deletion, and
matching merge or Actions state does not prove that TulipFarm caused it.
