Skip to content

Replace gh CLI dependency with GitHub App credentials in assistant-github#163

Open
gregology[bot] wants to merge 7 commits intomainfrom
github-app-auth
Open

Replace gh CLI dependency with GitHub App credentials in assistant-github#163
gregology[bot] wants to merge 7 commits intomainfrom
github-app-auth

Conversation

@gregology
Copy link
Contributor

@gregology gregology bot commented Mar 24, 2026

Why

Actions taken by the assistant (creating issues, eventually commenting on PRs) were showing up as the authenticated user because we shelled out to gh api and inherited whatever gh auth login session was active. Now that the integration creates issues on the user's behalf, it needs its own identity so there's a clear distinction between "the human did this" and "the bot did this."

GitHub App installation tokens solve this cleanly — the assistant authenticates as the App, and GitHub shows the App's identity on anything it creates.

The secondary motivation: removing the hard runtime dependency on gh being installed and authenticated. One less thing to go wrong during setup, one less thing for doctor to check.

Approach

Replaced all subprocess.run(["gh", "api", ...]) calls with direct HTTP requests via httpx. Authentication uses GitHub App credentials: generate a JWT (RS256, via PyJWT), exchange it for an installation token, use that token for all API calls. Fresh token on every GitHubClient instantiation — no caching needed given the 12h schedule interval.

