Status: in flight (App provisioning complete; agent dispatch wiring this PR) Tracking: issue #335 Owner: ops
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.
| Bot | Used by | Permissions |
|---|---|---|
raxx-dev-bot |
feature-developer, ux-polisher, ux-designer | Contents R/W, PRs R/W, Issues R/W, Workflows R/W |
raxx-ops-bot |
sre-agent, security-agent, card-groomer | Contents R, PRs R/W, Issues R/W, Code/Secret scanning alerts R/W |
raxx-pm-bot |
product-manager, software-architect, marketing-strategist, business-legal-researcher, data-scientist | Contents R/W, PRs R/W, Issues R/W, Discussions R/W |
Mapping is canonical at scripts/agents/agent_bot_map.yaml.
[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.
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:
APP_ID — numeric App ID from GitHubINSTALLATION_ID — numeric Installation ID after the App is installed on TradeMasterAPIPRIVATE_KEY_PEM — full PEM contents (BEGIN/END RSA PRIVATE KEY block)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:
INFISICAL_CLIENT_IDINFISICAL_CLIENT_SECRETINFISICAL_PROJECT_IDThis 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.
Two patterns are supported:
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).
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.
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.
The mint script can fail in three ways:
exit 2) — Infisical secrets not pulled. Operator runs the dispatcher with proper Infisical context.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.Never silently use the operator PAT without a logged warning. Auditability beats convenience.
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.
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.
When the GitHub organization is renamed or the repo is transferred to a new org, three things change and must be remediated in order:
POST /app/installations/{old_id}/access_tokens. Update INSTALLATION_ID in Infisical at /MooseQuest/<bot-name>/ for all three bots.github.com/organizations/<old-org>/ links in runbooks need to point to the new org. See docs/ops/runbooks/github-app-provisioning.md.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.TradeMasterAPI → note the new Installation ID from the URL.INSTALLATION_ID in Infisical at /MooseQuest/raxx-dev-bot/, /MooseQuest/raxx-ops-bot/, /MooseQuest/raxx-pm-bot/.scripts/agents/with_bot_token.sh raxx-ops-bot gh api /user — expect "login": "raxx-ops-bot[bot]".ghs_ (installation token), not ghp_ (PAT).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.
scripts/agents/mint_github_token.py — mint helperscripts/agents/with_bot_token.sh — wrapperscripts/agents/agent_bot_map.yaml — mapping configdocs/ops/runbooks/github-app-provisioning.md — provisioning runbookdocs/ops/runbooks/rotation/github-app-installation-token.md — rotation SOPdocs/incidents/2026-05-09-bot-token-mint-404.md — org migration that surfaced both the stale installation ID and the DEFAULT_PATH_PREFIX bug