Raxx · internal docs

internal · gated

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. MCP tool allowlist. The handoff agent's session_context must 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.

  7. 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_after field (SC-HANDOFF-VELVET-01). Short tasks use time-bounded surface tokens and manual revocation on task completion.

  8. 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

Negative / risks

Neutral


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


Revisit when