The @me shorthand in search queries (a gh CLI convenience the raw API doesn't support) is replaced with an explicit github_user config field.

Alternatives considered

Keep gh CLI, just switch authgh supports authenticating as a GitHub App, but it's awkward (environment variables, no clean token lifecycle control). Doesn't actually remove the runtime dependency, which was half the point.

PyGithub — full-featured but heavy. We only use a handful of endpoints. httpx is a better fit since we already know exactly which API calls we need, and it gives us async support down the road if we want it.

Token caching with refresh — Installation tokens last 1 hour, but we only run every 12 hours. Generating a fresh token each time is simpler and avoids a whole class of stale-token bugs.

Review walkthrough

Each commit maps to one logical step:

  1. Add dependencies and update manifest schemapyproject.toml gets httpx and PyJWT[crypto]. manifest.yaml gets four new required config fields (github_user, app_id, installation_id, private_key).

  2. Rewrite GitHubClient to use httpx + GitHub App auth — The core change. _run_gh/_gh_api are gone, replaced by _request which uses httpx.Client with retry/backoff. Constructor takes credentials, generates JWT, fetches installation token. @me replaced with self._github_user in all search queries.

  3. Thread credentials to all GitHubClient call sites — Every handler that instantiates GitHubClient() now passes credentials from the integration config. Follows the same # type: ignore[attr-defined] pattern the email integration uses (dynamic config models from manifest schema).

  4. Update setup wizard and doctorsetup.py drops the gh CLI detection, prompts for App credentials and github_user instead, writes secrets via !secret references. doctor.py replaces the gh installed/authenticated check with a check that the App credentials can generate a valid installation token.

  5. Update tests — Mock surface changed from _gh_api/_run_gh to _request (and _generate_jwt/_fetch_installation_token during construction). Added tests for JWT generation, github_user substitution in search queries, and retry logic.

  6. Update documentation and examplesAGENTS.md, AGENTS.yaml, README.md, example.config.yaml, example.secrets.yaml all updated to reflect the new auth mechanism.

Closes #162

Developer metrics

Total duration: 23m 16s
Turns: 428
Tool calls: 336
Tokens: 6,927,217 input / 54,095 output

Stage Model Duration Turns Tool calls Tokens (in/out) Cache read Cache creation
triage claude-opus-4-6 1m 55s 58 55 63,496 / 1,363 46,953 16,539
decompose claude-opus-4-6 1m 55s 26 25 82,143 / 3,992 57,754 24,385
implement_step_1 claude-opus-4-6 0m 58s 26 20 139,885 / 1,673 119,291 20,587
implement_step_2 claude-opus-4-6 2m 39s 24 15 527,509 / 10,145 487,352 40,142
implement_step_3 claude-opus-4-6 6m 27s 113 76 3,122,640 / 18,302 3,041,393 81,200
implement_step_4 claude-opus-4-6 3m 45s 47 33 604,162 / 8,067 565,059 39,087
docs_review claude-opus-4-6 2m 9s 73 66 395,461 / 2,304 344,370 51,083
quality_fix claude-opus-4-6 1m 49s 44 31 1,229,792 / 6,543 1,144,058 85,719
craft_pr claude-opus-4-6 1m 34s 17 15 762,129 / 1,706 662,952 99,164

Resolves #162

gregology bot added 6 commits March 24, 2026 00:50
…hema with GitHub App fields

Two files change:

1. **`packages/assistant-github/pyproject.toml`** — add `PyJWT[crypto]` and `httpx` to the `dependencies` list alongside the existing `assistant-sdk>=0.1.0`.

2. **`packages/assistant-github/src/assistant_github/manifest.yaml`** — add four new properties to `config_schema.properties`: `github_user` (string), `app_id` (string), `installation_id` (string), `private_key` (string). All four should be listed in `config_schema.required`.
…ntication

Rewrite **`packages/assistant-github/src/assistant_github/client.py`**:

**Constructor** — accept `app_id: str`, `installation_id: str`, `private_key: str`, `github_user: str`. On init:
1. Create a JWT (RS256) with claims `iss=app_id`, `iat=now-60`, `exp=now+600` using `jwt.encode(payload, private_key, algorithm='RS256')`.
2. POST to `https://api.github.com/app/installations/{installation_id}/access_tokens` with `Authorization: Bearer <jwt>` to obtain an installation token.
3. Store the installation token and `github_user` as instance attributes.
4. Create an `httpx.Client` instance with base_url `https://api.github.com`, default headers `Authorization: Bearer <token>`, `Accept: application/vnd.github+json`, `X-GitHub-Api-Version: 2022-11-28`, and a reasonable timeout (30s).

**Replace `_run_gh` and `_gh_api`** with a single `_request(method, url, *, params=None, json=None, headers=None)` method that uses `self._http.request(...)` with the existing retry/backoff logic (3 attempts, exponential backoff 1s/2s/4s). Raise on non-2xx after retries. Return parsed JSON (or raw text for diff endpoints).

**Update public methods:**
- `get_pr`, `get_pr_detail`, `get_issue`, `get_issue_detail` — call `_request('GET', f'/repos/{org}/{repo}/pulls/{number}')` etc.
- `get_pr_diff` — call `_request('GET', ...)` with override header `Accept: application/vnd.github.v3.diff`, return raw text.
- `create_issue` — call `_request('POST', f'/repos/{org}/{repo}/issues', json={'title': title, 'body': body})`.
- `active_prs`, `active_issues` — replace all `@me` occurrences with `self._github_user` in search query strings. Use `_request('GET', '/search/issues', params={'q': query})` instead of `_gh_api`.
- `_search_raw` — adapt to use `_request` with the search endpoint.

**Remove** all `subprocess` imports and usage. Remove `_run_gh` and `_gh_api` methods entirely.
…etup wizard and doctor

**Handler call sites** — update all 5 files that instantiate `GitHubClient()` to pass credentials from the integration config:
- `platforms/pull_requests/check.py` (line 21)
- `platforms/pull_requests/collect.py` (line ~29)
- `platforms/issues/check.py` (line 21)
- `platforms/issues/collect.py` (line ~23)
- `services/create_issue.py` (line 38)

Each handler already receives an `integration` dict in its context/task. Extract `integration['app_id']`, `integration['installation_id']`, `integration['private_key']`, `integration['github_user']` and pass them to `GitHubClient(app_id=..., installation_id=..., private_key=..., github_user=...)`.

**`app/setup.py`** — in the `setup_github()` function:
- Remove the `gh` CLI detection and `gh auth status` check (lines ~212-226).
- Add prompts for: `github_user`, `app_id`, `installation_id`, and `private_key` (accept a path to a `.pem` file and read its contents).
- Store `private_key` in the secrets dict (matching the email pattern: `secrets[f'{name}_github_private_key'] = private_key_contents`). Store `app_id` and `installation_id` in secrets too.
- Return `(integrations, secrets)` tuple like `setup_email()` does (currently returns only integrations).
- Add `github_user` directly in the integration config dict.
- Use `!secret` references for the three credential fields in the integration config.

**`app/doctor.py`** — in `check_gh()` (lines ~111-128):
- Replace the gh CLI installed/authenticated check with a check that loads GitHub integration config, extracts app credentials, creates a JWT, and verifies the installation token endpoint returns 2xx.
- Update the diagnostic output messages accordingly.
**Tests** — update `packages/assistant-github/tests/test_client.py`:
- `GitHubClient` instantiation in tests needs mocking since the constructor now calls the GitHub API for an installation token. Mock `httpx.Client.post` (or patch `_request`) during construction to return a fake token.
- Replace all `@patch` targets from `_gh_api`/`_run_gh` to the new `_request` method.
- Existing test assertions for parsing logic (get_pr status derivation, get_issue labels, create_issue return values, _scope_qualifiers, _search_entities deduplication) should remain largely the same — just update the mock surface.
- Add new tests: (a) `github_user` substitution — verify that `active_prs` and `active_issues` build queries with the configured username instead of `@me`, (b) JWT generation — verify a JWT is created with correct claims (iss, iat, exp) and RS256 algorithm, (c) constructor calls the installation token endpoint.

**Documentation:**
- `packages/assistant-github/AGENTS.md` — remove references to `gh` CLI as the API client. Document that the client uses httpx with GitHub App installation tokens. Update the authentication section.
- `example.config.yaml` — add a GitHub integration example block with `github_user`, `app_id: !secret github_app_id`, `installation_id: !secret github_installation_id`, `private_key: !secret github_private_key`.
- `example.secrets.yaml` — add `github_app_id`, `github_installation_id`, `github_private_key` entries.
- `README.md` — update any GitHub setup instructions to reference GitHub App credentials instead of `gh auth login`.
Copy link
Owner

@gregology gregology left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we including github_secrets details in app/setup.py? These are concerns of the assistant-github package and it looks like we're tightly coupling that logic into the core app instead of respecting logical boundaries of what belongs in the app and what belongs in the packages

@gregology
Copy link
Contributor Author

gregology bot commented Mar 24, 2026

Good call — moved the GitHub credential prompting into assistant-github/setup.py and added a setup_hook field to the manifest so the core app discovers and calls it generically. Same pattern as handler and service registration: the manifest declares a dotted path, the core loads and invokes it.

The core setup wizard now iterates over discovered manifests, calls any declared setup hooks with a prompts adapter, and merges the results. No GitHub-specific knowledge left in app/setup.py. Email setup still lives in the core for now but could follow the same pattern later if needed.

Developer metrics

Total duration: 7m 37s
Turns: 136
Tool calls: 111
Tokens: 1,544,780 input / 10,133 output

Stage Model Duration Turns Tool calls Tokens (in/out) Cache read Cache creation
analyze_feedback claude-opus-4-6 1m 30s 36 35 115,559 / 630 82,373 33,182
implement claude-opus-4-6 5m 32s 94 72 981,541 / 8,826 936,571 44,947
quality_fix claude-opus-4-6 0m 21s 5 3 275,003 / 457 214,451 60,547
craft_update claude-opus-4-6 0m 13s 1 1 172,677 / 220 86,219 86,455

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.

Replace gh CLI dependency with GitHub App credentials in assistant-github

1 participant