Operant Studio
OPER-9C

Replace GH_DISPATCH_PAT with GitHub App installation auth

Band
MEDIUM
Suggested
Justin Cooke

Body

## Context

On [REDACTED-DOB] the entire agent-dispatch pipeline stopped for hours because the
`GH_DISPATCH_PAT` in Vercel returned 404s on both target repos. Root cause:
the fine-grained PAT's repository allow-list didn't include both
`Operant-Health/operant-studio` and `Operant-Health/portico`. This is the
second `GH_DISPATCH_PAT` outage class (first was expiry, handled by OPER-9A).

The root architectural issue: personal access tokens are tied to a human
identity, expire every 90 days, and require manual allow-list maintenance.
Every rotation is a manual ritual with real outage risk when missed.

GitHub Apps with installation auth eliminate this entire category: the org
owns the app (not a human), installations don't expire, tokens are minted
on-demand per API call. Same access model, no rotation, no allow-list drift.

## Goal
Eliminate 90-day PAT rotation ritual by moving GitHub authentication from a
personal access token (`GH_DISPATCH_PAT`) to a GitHub App installation. App
installations don't expire; short-lived (1-hour) tokens are minted on demand
per API call. Zero rotation ritual going forward.

Supersedes OPER-9B (dual-PAT failover — unnecessary once primary auth doesn't expire).

## Human-gate steps (Sweet @sweet-op — ~10 minutes, one time)

1. **Register the GitHub App** at
   https://github.com/organizations/Operant-Health/settings/apps/new
   - Name: `Operant Dispatcher`
   - Homepage URL: `https://fiesta.porticoaba.com`
   - Webhook: **uncheck "Active"** (we don't need webhooks)
   - Repository permissions:
     - Actions: Read and write
     - Contents: Read and write
     - Metadata: Read (auto-selected)
     - Pull requests: Read and write
   - Where can this GitHub App be installed: **Only on this account**
   - Click **Create GitHub App**
2. On the app's settings page: scroll to **Private keys**, click
   **Generate a private key**. A `.pem` file downloads.
3. Note the **App ID** (top of the app settings page).
4. Left sidebar: **Install App** -> choose Operant-Health -> select
   `operant-studio` and `portico` (or "All repositories"). Click **Install**.
5. After installation, the URL shows the **Installation ID** — the number after
   `/installations/` in the URL bar (e.g. `.../installations/[REDACTED-MRN]`).
6. Go to Vercel -> `operant-studio-web` -> Settings -> Environment Variables.
   Add three variables (Production only):
   - `GH_APP_ID` = the App ID from step 3
   - `GH_APP_INSTALLATION_ID` = the Installation ID from step 5
   - `GH_APP_PRIVATE_KEY_PEM` = paste the entire `.pem` file contents (including
     `-----BEGIN...-----` and `-----END...-----` lines)
7. Drop a comment on this Ledger ticket: `sweet: app installed, env set, ready
   for dispatcher`. That comment unblocks the dispatcher agent to start the
   code work.

## Agent-owned steps (dispatcher — after Sweet's comment)

1. Add `@octokit/auth-app` dependency to `packages/dispatcher/package.json`.
2. In `packages/dispatcher/src/github-dispatch.ts`, replace the two `process.env.GH_DISPATCH_PAT` reads (lines ~117 and ~272) with a shared
   `getInstallationToken()` helper that:
   - Reads `GH_APP_ID`, `GH_APP_INSTALLATION_ID`, `GH_APP_PRIVATE_KEY_PEM`
   - Uses `createAppAuth` from `@octokit/auth-app` to mint a 1-hour installation token
   - Caches the token in-memory until 5 minutes before expiry
   - Throws with a clear misconfig error if any of the 3 env vars are absent
3. Update `packages/dispatcher/src/github-pat-expiry.ts` — this file becomes
   obsolete. Delete it and remove the cron in
   `apps/web/src/app/api/cron/pat-expiry-check/route.ts` (also delete the cron
   registration from `apps/web/vercel.json`).
4. Grep the repo for other `GH_DISPATCH_PAT` references (there may be 1-2 in
   test helpers or scripts). Migrate each to the App auth pattern or gate them
   behind a fallback that reads the old env var for local dev.
5. Add a Vitest around `getInstallationToken()` that mocks `createAppAuth` and
   verifies caching + expiry behavior.
6. In the PR body, include a checklist:
   - [ ] Sweet has confirmed `GH_APP_ID` / `GH_APP_INSTALLATION_ID` / `GH_APP_PRIVATE_KEY_PEM` are set in Vercel Production
   - [ ] Local dev fallback documented in README (or dispatcher package README)
   - [ ] `pat-expiry-check` cron removed
   - [ ] `GH_DISPATCH_PAT` env var can be removed from Vercel after merge

## Post-merge (Sweet — ~1 minute)

1. Remove `GH_DISPATCH_PAT` env var from Vercel.
2. Revoke the old fine-grained PAT at
   https://github.com/settings/personal-access-tokens.

## Acceptance criteria

- Dispatcher fires workflow_dispatch on both `operant-studio` and `portico`
  using an App-minted installation token.
- `pat-expiry-check` cron no longer exists.
- No file in `packages/dispatcher` or `apps/web` reads `GH_DISPATCH_PAT` (except
  optional local-dev fallback with a code comment explaining it).
- One full agent-dispatch cycle end-to-end (PLANNING_VALIDATED -> IN_PROGRESS ->
  PR_OPEN) completes with the new auth mechanism.

## Why this matters

- Eliminates the 90-day rotation ritual (zero manual work going forward)
- Eliminates the "PAT expired -> fleet dead" outage class
- Removes the SPOF exposed on [REDACTED-DOB] when the old PAT's repo allow-list
  didn't include both target repos
- Standard pattern used by Vercel's own GitHub integration and CodeRabbit


---
<!-- admission-rejected -->
**Admission rejected (missing_feature_id):** ticket body is missing `feature_id: FEAT-...` frontmatter — declare which feature this ticket implements

Attachments

Loading attachments…

Comments

Loading comments…