Agent bot-token setup runbook
Purpose: Wire up programmatic Infisical access so dispatched agents authenticate as raxx-{dev,ops,pm}-bot instead of the operator's PAT.
Audience: Kristerpher (operator) — run once after the GitHub Apps are provisioned per docs/ops/runbooks/github-app-provisioning.md and the secrets are stored in Infisical.
Time: ~10 minutes (5 of those waiting for Infisical UI to load).
Tracking: Issue #335.
How it works
[Your shell env] [Infisical] [GitHub]
INFISICAL_CLIENT_ID /MooseQuest/raxx-dev-bot/ App 3501352
INFISICAL_CLIENT_SECRET APP_ID ↓
INFISICAL_PROJECT_ID INSTALLATION_ID Installation
PRIVATE_KEY_PEM token (1hr)
↓ ↓ ↓
└─── Universal Auth login ────────────────────┘ │
↓ │
session token (~10 min) │
↓ │
GET /api/v3/secrets/raw?path=/MooseQuest/raxx-dev-bot ──────────┘
↓
[APP_ID, INSTALLATION_ID, PRIVATE_KEY_PEM]
↓
sign 9-min JWT (RS256)
↓
POST /app/installations/{id}/access_tokens
↓
GH_TOKEN=ghs_...
The mint script makes one Infisical login + one secrets fetch + one GitHub token mint per invocation. ~500ms-1s per gh call when used as a one-shot. For agents making ≥3 gh calls in a row, use the session pattern (eval "$(... --export)") to reuse the GitHub token for its 1-hour lifetime.
The only thing on your laptop is the Universal Auth client credentials — three env vars. Bot keys never leave Infisical except in transit.
Step 1 — Create the Infisical Machine Identity
A "Machine Identity" with Universal Auth is the agent dispatcher's identity in Infisical. Create one:
- Open Infisical → Access Control → Identities → Create Identity
- Name:
raxx-agent-dispatcher - Auth method: Universal Auth
- After creation, on the identity's page click Create Client Secret — copy the client ID and client secret (the secret is shown ONCE).
Grant the identity access to the bot paths
Still on the identity page → Project Roles → assign access to your project:
- Project:
MooseQuest(or whichever Infisical project holds the bot secrets — copy its Project ID from project settings) - Role: custom role with
readon/MooseQuest/raxx-dev-bot/*,/MooseQuest/raxx-ops-bot/*,/MooseQuest/raxx-pm-bot/*(or a more permissive built-in role likeviewerif you trust the dispatcher with broader read access) - Environment:
prod(or whichever environment the bot secrets live in — slug is what we'll set asINFISICAL_ENV)
The identity needs ONLY read on the bot paths. Don't grant write — the rotation pipeline writes; the dispatcher only reads.
Step 2 — Verify the bot secrets are in Infisical
Each bot's path should hold three secrets with these exact names:
/MooseQuest/raxx-dev-bot/
APP_ID = 3501352
INSTALLATION_ID = <numeric installation id>
PRIVATE_KEY_PEM = -----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----
(Same shape for raxx-ops-bot and raxx-pm-bot.)
If you stored them under different names earlier (e.g., app-id lowercase per the original provisioning runbook), rename them — POSIX env var conventions break with hyphens, and the mint script expects APP_ID / INSTALLATION_ID / PRIVATE_KEY_PEM exactly.
Step 2.5 — (Self-hosted Infisical only) Cloudflare Access service token
If your Infisical instance is behind Cloudflare Access (e.g., Raxx's vault.raxx.app), the API requests need to bypass CF Access too. Skip this step if you're using Infisical Cloud at app.infisical.com.
- Cloudflare Zero Trust dashboard → Access → Service Auth → Service Tokens → Create Service Token
- Name:
raxx-agent-vault-access - Duration: 1 year (matches the bot key rotation cadence)
- Copy the Client ID and Client Secret (the secret is shown ONCE)
- Apply to your Access app: Access → Applications → click the app fronting Infisical (e.g.,
vault.raxx.app) → Policies → Add policy - Action: Service Auth - Include: Service Token → pickraxx-agent-vault-access- Save - Set two more env vars in your shell (per Step 3 below):
bash export CF_ACCESS_CLIENT_ID='<service-token-client-id>' export CF_ACCESS_CLIENT_SECRET='<service-token-client-secret>'
If the mint script sees both CF_ACCESS_CLIENT_ID and CF_ACCESS_CLIENT_SECRET in env, it adds CF-Access-Client-Id and CF-Access-Client-Secret headers to every Infisical API call. If only one is set, the script ignores both (would 403 anyway).
Gotcha: Service Auth policies cannot be created inline from the Access app's policy builder — you have to create the policy as type "Service Auth" first in Access → Service Auth, then attach it to the Access app's Policies tab. Cloudflare's UI doesn't make this obvious.
Step 3 — Set the env vars in your shell
These env vars need to be in any shell that dispatches agents. Pick whichever pattern you already use:
Option A — direnv (recommended)
.envrc in the repo root (gitignored — it's already in .gitignore patterns):
# Infisical Universal Auth
export INFISICAL_CLIENT_ID='<machine-identity-client-id>'
export INFISICAL_CLIENT_SECRET='<client-secret-shown-once>'
export INFISICAL_PROJECT_ID='<your-infisical-project-id>'
# Self-hosted Infisical override (if using vault.raxx.app):
export INFISICAL_HOST='https://vault.raxx.app'
# Cloudflare Access bypass (only if Infisical is behind CF Access):
export CF_ACCESS_CLIENT_ID='<cf-service-token-id>'
export CF_ACCESS_CLIENT_SECRET='<cf-service-token-secret>'
# Optional defaults:
# export INFISICAL_ENV='prod' # default
# export INFISICAL_PATH_PREFIX='/MooseQuest/' # default
direnv allow after writing.
Option B — ~/.zshrc / ~/.bashrc
Same export lines but in your shell rc. Means every shell you open has the dispatcher creds. Slightly less secure than direnv (broader env exposure) but simpler if you're not already using direnv.
Option C — 1Password CLI / op secret references
If you already use 1Password for shell secrets:
export INFISICAL_CLIENT_ID="$(op read 'op://Private/raxx-agent-dispatcher/client-id')"
export INFISICAL_CLIENT_SECRET="$(op read 'op://Private/raxx-agent-dispatcher/client-secret')"
export INFISICAL_PROJECT_ID='<project-id>' # not a secret
op will prompt for biometric on first use per session.
Step 4 — Smoke test
# In a shell where the env vars are set:
scripts/agents/with_bot_token.sh raxx-dev-bot gh api /user
Expected: JSON for the bot user (look for "login": "raxx-dev-bot[bot]").
Common failures:
| Symptom | Cause | Fix |
|---|---|---|
INFISICAL_* env vars not set; falling back to operator PAT |
Env vars aren't reaching the wrapper | Reload your shell, or direnv reload, or check option A/B/C |
Infisical login failed: HTTP 302 |
Cloudflare Access is in front of the host but no CF Access service token is configured | Set CF_ACCESS_CLIENT_ID + CF_ACCESS_CLIENT_SECRET per Step 2.5 |
Infisical login failed: HTTP 401 |
Wrong client ID/secret, or identity not in the right project | Verify creds; check the identity's project assignments |
Infisical login failed: HTTP 403 |
CF Access service token is set but not authorized for the host's Access policy | In Cloudflare → Access → Applications → Policies, attach the Service Auth policy that includes the agent-dispatcher service token |
Infisical fetch failed at /MooseQuest/raxx-dev-bot: HTTP 403 |
Identity has the wrong scopes / not granted access to that path | Add read permission on the bot path in Infisical |
bot secrets at /MooseQuest/raxx-dev-bot/ missing keys: PRIVATE_KEY_PEM |
Secret stored under a different name | Rename in Infisical to APP_ID / INSTALLATION_ID / PRIVATE_KEY_PEM |
GitHub API returned 401 |
App revoked, or Installation ID is wrong, or PEM corrupted | Check the App's installation page on GitHub; re-download the PEM if needed |
If you can't decode the failure, run the mint script directly without --quiet:
python3 scripts/agents/mint_github_token.py --bot raxx-dev-bot
That prints the full error chain to stderr.
Rotating credentials
Bot key (PEM) rotation: Per the rotation SOP at docs/ops/runbooks/rotation/github-app-installation-token.md. Update the PEM in Infisical at the bot's path. Next agent dispatch picks up the new key automatically — no local change needed. This is the whole point of the Infisical pull.
Universal Auth client secret rotation: The Machine Identity's client secret should rotate ~365 days. Procedure:
- In Infisical: identity page → Create Client Secret (a second one) → copy
- Update
INFISICAL_CLIENT_SECRETin your shell config (direnv / zshrc / 1password) - Reload shell, smoke-test
- Once the new secret works, revoke the old client secret in Infisical
You can have two client secrets active simultaneously to make the rotation seamless.
Security notes
- Universal Auth client secret IS a secret. Treat it like a password. Don't paste into Slack, don't put in the repo, don't leave in
~/Downloads/. - The Machine Identity should have read-only access to the bot paths. If it has write access, a compromised dispatcher could overwrite bot keys with attacker-controlled ones.
- Audit the Machine Identity's activity periodically: Infisical → identity → audit log. Look for fetches outside expected dispatch windows.
- If the client secret leaks: revoke it in Infisical immediately, then rotate.
References
docs/architecture/agent-github-identity.md— design + dispatch patternsdocs/ops/runbooks/github-app-provisioning.md— GitHub App creationdocs/ops/runbooks/rotation/github-app-installation-token.md— bot key rotationscripts/agents/with_bot_token.sh— the wrapperscripts/agents/mint_github_token.py— the mint helper (Infisical pull + JWT mint)- Issue #335 — implementation tracking
- Infisical Universal Auth docs
- Infisical secrets API