Skip to content

feat(gastown): GitHub OAuth Device Flow for user-scoped git identity and attribution #1350

@jrf0110

Description

@jrf0110

Summary

Replace the manual PAT entry in town settings with a one-click GitHub OAuth Device Flow that authenticates git operations as the individual user. Supersedes #1001 (git commit co-authorship) — this approach solves attribution, contribution credit, and PR authorship in one step, without needing to separately resolve user name/email or inject commit trailers.

Parent: #204 (Phase 4: Hardening)
Supersedes: #1001

Problem

Today, agent git operations (push, PR creation, merge) either use a GitHub App installation token (actions show as "bot") or a manually-entered PAT (requires the user to navigate to GitHub settings, create a token, copy-paste it). Neither is ideal:

  • App tokens: Commits and PRs are attributed to the app, not the user. No contribution graph credit. No human provenance trail.
  • Manual PAT: High-friction setup. Users frequently get scopes wrong, tokens expire, and there's no refresh mechanism.

Solution

Add a GitHub OAuth Device Flow to the town settings UI. The user clicks "Connect GitHub," enters a short code in GitHub's browser UI, and the resulting OAuth user token is stored in the town config. All subsequent git operations authenticate as that user.

Why Device Flow (not standard OAuth redirect)

The standard OAuth Authorization Code flow requires a redirect URI, which is awkward for a settings page that's already open. The Device Flow is designed for exactly this UX: the user authorizes in a separate browser tab and the token arrives via polling. No redirect, no popup, no copy-paste.

What the user token provides

Capability App Installation Token User OAuth Token
Commit GIT_AUTHOR Bot / app identity User's name + email
PR author on GitHub App User
Contribution graph credit No Yes
Access scope Repos where app is installed All repos user can access
gh CLI compatible Yes Yes
Token lifetime 1 hour (auto-refresh) Non-expiring (until revoked)

With the user token, GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL and GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL can both be resolved from the token's identity — or the agent can remain as committer while the user is the author (per #1001's original design). Either way, the token itself handles authentication.

UX Flow

  1. User opens Town Settings → Git Authentication section
  2. Clicks "Connect GitHub" button
  3. UI calls worker endpoint POST /api/towns/:townId/auth/github/device-code
  4. Worker calls POST https://github.com/login/device/code with the OAuth App's client_id and scopes (repo, workflow)
  5. GitHub returns device_code, user_code, verification_uri, interval
  6. UI displays: "Enter code ABCD-1234 at github.com/login/device" with a link that opens in a new tab
  7. Worker starts polling POST https://github.com/login/oauth/access_token with the device_code every interval seconds
  8. User enters the code in GitHub's UI and clicks "Authorize"
  9. Poll returns access_token + token_type + scope
  10. Worker stores the token in townConfig.git_auth.github_token and resolves the user's name/email via GET /user on the GitHub API
  11. UI shows "Connected as @username" with a disconnect button

Data stored

townConfig.git_auth = {
  github_token: string;           // OAuth user token
  github_username: string;        // Resolved from GET /user
  github_user_email: string;      // Resolved from GET /user/emails (primary)
  github_user_name: string;       // Resolved from GET /user (display name)
  connected_at: string;           // ISO timestamp
};

Agent dispatch changes

When dispatching any agent, pass the user's git identity to the container:

GIT_AUTHOR_NAME: townConfig.git_auth.github_user_name ?? agentName,
GIT_AUTHOR_EMAIL: townConfig.git_auth.github_user_email ?? `${agentName}@gastown.kilo.ai`,
GIT_COMMITTER_NAME: `${agentName} (${agentRole})`,
GIT_COMMITTER_EMAIL: `${agentName}@gastown.kilo.ai`,
GH_TOKEN: townConfig.git_auth.github_token,

This gives:

  • Author = the human (contribution credit, provenance)
  • Committer = the agent (traceability)
  • gh CLI = authenticated as the user (PRs opened by the user)

Commit trailers (from #1001)

Retain the polecat system prompt instruction to include trailers:

Co-authored-by: Toast (Polecat) <toast@gastown.kilo.ai>
Bead-ID: gt-abc-123

The Requested-by trailer from #1001 becomes unnecessary since GIT_AUTHOR already identifies the human.

Implementation

New endpoints

  • POST /api/towns/:townId/auth/github/device-code — Initiates the device flow, returns { userCode, verificationUri, expiresIn }
  • GET /api/towns/:townId/auth/github/poll — Polls for the token (called by UI on interval). Returns { status: 'pending' | 'complete', username? }
  • DELETE /api/towns/:townId/auth/github — Disconnects: clears the stored token

UI changes

  • Town Settings → Git Authentication section: replace the PAT text input with a "Connect GitHub" button
  • Connected state shows @username with avatar and a "Disconnect" button
  • Keep the manual PAT input as a fallback (collapsed/advanced section) for GitHub Enterprise or non-GitHub providers

Prerequisites

  • A GitHub OAuth App (or enable Device Flow on the existing GitHub App) with repo and workflow scopes
  • The OAuth App's client_id stored as a worker secret/env var
  • Device Flow enabled in the OAuth App settings on GitHub

Acceptance Criteria

  • "Connect GitHub" button in town settings initiates Device Flow
  • User authorizes in GitHub tab, token is stored automatically
  • Connected state shows username with disconnect option
  • Agent commits have GIT_AUTHOR = user, GIT_COMMITTER = agent
  • PRs created by agents show the user as author on GitHub
  • GitHub contribution graphs credit the user
  • gh CLI in the container authenticates as the user
  • Manual PAT entry still available as fallback
  • Token survives town restarts (persisted in townConfig)
  • Disconnect clears the token from townConfig

Notes

  • For org-scoped towns (feat(gastown): add org-level towns, rigs, and auth middleware #1153), different users may connect different GitHub accounts. The token is per-town, so the town creator's identity is used. Per-rig tokens could be a future extension.
  • GitLab has an equivalent Device Flow — this issue focuses on GitHub but the architecture should be extensible.
  • Non-expiring OAuth tokens can be revoked by the user at any time on GitHub (Settings → Applications). The agent should handle 401s gracefully and surface "reconnect needed" in the UI.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestkilo-auto-fixAuto-generated label by Kilokilo-triagedAuto-generated label by Kilo

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions