ADR 0131 — Handoff Agent Least-Privilege Identity Model
Status: Proposed Date: 2026-06-20 UTC Deciders: software-architect (proposed); operator decision required on OQ1 and OQ2 Scope: All handoff agent dispatches — Claude Cloud / CCR scheduled agents, new-repo agents, any unattended autonomous task — across every credential surface they touch. Extends ADR-0130 (vault surface) to the full surface inventory.
Context
ADR-0130 established the per-agent Infisical machine identity model: each agent class gets its own scoped, read-only, independently revocable vault identity. That decision was scoped to Infisical.
A handoff agent — any Claude Cloud, scheduled CCR, new-repo, or unattended task — touches additional credential surfaces: GitHub (repo write), cloud provider APIs (Heroku, AWS, Cloudflare, Stripe), Apple IAP, and MCP tool bindings. Today those surfaces are either shared admin credentials or inherited ambient context.
The operator has stated an explicit hard rule: "anything handed off to any agent — a Claude Cloud / scheduled CCR agent, a new repo's agent, any unattended task — must get its OWN minted, least-privilege, independently-revocable identity; NEVER the shared admin identity or blanket permissions."
This ADR records the decision on how to implement that rule across the full surface inventory, and how provisioning, revocation, and the dispatch gate are enforced.
The full design is in docs/architecture/handoff-agent-least-privilege-identity.md.
Decision
Adopt the Handoff Agent Least-Privilege Identity Model as described in the companion design doc, with the following binding commitments:
-
HANDOFF-INVARIANT-1 is a hard invariant. No handoff agent ever holds the admin identity or any blanket-scope credential. Every handoff agent's access is: least-privilege, scoped, time-bound, independently revocable, and audited. This invariant extends HANDOFF-INVARIANT-1 stated in the design doc and is enforced at dispatch time.
-
Step 0 is not optional. Every handoff dispatch begins with provisioning the scoped identity set for that specific task. The orchestrator will not dispatch a handoff without confirming the PROVISION_MANIFEST in vault. Until the automated script (SC-HANDOFF-GATE-01) ships, the manual provisioning checklist in §6.2 of the design doc is the required gate.
-
Infisical vault path for handoffs. All handoff credentials are stored at
/MooseQuest/handoffs/<task-slug>/in Infisical. No handoff identity can read any other path without explicit provisioning. The/MooseQuest/handoffs/folder must be created before first use. -
GitHub scoping. Handoff agents use fine-grained PATs (interim) or per-handoff GitHub App install scoping (target state, SC-HANDOFF-GITHUB-01). Permissions: contents:write, pull_requests:write, issues:write — nothing else. No protected-branch push, no admin, no secrets:read, no workflows:write.
-
Financial credentials default to test/sandbox. No handoff agent receives a live financial credential (Stripe live key, Apple production IAP key, live broker OAuth token) without explicit operator approval recorded in the provisioning manifest. This is an enforcement of platform invariant HANDOFF-INVARIANT-1 H7.
-
MCP tool allowlist. The handoff agent's
session_contextmust contain an explicit MCP tool allowlist. Tools not on the list are not available to the session. Ambient session context inheritance is not acceptable for handoffs. -
Velvet integration for TTL > 1 hour. Handoff credentials with an expected task duration exceeding 1 hour are enrolled in the Velvet subscription manifest with an
auto_revoke_afterfield (SC-HANDOFF-VELVET-01). Short tasks use time-bounded surface tokens and manual revocation on task completion. -
Naming convention is mandatory. Handoff identities follow
<surface>-handoff-<task-slug>-<YYYYMMDD>. The task-slug matches the GitHub issue slug. This allows any handoff identity to be traced back to the card and revoked without searching.
Language choice rationale
No new service introduced. This ADR introduces a provisioning script
(scripts/agents/provision_handoff_identity.py) and orchestrator extensions, not a
new independently deployable service. These are Tier 2 (Python) tooling additions:
they are not on the auth hot path, have no p99 < 5ms budget, and have no
memory-safety-critical properties. The provisioning script runs at task-dispatch time
(operator-initiated), not in a real-time request path.
API contract portability: the provisioning script's output is a structured env block (JSON) injected by the orchestrator. This contract is language-agnostic and would be preserved in any future port.
Consequences
Positive
- Any compromise of a handoff identity has bounded blast radius: only the task's specific resources are exposed, not the platform admin identity or any other agent.
- Independent revocation is possible per handoff, per surface, without disrupting any running service or other agent.
- Test/sandbox financial credential default prevents a misbehaving handoff from touching live money or live customer data.
- The PROVISION_MANIFEST gate provides an audit record of what credentials were issued for each handoff, enabling forensic reconstruction after the fact.
- The MCP tool allowlist prevents a handoff from calling tools outside its task scope, even if the session context would otherwise allow them.
Negative / risks
- Provisioning friction increases before the automated script ships. Each handoff currently requires manual provisioning steps. For a team at this size, this is manageable but notable.
- Fine-grained PAT interim solution (OQ1 in design doc) still attributes the PAT to the raxx-dev-bot account rather than a truly isolated per-handoff identity. The blast radius of a compromised PAT is bounded by the PAT's permissions, not the bot's account-level permissions — this is the relevant control.
- Heroku does not support per-resource-scoped API keys natively (OQ2 in design doc). Until OQ2 is resolved, Heroku access by handoffs requires a workaround or operator action.
auto_revoke_afteris a new Velvet field that requires a Velvet code change before handoffs with duration > 1 hour can auto-expire.
Neutral
- The existing shared GitHub App bot identities (raxx-dev-bot, etc.) are unchanged. They continue to be used for live, operator-present sessions. Handoffs get additional fine-grained PATs or scoped install tokens on top of the existing model.
- The vault path
/MooseQuest/handoffs/is a new leaf in the existing Infisical hierarchy. No schema changes to Infisical itself.
Alternatives considered
Alternative A — Extend existing shared bot identities with narrower permissions
Use the existing raxx-dev-bot / raxx-pm-bot with per-PR or per-task permission narrowing applied at spawn time via session context constraints.
Rejected because: session-context permission narrowing is advisory in the current model — it restricts what the agent does but does not restrict what the credential can do if the agent is compromised or misbehaves. A compromised raxx-dev-bot token still has the full bot's GitHub permissions at the GitHub API level. Independent revocation of one handoff task without affecting the shared bot is not possible.
Alternative B — Operator-manual credential handoff (no automation)
Require the operator to manually provision and inject credentials for each handoff, accepting the operational burden.
Rejected because: this is the current state for ad-hoc agent dispatches and it scales poorly. As handoff frequency increases, manual provisioning becomes a bottleneck and a source of errors. The goal is an automated gate, not a process tax.
Alternative C — Single "handoff bot" identity with per-task scope enforcement in session context
Create one shared handoff bot identity with broad permissions; enforce task scope via session context constraints (what the agent is allowed to call).
Rejected because: this does not satisfy HANDOFF-INVARIANT-1. A compromised session with the shared handoff bot identity can still access the full scope of the bot's credentials at the API level, regardless of session-context constraints. Per-task provisioning with per-task credential minting is the only approach that provides true independent revocation.
Security / GDPR checklist
- PII collected: Handoff identity provisioning logs record task slug, surfaces provisioned, timestamps. No credential values. No user PII in provisioning logs. If the handoff task processes user data (e.g., IAP events, Stripe webhook payloads), that data is handled by the task logic, not the identity provisioning layer.
- Retention period: Provisioning audit logs: 90 days, matching platform policy (ADR-0077). Infisical handoff identity audit logs: 90 days.
- Deletion on DSR: Provisioning logs do not contain end-user PII. DSR deletion path for user data processed by handoff tasks follows the relevant service's DSR procedures (Queue billing tables, etc.).
- Audit trail: Every secret read/write by a handoff identity generates an Infisical audit event tagged with the handoff machine identity (task slug embedded in identity name). PROVISION_MANIFEST in vault provides a durable record of what was provisioned, when, and for how long.
- Stored credentials: Scoped credentials stored in Infisical at
/MooseQuest/handoffs/<task-slug>/and injected into agent env at spawn time. Not committed to git. Not written to disk. Not in logs. Deleted on task completion orauto_revoke_afterexpiry. Satisfies I1, I4, H6. - Breach notification path: Compromised handoff identity → revoke in Infisical (<60 seconds); blast radius bounded to task's scoped paths. If compromised identity accessed user-correlated data: GDPR 72-hour notification clock starts on confirmation. Test/sandbox credential compromise does not expose live customer data.
- Secrets location + rotation: At
/MooseQuest/handoffs/<task-slug>/in Infisical. Rotatable / deletable without redeploy. Velvetauto_revoke_afterautomates expiry for tasks > 1 hour. - Kill-switch: Per-surface revocation (§4.6 of design doc). Each surface credential is independently deletable in < 60 seconds. No shared-identity kill required.
Revisit when
- SC-HANDOFF-GITHUB-01 ships per-handoff GitHub App install scoping. At that point, the "fine-grained PAT under raxx-dev-bot" interim (OQ1) can be retired.
- Heroku releases per-resource or per-app API key scoping. OQ2 resolution may simplify the Heroku credential model.
- The Velvet
auto_revoke_afterfield is implemented (SC-HANDOFF-VELVET-01). Update this ADR to note that automated expiry is operational. - A handoff frequency of > 5 per day is reached in production. Revisit the provisioning script UX and whether fully automated end-to-end provisioning is warranted (currently operator-initiated step 0).
- ADR-0130 is updated (Option C — private mesh). If vault moves behind a mesh, the handoff Infisical identity model is unchanged; update the handoff spec to note the new vault endpoint.