Skip to content

Add GitHub App OAuth device flow for gh CLI authentication#50

Draft
murrayju wants to merge 4 commits intomainfrom
hermes/gh-app-auth
Draft

Add GitHub App OAuth device flow for gh CLI authentication#50
murrayju wants to merge 4 commits intomainfrom
hermes/gh-app-auth

Conversation

@murrayju
Copy link
Member

Summary

  • Replace the Docker-based gh auth login device flow with a native GitHub App OAuth device flow
  • GitHub actions (PRs, pushes, comments) from the sandbox now appear with "via Hermes" attribution
  • hermes auth login gh no longer requires Docker — the device flow is pure HTTP
  • Add hermes auth logout gh command to clear stored credentials

How It Works

  1. Timescale registers a single GitHub App (e.g., "Hermes") — one app for all users
  2. Users run hermes auth login gh and go through the standard OAuth device flow (enter a code at github.com/login/device)
  3. The resulting user access token (ghu_...) is stored in the OS keyring and injected into the sandbox's ~/.config/gh/hosts.yml
  4. All GitHub API actions performed by gh CLI in the sandbox are attributed to the user "via Hermes"

Architecture

  • No server-side secrets needed — only the app's public client_id is hardcoded in hermes
  • No Docker for auth — the device flow is implemented as direct HTTP calls (fetch)
  • Token doesn't expire — the app should have token expiration disabled (opt-out in GitHub App settings)
  • Credential priority: GitHub App token > host gh auth > hermes keyring cache

Prerequisites (before this is usable)

  1. Register a GitHub App on the timescale org with:
    • Permissions: Contents R/W, Pull Requests R/W, Metadata R/O (minimum)
    • Device flow enabled
    • Token expiration disabled (opt-out)
    • Public visibility
  2. Update the GITHUB_APP_CLIENT_ID constant in src/services/githubApp.ts with the real client ID

Files Changed

File Change
src/services/githubApp.ts New — Device flow HTTP calls, token storage/validation, client ID constant
src/services/githubAppAuth.ts New — Auth process orchestration (start device flow + poll for token)
src/services/githubApp.test.ts New — Basic tests for the client ID constant
src/services/gh.ts GitHub App token added as highest-priority credential source
src/components/GhAuth.tsx Uses native device flow instead of Docker-based gh auth login
src/commands/config.tsx Config wizard uses new auth flow
src/commands/auth.ts gh auth no longer requires Docker; added logout subcommand

What Does NOT Change

  • Sandbox Dockerfiles / cloud snapshots — no changes needed
  • gh CLI inside the sandbox — still reads hosts.yml, doesn't know the difference
  • Git commit identity — still "Hermes Agent"
  • The old ghAuth.ts (Docker-based flow) is preserved but no longer imported

@CLAassistant
Copy link

CLAassistant commented Feb 25, 2026

CLA assistant check
All committers have signed the CLA.

Hermes Agent added 3 commits February 25, 2026 15:09
Replace the Docker-based `gh auth login` flow with a native GitHub App
OAuth device flow. This lets GitHub actions (PRs, pushes, comments) from
the sandbox appear with 'via Hermes' attribution on the user's actions.

Key changes:
- New githubApp.ts service: device flow HTTP calls, token storage/validation
- New githubAppAuth.ts: auth process orchestration (start + polling)
- gh.ts: GitHub App token is now highest-priority credential source
- GhAuth.tsx: uses native device flow instead of Docker container
- config.tsx: updated to use new auth flow
- auth.ts: added logout command, gh auth no longer requires Docker

The flow uses only the app's public client_id (no server-side secrets).
Tokens are stored in the OS keyring and don't expire. The old ghAuth.ts
(Docker-based flow) is preserved but no longer imported anywhere.
The login flow was short-circuiting on host gh credentials, making it
impossible to enter the GitHub App device flow when the user already
had gh CLI configured on the host. The fallback to host credentials
belongs in the credential resolution layer (gh.ts), not in the login
flow.
After the OAuth device flow, check whether the Hermes app is installed
on any orgs/repos. If not, open the installation URL in the browser and
prompt the user to install. This happens in three places:

- Post-auth: after device flow completes, check hasAnyInstallation()
  and show install prompt if needed (GhAuth.tsx, config.tsx)
- Session start (TUI): in startSession(), verify the GitHub App token
  has access to the target repo before creating the sandbox (sessions.tsx)
- Session start (CLI): same check in branchAction() (branch.ts)

New functions in githubApp.ts:
- hasAnyInstallation(token): checks GET /user/installations
- checkRepoAccess(token, repoFullName): checks GET /repos/{owner}/{repo}
- GITHUB_APP_SLUG, GITHUB_APP_INSTALL_URL constants

The auth process now returns a GithubAppAuthResult with a
needsInstallation flag so callers can react appropriately.
@murrayju murrayju marked this pull request as draft February 26, 2026 04:19
@murrayju
Copy link
Member Author

Findings: "via hermes-cli" attribution not appearing on PRs

Problem

After completing the GitHub App OAuth device flow and confirming a ghu_ (GitHub App user access token) is stored in the keyring and injected into the container, PRs created by the agent do not show the expected "via hermes-cli" attribution badge in the GitHub UI. The performed_via_github_app field on the PR is null.

Root Cause

The gh CLI hardcodes Authorization: token <TOKEN> for all internal API requests (see api/http_client.go:AddAuthTokenHeader). GitHub's API treats the token and Bearer schemes differently:

  • Authorization: Bearer ghu_xxx → GitHub recognizes the token as a GitHub App user-to-server token → app attribution appears (performed_via_github_app is populated, "via hermes-cli" badge shows in UI)
  • Authorization: token ghu_xxx → GitHub treats it as a generic OAuth token → no app attribution

Evidence

  1. hermes gh auth token confirms the container receives the ghu_ token
  2. hermes gh auth status confirms gh CLI is using it
  3. Direct curl with Authorization: Bearer ghu_xxx to create an issue comment → performed_via_github_app is populated with hermes-cli app details, and the "via hermes-cli" badge appears in the GitHub UI
  4. gh pr create using the same token → performed_via_github_app is null, no badge

Upstream Issue

This is a known issue in the gh CLI:

Status

Putting this PR on hold until the upstream gh CLI issue is resolved. The code changes in this PR (device flow auth, token refresh) are correct and ready, but the "via" attribution — which is the primary user-facing motivation — is blocked by the gh CLI's auth header format.

Possible Workarounds (if we want to unblock before upstream fix)

  1. Add a hermes-cli label to PRs via raw API (using Bearer auth) after the agent creates them
  2. Post a comment on the PR via raw API so at least one element has the "via" badge
  3. Instruct agents to use gh api with explicit Bearer header instead of gh pr create

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