Skip to content

GitHub App identity: amico[bot] acts for Amicode on harmoniqs repos (token core + gh/git shims) #399

Description

@aarontrowbridge

Important

Problem

Amicode's GitHub actions (issue/PR creation via gh, the handoff verb, repo-sync) all run under the researcher's personal gh login, so automation is indistinguishable from the human in the audit log, PRs attribute to a person, and there is no scoped revocable credential for agent activity.

Approach

Give Amicode its own GitHub identity via an org-level GitHub App ("Amico", installed on all harmoniqs repos): a pure token-minting core in amico-run plus PATH-shim launchers (amico-gh, amico-git-credential) in the extension's launcher bin dir, so every gh/git push in an agent session authenticates as amico[bot] with zero prompt changes. Commit authorship stays the researcher's — only PR/issue creation and pushes carry the bot identity (the Claude Code split).

Approaches Considered

  • GitHub App + launchers (chosen) — canonical automation identity, self-refreshing 1-hour installation tokens, per-org scoping, instant PEM kill switch.
  • Machine user account + PAT — zero code to start, but expiring tokens, seat cost, reads as human in audit log.
  • github-actions bot token — free but repo-scoped, ephemeral, only exists inside Actions runs.

Scope

In: credential-file contract (~/.amico/github.json + token cache), JWT mint + installation-token fetch + cache-with-skew (pure core), amico-gh PATH shim with transparent fallback to real gh, git credential helper injected via spawn-env git config when configured, hermetic unit tests, draft PR.
Out: Connection-panel UI (Phase 2, separate issue) — this lands headless; PEM upload UX follows the existing credential-file patterns. Out: fleet propagation (Phase 3). Out: webhook/event handling — the App is credential-only, no callbacks.

Assumptions / Open Qs

Org owner creates the App and grants contents-RW, pull-requests-RW, issues-RW, members-R, administration-RW; installation covers all repos including the private opencode fork. Node crypto RS256 signing is acceptable (no new deps).


Acceptance Criteria

  1. ~/.amico/github.json absent → amico-gh execs the real gh with no env changes (byte-identical passthrough; today's behavior).
  2. Config present → shim mints an installation token (RS256 JWT → access-token endpoint), exports GH_TOKEN, execs real gh; token cached in ~/.amico/github-token.json (0600) and reused until 5 minutes of life remain.
  3. Malformed config / unreadable PEM / mint failure → distinct, token-free, actionable errors on stderr with nonzero exit; never prints the PEM, JWT, or token.
  4. JWT payload: iss = app id, iat/exp inside GitHub's 10-minute window with 60s negative clock skew; header alg RS256, typ JWT.
  5. Git credential helper answers get for github.com over https with x-access-token + the minted token; silent exit (git falls through) when unconfigured or for other hosts.
  6. Spawn env gains the credential-helper git config ONLY when the connection file exists; absent file → no GIT_CONFIG vars added (regression-safe).
  7. All logic unit-tested hermetically (no network): fetch and child-process seams injectable, matching the pasqal_launch/handoff test style.
  8. Test suite + lint pass with no regressions.

Key Decisions

  • Bot PRs, human-authored commits: no commit-identity changes anywhere; only transport auth changes.
  • PATH-shim interception (not rewriting call sites): every current and future gh call — agent sessions, handoff verb, repo-sync — becomes the bot with no code changes at call sites.
  • The shim must resolve the real gh by scanning PATH minus its own directory (a bare exec would recurse into itself).
  • Env overrides follow house style: AMICO_GITHUB_FILE, AMICO_GITHUB_TOKEN_FILE (mirroring AMICO_PASQAL_FILE).
  • ssh remotes are untouched: the credential helper only serves https github.com; pushes over ssh keep working as the user (authorship is commit metadata either way).
  • Pure core in amico-run (no I/O in the logic module; file/exec/fetch seams injected), launchers as thin executables.

Constraints & Invariants

  • Never log or embed secrets (PEM, JWT, token) in errors, stdout metadata, or test fixtures — errors are token-free by construction.
  • Credential file must never be committed; tests point env overrides at temp dirs.
  • No new npm dependencies: JWT signing via node:crypto.
Prior Art / Patterns
  • packages/amico-run/src/pasqal_launch.ts — credential-file shape-check style: distinct ConfigError classes, token-free actionable messages, env override for the file path.
  • packages/amico-run/src/handoff.ts — pure-core + injected gh api exec seam, hermetic tests (handoff_verb.test.ts).
  • packages/extension/src/server_auth.ts buildServerSpawnEnv — the spawn-env seam that already prepends the launcher bin dir to PATH; GIT_CONFIG injection goes here, gated on config-file existence (sync stat, telemetry-resolver precedent).
  • packages/amico-run/src/pasqal_devices.ts~/.amico/*.json + env-override config resolution.
  • packages/amico-run/test/pasqal_launch.test.ts — recording-spawn stub pattern for exec seams.

Source

  • Resolved in session (plan of record): GitHub App route, all-harmoniqs-repos install, bot-PRs/human-commits — user-confirmed 2026-08-17.
  • Claude Code's /install-github-app as the external pattern being adapted.

Notes

Phase 0 (manual, org owner): create the org App, set permissions, install on all repos, record App ID + Installation ID, place PEM. Phase 2 (Connection panel) and Phase 3 (fleet) follow as separate issues. Revoking the PEM is the kill switch.

Metadata

Metadata

Labels

area:cliarea:nodehitlHuman-in-the-loop — needs human review/decision

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions