Raxx · internal docs

internal · gated

Agent GitHub identity

Status: active (orchestrator injection wired — #2284) Tracking: issue #335, #2284 (SC-IDENT-2) Owner: ops

Why

When dispatched agents (feature-developer, sre-agent, card-groomer, etc.) make GitHub API calls, push commits, or open PRs/issues, every action authors as the operator (MooseQuest). That makes activity ambiguous on a busy day — was that PR comment from Kristerpher or from the security agent? — and pollutes the operator's notification feed with bot self-traffic.

This design replaces the operator's PAT with three GitHub Apps acting as bot identities, scoped per agent class.

The three bots

Bot Used by Permissions
raxx-dev-bot feature-developer, software-architect, ux-polisher, ux-designer, data-scientist Contents R/W, PRs R/W, Issues R/W, Workflows R/W
raxx-ops-bot sre-agent, security-agent, card-groomer, qa-agent Contents R, PRs R/W, Issues R/W, Code/Secret scanning alerts R/W
raxx-pm-bot product-manager, marketing-strategist, business-legal-researcher Contents R/W, PRs R/W, Issues R/W, Discussions R/W

Mapping is canonical at scripts/agents/agent_bot_map.yaml.

Token-mint flow

[App ID] + [Installation ID] + [Private key PEM]   ← from Infisical
                  ↓
       Sign a 9-min JWT (RS256, iss = App ID)
                  ↓
   POST /app/installations/{id}/access_tokens
                  ↓
       Installation token (1-hour validity)
                  ↓
       Set GH_TOKEN, run gh CLI / git push

Implemented at scripts/agents/mint_github_token.py.

Secrets storage

Single source of truth: Infisical at /MooseQuest/<bot-name>/ (Infisical project MooseQuest; this path is independent of the GitHub org name). Three secrets per bot:

No local secret cache. Bot keys never live on the operator's disk. The mint script pulls them live from Infisical on every invocation via Universal Auth.

The only thing in the operator's shell environment is the Machine Identity that authenticates to Infisical:

This decouples bot key rotation from operator-workstation state. Rotating a bot's PEM in Infisical takes effect on the next agent dispatch — no local file update, no re-source-of-config, no operator-side action.

Trade-off: each gh call routed through the wrapper takes ~500ms–1s (Infisical login + secrets fetch + GitHub token mint). For agents making ≥3 gh calls in a row, the session pattern (eval "$(... --export)") caches the resulting GitHub token for its 1-hour lifetime, amortizing the network cost.

Setup runbook: docs/ops/runbooks/agent-bot-tokens-setup.md.

Agent dispatch pattern

Three patterns are supported, from most to least preferred:

A. Orchestrator spawn-time injection (preferred for automated dispatch)

When the orchestrator dispatches an agent, it calls scripts/orchestrator/agent_token_injector.get_bot_token_for_agent() to resolve the agent class to the correct bot, mint a cached installation token, and inject it as GH_TOKEN in the agent's environment before spawn:

from scripts.orchestrator.agent_token_injector import get_bot_token_for_agent

token = get_bot_token_for_agent("feature-developer")
# pass token as GH_TOKEN in the agent's env dict at spawn time

The token is cached per-bot for 50 minutes (tokens expire at 60 min; the 10-min buffer prevents near-expiry hand-offs mid-agent-run). If the agent class has no entry in scripts/agents/agent_bot_map.yaml, UnknownAgentError is raised and the orchestrator must block the dispatch rather than silently falling back to the operator PAT. Silent fallback breaks the attribution model — see ADR-0096 for the full rationale.

Agent-class → bot mapping (canonical at scripts/agents/agent_bot_map.yaml):

Agent class Bot
feature-developer, software-architect, ux-polisher, ux-designer, data-scientist raxx-dev-bot
sre-agent, security-agent, card-groomer, qa-agent raxx-ops-bot
product-manager, marketing-strategist, business-legal-researcher raxx-pm-bot

The CI job agent-bot-map-coverage (#2284, SC-IDENT-2) enforces that every .claude/agents/*.md file has a matching entry in the map, so the orchestrator never encounters an unregistered class in production.

B. Single command (manual / agent-initiated)

scripts/agents/with_bot_token.sh raxx-dev-bot gh pr create --title "..."

The wrapper mints a fresh token, sets GH_TOKEN for one command, runs the command, and discards the token. Falls back to the operator's PAT if the mint fails (logged warning to stderr).

C. Session-scoped (manual / agent-initiated, ≥3 gh calls)

eval "$(python scripts/agents/mint_github_token.py --bot raxx-ops-bot --export)"
gh issue create ...
gh pr comment ...

GH_TOKEN stays in the shell environment for ≤1 hour. Use this when an agent makes ≥3 gh calls in quick succession.

Per-agent integration

Each agent's .claude/agents/<agent>.md system prompt includes a 5-line "GitHub identity" preamble pointing to its bot. Agents are responsible for using the wrapper or session pattern before any gh CLI call.

Failure modes + fallback

The mint script can fail in three ways:

  1. Missing env vars (exit 2) — Infisical secrets not pulled. Operator runs the dispatcher with proper Infisical context.
  2. GitHub API rejection (exit 3) — App credentials wrong, App revoked, scope mismatch. Cause is in the response body. Falls through to the operator PAT (with warning) so the agent can still complete its task — surface the warning so the operator notices.
  3. Network failure — Same as #2; agent gets a warning + falls back to PAT.

Never silently use the operator PAT without a logged warning. Auditability beats convenience.

Git commit attribution (deferred)

Bot identities for GitHub API calls ship in this PR. Bot identities for commit author require setting git config user.name "raxx-dev-bot[bot]" + user.email "<bot-user-id>+raxx-dev-bot[bot]@users.noreply.github.com" per-worktree. That requires looking up the bot's user ID after install (gh api /users/raxx-dev-bot%5Bbot%5D) and is tracked as a follow-up. v1 ships with API-call attribution only; commit attribution stays with the operator.

Rotation

Private keys rotate per the existing SOP at docs/ops/runbooks/rotation/github-app-installation-token.md. Cadence: 365 days per bot. The rotation pipeline (#300, #331) will eventually surface this in the console rotation UI.

GitHub org migration checklist

When the GitHub organization is renamed or the repo is transferred to a new org, three things change and must be remediated in order:

  1. App installations do not migrate automatically. GitHub App installations are scoped to an org. After an org rename or ownership transfer, every App must be re-installed on the new org. The App definition (App ID + private key) stays intact; only the installation (and its ID) changes.
  2. Installation IDs change. Each re-installation on the new org creates a new installation ID. The old ID returns 404 from POST /app/installations/{old_id}/access_tokens. Update INSTALLATION_ID in Infisical at /MooseQuest/<bot-name>/ for all three bots.
  3. Provisioning runbook URLs must be updated. Any hardcoded github.com/organizations/<old-org>/ links in runbooks need to point to the new org. See docs/ops/runbooks/github-app-provisioning.md.

Remediation procedure for org migration

  1. In the GitHub UI (https://github.com/organizations/raxx-app/settings/apps), confirm the three Apps (raxx-dev-bot, raxx-ops-bot, raxx-pm-bot) appear. If not, re-provision from scratch per docs/ops/runbooks/github-app-provisioning.md.
  2. For each App: go to Install App → install on the new org → select TradeMasterAPI → note the new Installation ID from the URL.
  3. Update INSTALLATION_ID in Infisical at /MooseQuest/raxx-dev-bot/, /MooseQuest/raxx-ops-bot/, /MooseQuest/raxx-pm-bot/.
  4. Smoke-test: scripts/agents/with_bot_token.sh raxx-ops-bot gh api /user — expect "login": "raxx-ops-bot[bot]".
  5. Verify token format starts with ghs_ (installation token), not ghp_ (PAT).

Vault path

Secrets live at /MooseQuest/<bot-name>/ in Infisical (Infisical project MooseQuest). The /MooseQuest/ prefix is the vault folder hierarchy and is independent of the GitHub org name — it does not need to change when the GitHub org is renamed.

The mint script reads INFISICAL_PATH_PREFIX from env (default /MooseQuest/). The effective secret path is {prefix}/{bot}, e.g. /MooseQuest/raxx-dev-bot.

Refs