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.
[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.
A "Machine Identity" with Universal Auth is the agent dispatcher's identity in Infisical. Create one:
raxx-agent-dispatcherStill on the identity page → Project Roles → assign access to your project:
MooseQuest (or whichever Infisical project holds the bot secrets — copy its Project ID from project settings)read on /MooseQuest/raxx-dev-bot/*, /MooseQuest/raxx-ops-bot/*, /MooseQuest/raxx-pm-bot/* (or a more permissive built-in role like viewer if you trust the dispatcher with broader read access)prod (or whichever environment the bot secrets live in — slug is what we'll set as INFISICAL_ENV)The identity needs ONLY read on the bot paths. Don't grant write — the rotation pipeline writes; the dispatcher only reads.
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.
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.
raxx-agent-vault-accessvault.raxx.app) → Policies → Add policy
- Action: Service Auth
- Include: Service Token → pick raxx-agent-vault-access
- Savebash
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.
These env vars need to be in any shell that dispatches agents. Pick whichever pattern you already use:
.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.
~/.zshrc / ~/.bashrcSame 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.
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.
# 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.
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:
INFISICAL_CLIENT_SECRET in your shell config (direnv / zshrc / 1password)You can have two client secrets active simultaneously to make the rotation seamless.
~/Downloads/.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)