Raxx · internal docs

internal · gated

ADR 0149 — Session and agent credential model: bounded scripts and per-repo scoped write identities; no shared admin

Status: Accepted 2026-10-30 by Kristerpher Henderson (operator). Previously Proposed (2026-08-30). The acceptance was conveyed to the session that merged this PR by the operator's merge directive of 2026-10-30; at merge there was no approval comment under the operator's own GitHub identity on the PR and no cited AskUserQuestion transcript, so per .claude/agents/_shared-conventions.md §Operator authorization identity this line is the record of the decision, not the authorization artifact (see the open item in §9). Date: 2026-08-30 UTC (authored; the PR that carries it was opened the same day). Accepted 2026-10-30 UTC; facts in §1–§3 and §9 were re-checked against develop at acceptance (the decision itself is unchanged). Deciders: Kristerpher Henderson (operator) — accepted 2026-10-30; software-architect (proposed this design); control-session (sibling implementation: bounded CI/fleet wrapper scripts, landed in #4566 — see §3.2 — and the operator Bash allow-list rules that invoke them, not yet landed; referenced here, not authored by this ADR). Scope: Session and dispatched-agent access to Infisical (vault.raxx.app), AWS SSM, the Woodpecker CI API, and any other credential surface a session or agent needs at runtime. Extends ADR-0130 (vault multi-agent machine identity, Accepted) and ADR-0131 (handoff agent least-privilege identity, Proposed, two open operator questions) from "which identity does an agent hold" to "where does write privilege live at all, and what is a session ever allowed to invoke directly." ADR-0151 (Accepted 2026-08-31) builds on this ADR and carves a scoped break-glass exception to §3.1 — see §3.8. Refs: ADR-0130, ADR-0131, ADR-0151, docs/architecture/vault-multi-agent-access-pattern.md, docs/architecture/handoff-agent-least-privilege-identity.md, docs/architecture/agent-github-identity.md, docs/ops/runbooks/vault-access.md, docs/ops/runbooks/vault-coverage-audit.md, docs/ops/runbooks/ci-woodpecker.md, .claude/agents/_shared-conventions.md §Operator authorization identity, the MooseQuest-LLC/vault-kit repository (bin/rvault, scripts/registry.toml; the /provision-vault-secret and /onboard-vault skills named below are operator-local and not in any repository, so they are described rather than linked), issues https://github.com/raxx-app/TradeMasterAPI/issues/4544, https://github.com/raxx-app/TradeMasterAPI/issues/4545, https://github.com/raxx-app/TradeMasterAPI/issues/3738, https://github.com/raxx-app/TradeMasterAPI/issues/1903, PRs #4566 (the two CI/fleet wrappers) and #4577 (ADR-0151).


1. Context

On 2026-08-29 UTC (with fleet follow-ups into 2026-08-30), two production-deploy recovery efforts (the stale HEROKU_PROD_API_KEY WP org secret, https://github.com/raxx-app/TradeMasterAPI/issues/4544, and the CI-fleet/monitoring incidents chained off it, https://github.com/raxx-app/TradeMasterAPI/issues/4545 — see that issue's comment timeline for the minute-by-minute account of agent-orphan recovery, ASG floor drift, and instance sizing) dead-ended repeatedly on credentials, not on engineering judgment.

The estate already has a read identity for Infisical: a shared Universal Auth machine identity for the raxx-secrets project, VAULT_PATH defaulting to /MooseQuest/, carried by every Woodpecker-org repo and every developer shell. Every session and dispatched agent that day had that identity available for reads. What none of them had — correctly, by the existing permission model — was any path to a write or a mutating action:

Corroboration note (added in fix-forward review): the refusal account and the four-mutations account above are the control-session's own report of that day, cited here because it is what motivated this ADR. As of this revision, neither https://github.com/raxx-app/TradeMasterAPI/issues/4544 nor https://github.com/raxx-app/TradeMasterAPI/issues/4545's comment history contains a record of the laundering-refusal exchange, the tag-delete denial, or the reboot denial — https://github.com/raxx-app/TradeMasterAPI/issues/4544's only comment covers the v1.13.3 re-tag deploy, and https://github.com/raxx-app/TradeMasterAPI/issues/4545's comments cover the agent-orphan runbook gap, ASG floor drift, and instance sizing (those three parts are corroborated there). Treat the refusal/denial narrative as reported, not as an audited fact, until a session transcript or log citation is added here. The dates follow the issue timestamps: both issues were created 2026-08-29T01:24Z; #4544 closed 2026-08-29T02:38Z and its recovery deploy of v1.13.3 was commented at 2026-08-29T05:40Z; #4545's comments run from 2026-08-29T05:41Z to 2026-08-29T14:54Z, plus one on 2026-08-30T16:37Z (instance-class pricing). So the incident itself is 2026-08-29 UTC, and 2026-08-30 enters only through the fleet and sizing follow-ups in #4545 and the merge of #4566 (§3.2).

The naive fix that was proposed and rejected: give sessions (or the agents they dispatch) a shared admin Infisical identity, so the next incident does not stall the same way. This was raised and explicitly rejected in the control-session discussion that produced this ADR — it would undo the per-agent least-privilege model ADR-0130 (Accepted) already established and ADR-0131 (Proposed, not yet operator-approved) proposes, replacing "scoped identity per agent class, independently revocable" with "one identity, compromise-once-lose-everything," for the sake of convenience during exactly the kind of high-pressure recovery where a mistake is most costly.

This ADR does not re-litigate the shared read identity's blast radius on its own — https://github.com/raxx-app/TradeMasterAPI/issues/3738 (provision per-agent scoped Infisical machine identities) remains its own tracked work (still open as of 2026-10-30), and https://github.com/raxx-app/TradeMasterAPI/issues/1903 (migrate CI workflows off static repo secrets onto the vault-sourced pattern) was closed as completed on 2026-09-08, delivered as a side effect of the GHA-to-Woodpecker migration — those pipelines load vault secrets through the shared org read identity, so #1903 closing did not narrow it and #3738 is the only open narrowing work — but §3.3 below makes landing https://github.com/raxx-app/TradeMasterAPI/issues/3738's narrowing (or an equivalent explicit deny rule) a hard prerequisite before any <repo-slug>-rw identity is minted, and that is the only gate on minting, because the -rw client secret's storage path sits inside the shared read identity's current grant (§3.3, §3.4). The repo-secret-write.sh wrapper (§3.3) gates a different thing — session invocation of the minted identities — not minting (the one rule is stated in §3.3 "Two gates, kept separate"). This ADR is about where write and mutating privilege lives, for sessions and the agents they dispatch, going forward.


2. Invariants restated

# Invariant Source
I1 No stored credentials that can be replayed. Platform invariant
I4 Credentials into infra (env / secret stores), never into files that ship. Platform invariant
I3 Audit trail for every state change affecting money, permissions, or data access. Platform invariant
HANDOFF-INVARIANT-1 No handoff/session identity ever holds admin or blanket-scope credentials; every credential-holding identity is independently revocable. ADR-0131 (Proposed, not yet Accepted — operator decision still required on that ADR's OQ1/OQ2; this ADR treats the invariant as binding for its own decisions regardless of ADR-0131's own status)
— A session denied an action must never obtain it via a peer session. New, this ADR (§5)
— Admin actions (minting identities, creating projects, membership) are operator-only, for sessions and the agents they dispatch; persistent services such as Velvet are outside this rule (§3.1). New, this ADR (§3.1)

No option below routes around any of these.


3. Decision

3.1 No session or agent ever holds an admin identity

No session, and no agent it dispatches, ever holds an org-admin or project-admin Infisical identity. Minting identities, creating Infisical projects, and changing project membership are performed only by Kristerpher Henderson, as org owner, directly — never relayed through a bot comment, never delegated to a session that "has admin creds available this one time." This closes the gap the peer-session-laundering attempt (§1, §5) tried to open.

Who §3.1 and §3.3 bind. They bind sessions and the agents a session dispatches, not the persistent services. Velvet is out of scope, and its grant is unchanged by this ADR. The design doc behind ADR-0130 (vault-multi-agent-access-pattern.md) gives Velvet ownership of secret lifecycle including mint (invariant I7), lists velvet-prod with read and edit on /MooseQuest/velvet/prod/ and on /MooseQuest/ for rotation distribution (§6.2), and words provisioning as "Operator (or Velvet with appropriate scopes)" (§6.3 step 1). This ADR neither widens nor narrows that grant. The §6.2 matrix gives Velvet secret read/edit, not Infisical identity administration, so Velvet minting <repo-slug>-rw identities is not sanctioned here (minting stays operator-only, §3.3, §3.6), and Velvet's rotation of those identities' client secrets (§8) runs under its existing grant. That grant reaches /MooseQuest/identities/ through its /MooseQuest/ write, by design; narrowing it is separate work, not decided here.

3.2 Privilege lives in bounded, reviewed scripts, not in sessions

Every mutating operation a session legitimately needs gets an in-repo wrapper script, not standing credentials in the session's own environment. A qualifying wrapper:

  1. Fetches its own credential at run time from the sanctioned store — SSM for AWS-resident secrets, Infisical for everything else — rather than reading one from an ambient env var the session already holds.
  2. Validates a narrow argument shape: the specific operation it exists for and nothing adjacent (e.g. cancel <repo-id> <pipeline-number> / restart <repo-id> <pipeline-number>, never a bare "trigger a manual pipeline" call — a different privilege with a different blast radius).
  3. Refuses anything outside its stated purpose, exiting non-zero with a diagnostic rather than guessing intent.
  4. Never prints the credential it fetched, in any output path (stdout, stderr, logs, a caught-exception traceback).
  5. Emits an audit line — timestamp, caller, operation, target, outcome — on every invocation, success or failure.
  6. Fetches its credential from a store/path the invoking session's own ambient identity cannot already read. A wrapper that only re-fetches the same secret a session-allow-listed read call can already return directly (e.g. an SSM parameter also reachable via a generic aws ssm get-parameter) narrows nothing at the credential layer — it is a naming convention around a credential the session already effectively holds. This is a hard requirement, not a nice-to-have (§3.5, §5).

A provision-secret.sh that the operator's local vault-kit checkout holds at scripts/provision-secret.sh (invoked via the operator-local /provision-vault-secret skill) is the closest existing illustration of the wrapper shape — narrow surface, credential fetched inside the script rather than handed to the caller, refusal by default, silent on values, loud on outcomes. It is not a reviewed or deployed artifact. As of 2026-10-30 that file is local and uncommitted: it is not on main of MooseQuest-LLC/vault-kit (upstream scripts/ holds migrate-env.sh, mint-identity.sh, provision-project.sh, seed-manifest.sh and registry.toml), it was created on 2026-08-29, and it has never been reviewed upstream. This ADR cites it only for its shape, as read from that local copy; the first step before anything may rely on it is committing it to vault-kit and reviewing it there (§9). It is not an example of point 6 above: it authenticates with the org-level admin Infisical identity (same 4-source precedence as rvault set, which is upstream in bin/rvault), and it accepts a caller-supplied <projectId> <secretPath> <keyName> with no allow-list or path pinning — so invoking it is admin-equivalent write to any path, not a bounded one. As written it remains operator/admin-run only; a .vault.toml-pinned mode that resolves <repo-slug>-rw (§3.3) would be a different, session-invocable artifact. See §3.3 and §3.5 for what a session-invocable vault-write wrapper actually requires.

Two CI/fleet wrappers were written under this ADR, by the control-session, as sibling work (not authored by this design doc) and merged to develop in #4566 on 2026-08-30. As of 2026-10-30 they are reviewed code but not yet allow-listed for session invocation (§3.5, §9):

Neither is authored here; this ADR defines the shape they must satisfy.

3.3 Per-repo scoped write identities for vault provisioning

Vault write access for provisioning is granted only to per-repo scoped machine identities, never to a session or a shared identity. Naming convention: <repo-slug>-rw. Each identity is bound to exactly:

This requires a new or modified wrapper — rvault set (upstream) and the local, uncommitted provision-secret.sh (§3.2) do not have a -rw-aware code path today. Both resolve the single org-admin credential via the same fixed 4-source precedence (macOS keychain rvault:admin → ~/.config/rvault/admin.env → RVAULT_ADMIN_CLIENT_ID/SECRET → INFISICAL_CLIENT_ID/SECRET); a repo's .vault.toml supplies only project ID, secret path, and environment, never an identity to authenticate as. Minting <repo-slug>-rw identities is necessary but not sufficient for a session-invocable write path. This ADR therefore requires, as a rollout prerequisite (§9), a new bounded wrapper — working name repo-secret-write.sh <keyName> --env <env>, implementation is sibling work, not authored here — that:

Two gates, kept separate (the one rule; §1, §3.7 and §9 restate it).

Existing <repo-slug>-ro viewer identities are unaffected.

Relationship to the sre-agent write grant in ADR-0130. ADR-0130 (Accepted) says the sre-agent "remains available for vault write operations", and its design doc's permission matrix (vault-multi-agent-access-pattern.md §6.2) lists sre-agent with read, create and edit on /MooseQuest/. This section supersedes that write grant, forward-only: it is left as is until repo-secret-write.sh (or its equivalent) is session-invocable — that is, once Gate 1 and Gate 2 (§9) are closed, at least one -rw identity is minted, and the wrapper's allow-list entry has landed (the Invocation gate's three conditions; Gate 2 closing alone is not the trigger, because narrowing earlier would remove the only existing vault-write grant with no replacement) — and from then on sre-agent is not a sanctioned vault-write route (its read use is unchanged). Whether the identity was ever provisioned with that grant is not verified here; the §9 checklist has the operator confirm it and narrow it. ADR-0130 and the access-pattern doc (§6.2 matrix and §7) carry a back-pointer to this paragraph.

Minting procedure (operator-only, numbered, names/paths only — no secret values in this ADR or any command output):

  1. Operator opens the Infisical UI/API for the target project (project ID per the taxonomy already in vault-kit/scripts/registry.toml).
  2. Operator creates a new Universal Auth machine identity named <repo-slug>-rw.
  3. Operator scopes it: project = the repo's assigned project; path = the repo's secret_path; environments = the subset the repo actually runs in; permission = create, edit on that path only (no delete, no other path or project).
  4. Operator stores the resulting client_secret via the existing bounded provisioning path, at /MooseQuest/identities/<repo-slug>-rw/ — never pasted into a chat transcript, a commit, or a script argument. Do not begin step 1 until the minting gate above is closed — the whole procedure is gated, not just this step; it is restated here because this is the step where the exposure occurs (§3.4): the shared read identity's grant is VAULT_PATH=/MooseQuest/ with no documented exclusion for /MooseQuest/identities/ (docs/architecture/vault-multi-agent-access-pattern.md §6.2/§6.3), so storing a -rw client_secret there today puts every repo's write credential inside the read scope every session and WP-org repo already holds — a new shared write privilege laundered through the vault, exactly the failure mode §3.1 exists to close. The gate closes only when either (a) issue 3738's per-agent scoped read identities replace the shared read identity, or (b) the shared read identity's grant is amended with an explicit deny on /MooseQuest/identities/**; in both cases that is verified before step 1 with the throwaway-path check in the minting gate. Step 6's negative test re-confirms it on the real path afterwards and cannot substitute for that check.
  5. Operator adds one line to vault-kit's registry.toml (that repo lives in the MooseQuest-LLC/vault-kit GitHub org, not this one) recording identity-name against repo-slug.
  6. Operator (or a session running rvault check) verifies the new identity can write its own path and cannot read or write any other repo's path — a negative test, not just a positive one. The negative test must also confirm the shared read identity itself cannot read /MooseQuest/identities/<repo-slug>-rw/ — on the real path, after step 4, this re-confirms the check the minting gate required before step 1 on a throwaway path (it does not close the gate, and it is not merely a same-identity isolation check).

3.4 The shared read identity is narrowed separately — except where §3.3 depends on it

The existing shared read identity (raxx-secrets project, VAULT_PATH=/MooseQuest/, carried by every WP-org repo and developer shell) is not touched by this ADR in general. Narrowing its overall blast radius is tracked as its own work: https://github.com/raxx-app/TradeMasterAPI/issues/3738 (per-agent scoped read identities, already scoped in ADR-0130's design doc; open as of 2026-10-30) and https://github.com/raxx-app/TradeMasterAPI/issues/1903 (migrate CI workflows off static repo secrets onto the vault-sourced pattern; closed as completed 2026-09-08, see §1). This ADR is additive to that work, not a substitute for it — with one exception: §3.3's minting gate makes closing the /MooseQuest/identities/ read gap (via https://github.com/raxx-app/TradeMasterAPI/issues/3738 or an explicit deny rule) a hard prerequisite for beginning the minting procedure at step 1 — not only for the step that stores the -rw client secret, since steps 1-3 would leave a live secret with nowhere sanctioned to put it — because that one path is where this ADR's own design would otherwise create a new laundered write privilege. The deny rule is verified before step 1 with a throwaway path under /MooseQuest/identities/; step 6 re-confirms it afterwards. Everything else the shared read identity can reach is unchanged and out of scope here.

3.5 The operator allow-list authorizes the bounded scripts

Sessions call the bounded scripts above — and the read paths that already work (SSM reads, Woodpecker read-only calls) — because the operator's Bash permission allow-list (project settings) explicitly names them. The allow-list is the enforcement point; this ADR does not implement it (the control-session's PR lands the actual entries) but records the intended shape:

Rule (script path / command prefix) Why it's allow-listed
repo-secret-write.sh <keyName> --env <env> (§3.3; not yet built as of 2026-10-30 — rollout prerequisite, §9; policy/breakglass-classification.yaml already points at scripts/ops/repo-secret-write.sh as its home) Bounded single-key write pinned to the invoking repo's own .vault.toml; resolves <repo-slug>-rw only, never the admin precedence; credential fetched internally; never prints a value.
scripts/vault/audit_coverage.py Read-only by construction; no write path exists.
scripts/agents/mint_github_token.py --bot <bot> / with_bot_token.sh Mints a scoped 1-hour GitHub App installation token for one of the three bots (raxx-dev-bot, raxx-ops-bot, raxx-pm-bot; ADR-0128 — the renumbered successor to the original ADR-0096, per docs/architecture/agent-github-identity.md). Two caveats, as built on 2026-10-30: (a) if the mint fails, with_bot_token.sh falls back to the operator's ambient GH_TOKEN/GITHUB_TOKEN with a stderr warning — "never a session-held long-lived PAT" holds only for orchestrator spawn-time injection, which blocks dispatch for an unmapped agent class (UnknownAgentError); (b) this script predates the ADR and does not meet §3.2 point 6 — it reads each bot's App credentials from /MooseQuest/<bot-name>/ using whatever Infisical machine identity the invoking shell carries, so any identity able to mint can also read the App private key; narrowing that follows #3738 (§3.4).
scripts/ops/wp-pipeline.sh cancel\|restart <repo-id> <pipeline-number> The two mutations reported denied on 2026-08-29, narrowed to exactly those verbs. No manual-trigger verb. Merged in #4566, not yet allow-listed. Blocked on a verb-scoped Woodpecker token existing (§3.2) — not yet true today, since the only credential available is the same admin token the SSM row below can already read.
scripts/ops/ci-agent-recycle.sh <instance-id> [--force-unprotect] Wedged-agent recovery, scoped to one named instance verified to be in the CI agent ASG; not a wildcard fleet action. Merged in #4566, not yet allow-listed. Blocked on the dedicated IAM role in §3.2 existing — the session's own ambient AWS principal must not hold the ASG terminate/protection actions the script performs.
aws ssm get-parameter --name <specific-read-only-parameter-path> / describe-instance-information … Read-only, and explicitly excludes /ci/woodpecker/admin-api-token (docs/ops/runbooks/ci-woodpecker.md) — that parameter is the Woodpecker admin JWT and must not be reachable by a generic allow-listed SSM read; enumerate the specific non-admin parameter paths a session needs (e.g. monitoring/health-check paths) rather than allow-listing the command with no path restriction.
infisical secrets get … --plain (via rvault run/check) Read path only; per-repo -ro identity; unaffected.

provision-secret.sh (the local, uncommitted script in §3.2) is deliberately NOT allow-listed for session invocation as written, and would not be if it were committed upstream unchanged (§3.2, §3.3): it authenticates as org-admin and accepts a free-form <projectId> <secretPath> <keyName>, so allow-listing it as written would make any allow-listed session admin-equivalent for every vault path. As written it stays operator-run only, same as rvault set as written. The only session-invocable variant is a different artifact: a .vault.toml-pinned mode, resolving <repo-slug>-rw and never the admin precedence, added to it once it is committed upstream (or to rvault set), as §3.3 and §9 allow. Such a mode would be allow-listed on the same terms as repo-secret-write.sh (first row above). Any session context in which rvault:admin (keychain) or ~/.config/rvault/admin.env resolves is, by construction, holding an admin credential and is out of policy for that reason alone, regardless of whether this script is invoked.

Not proposed: any bare aws ec2 reboot-instances, git push --tags --force / remote tag delete, direct curl … api.infisical.com write, or Woodpecker manual-trigger call. Those stay behind a named wrapper (or operator-only) precisely because the wrapper is what makes the argument shape narrow and the audit line exist.

3.6 What stays operator-only regardless of any wrapper

No wrapper is written for the following — Kristerpher Henderson performs these directly, every time:


3.7 Relationship to what onboard-vault / onboard-ci already did

This ADR is forward-only and additive; it does not describe a migration of the already-onboarded repos, and it does not mean the estate "skipped" a step. Concretely, of the two halves of the model:

Effect on the skills — nothing changes until the §9 gate that applies to each skill closes (minting is gated on #3738 alone; the vault-write wrapper gates session invocation, not minting; the CI/fleet scoped credentials gate /onboard-ci; see §3.3 "Two gates, kept separate"):

Skill Today (unchanged by this ADR until prerequisites close) After prerequisites
/onboard-vault Mints <slug>-ro viewer only; writes use the operator admin identity. Gains a <slug>-rw minting step — gated on https://github.com/raxx-app/TradeMasterAPI/issues/3738 alone (§3.3 minting gate / §9), because a -rw client secret stored under /MooseQuest/ would otherwise sit inside the shared read identity's grant. repo-secret-write.sh does not gate this step; it gates session invocation of what the step mints.
/onboard-ci Wires the shared org read secrets + copies wpc_load_vault_secrets.py; no wrappers, no allow-list. Gains a step to distribute + allow-list the bounded CI/fleet wrappers — gated on the verb-scoped Woodpecker token and dedicated recycle IAM role (§3.2, §9). Per the operator's 2026-08-31 decision, this stays strict: no wrapper is allow-listed for direct session invocation until those scoped credentials exist (the ADR-0151 Tier B route to the same operations is separate, §3.8).
/provision-vault-secret rvault set (repo .vault.toml) or the local, uncommitted provision-secret.sh (admin; §3.2). repo-secret-write.sh (§3.3), or an equivalent .vault.toml-pinned mode of rvault set / provision-secret.sh, becomes the only session-invocable write path. provision-secret.sh and rvault set as written stay operator-only; only a pinned mode is session-invocable (§3.5).

Until issue 3738 and the scoped CI credentials land, the two skills keep doing exactly what they do now, and the only new artifacts are documents (this ADR) and the sibling wrapper scripts (unused as session-invocable until their prerequisites close). Onboarding a new repo in the interim follows the current -ro-only procedure; it acquires a -rw identity retroactively when 3738 closes, via a re-run of /onboard-vault (idempotent).

3.8 Relationship to ADR-0151 (added at acceptance, 2026-10-30)

ADR-0151 (graduated agent write-authority, Accepted 2026-08-31, #4577) builds on this ADR and, in its own §1 and §3, declares itself a deliberate, scoped exception to §3.1 (and to HANDOFF-INVARIANT-1) for its break-glass tiers only. This section records that so a reader of this ADR is not misled; it changes nothing in §3.1–§3.7.

4. Language choice rationale

N/A — this ADR introduces no new service (no new process, daemon, or independently deployable component). It defines a credential-access pattern and wrapper scripts running inside existing execution contexts (session shells, Woodpecker CI, SSM Run Command targets). Per the ADR template, this section is skipped.


5. Threat model

Session compromise. A compromised session should get, at worst, whatever the allow-listed bounded scripts expose — never a standing admin credential. That is the design intent of §3.2's wrappers (fetch internally, never hand the value back to the caller), and it holds for a wrapper that satisfies point 6 of §3.2. It does not yet hold for wp-pipeline.sh as scoped today: its only available Woodpecker credential is the same admin JWT at SSM /ci/woodpecker/admin-api-token that a session's already-allow-listed generic SSM read can fetch directly (docs/ops/runbooks/ci-woodpecker.md), so until that gap closes (§3.2, §3.5, §9), the actual bound on a compromised session is the classifier plus the allow-list's narrow argument shapes, not credential non-exfiltration — a compromised session that can pass the classifier and the allow-list can already read the admin Woodpecker token directly, wrapper or no wrapper. Say this plainly rather than claim a stronger guarantee the credential layer does not yet provide.

Prompt injection. A session manipulated (via a malicious PR description, a poisoned file, an adversarial tool response) into attempting an out-of-scope action is stopped at the argument-validation layer of the bounded script (§3.2 point 2) even if it is stopped nowhere else.

Peer-session permission laundering. This is the exact failure mode reported on 2026-08-29 (§1, corroboration note), called out explicitly rather than left implicit: a session or agent denied an action must never obtain it by asking a different session or agent to perform it on its behalf, even when the requesting session claims to relay direct operator authorization. The two sre-agent refusals that day were correct before this ADR existed; this ADR makes the rule the documented standard, consistent with .claude/agents/_shared-conventions.md §Operator authorization identity (operator sign-off must be posted under the operator's own GitHub identity or given directly via AskUserQuestion — never relayed through a bot or a peer session claiming to carry it). If a genuinely operator-authorized mutation is blocked, the correct path is the operator performing it directly, or the operator adding a new narrowly-scoped wrapper — never a peer session vouching for authorization it cannot itself verify.


6. Consequences

Positive

Negative / risks

Neutral


7. Alternatives considered

Alternative Verdict Why
Shared admin identity for all sessions/agents Rejected One compromise exposes every secret and mutation surface at once; undoes ADR-0130's (Accepted) least-privilege model and what ADR-0131 (Proposed) proposes; the naive fix explicitly rejected in the control-session discussion (§1).
Per-session admin identity (each session mints its own full-scope identity) Rejected Still blanket-scope per identity — a single compromised session can mutate anything in vault/AWS/CI; only improves revocation granularity, not the actual risk (breadth of what one identity can do).
Keep the status quo (read-only everywhere, no bounded scripts) Rejected Cost is the mutations, refusals, and multi-hour prod-deploy freeze reported for 2026-08-29 (§1, with the corroboration caveat noted there), resolved only by the operator performing every mutation personally. Not sustainable as the standing model regardless of exactly how many mutations were denied that specific day.

8. Security / GDPR checklist


9. Rollout checklist

Hard prerequisites (added in fix-forward review). Three gates; each blocks
only what it names and does not block the others (the one rule is stated in
§3.3, "Two gates, kept separate"):

Gate 1 - MINTING (blocks every "Mint <repo-slug>-rw" step in the Operator list
below, and nothing else):
[ ] https://github.com/raxx-app/TradeMasterAPI/issues/3738 lands (or an equivalent explicit deny rule on
    /MooseQuest/identities/**) — closes the §3.3 minting-gate / §3.4 gap where a
    -rw client secret would otherwise sit inside the shared read identity's
    grant. (Re-checked 2026-10-30: #3738 still open; no deny rule on
    /MooseQuest/identities/** exists in the repo.)
[ ] Gate 1 is verified BEFORE minting step 1, with a throwaway non-secret key
    at a scratch path under /MooseQuest/identities/: the shared read identity
    (or, if #3738 landed, each per-agent read identity that replaced it) is
    refused (permission denial, not a not-found) when it reads it, then the
    placeholder is deleted (§3.3). Step 6's
    negative test re-confirms this on the real path afterwards; it does not
    close the gate.

Gate 2 - SESSION INVOCATION of vault writes (does NOT gate minting):
[ ] repo-secret-write.sh (§3.3) — or an equivalent .vault.toml-pinned,
    <repo-slug>-rw-resolving mode added to rvault set (or to
    provision-secret.sh once it is committed upstream, §3.2) —
    lands and is reviewed. Until it exists, no vault-write wrapper is
    session-invocable regardless of how many -rw identities are minted
    (minted identities are operator-direct-use only in the meantime).
    Necessary, not sufficient: a vault-write path is session-invocable only
    once Gate 1 and Gate 2 are closed, at least one -rw identity is minted,
    AND the wrapper's allow-list entry (§3.5 table, first row) has landed —
    that entry is the Control-session allow-list item below, not part of
    Gate 2 or Gate 3.

Gate 3 - ALLOW-LISTING the CI/fleet wrappers and the SSM read rule (does NOT
gate minting):
[ ] OPERATOR DECISION 2026-08-31 (Kristerpher Henderson): CI/fleet wrappers
    take the STRICT posture — wp-pipeline.sh and ci-agent-recycle.sh are NOT
    directly session-invocable (no allow-list entry for the wrappers
    themselves) until the two scoped credentials below exist. No interim
    "admin token inside the hardened script" step for the direct wrappers.
    This gates direct allow-listing only; the [ADR-0151](https://internal-docs.raxx.app/architecture/adr/0151-graduated-agent-write-authority.html) Tier B route to the same
    three operations is separate (§3.8).
[ ] A verb-scoped Woodpecker API token (or equivalent narrower credential)
    exists for wp-pipeline.sh, distinct from the admin JWT at SSM
    /ci/woodpecker/admin-api-token (§3.2, §5).
[ ] A dedicated IAM role (e.g. ci-agent-recycle-executor) scoped to the
    Auto Scaling actions ci-agent-recycle.sh performs
    (autoscaling:DescribeAutoScalingInstances, SetInstanceProtection,
    TerminateInstanceInAutoScalingGroup — the mutating two limited to
    ci-woodpecker-agents-asg) exists for ci-agent-recycle.sh, and the
    session's ambient AWS principal is confirmed to hold none of them
    (§3.2). (Originally written for ssm:SendCommand + ec2:RebootInstances,
    which is not what the merged script does.)
[ ] The operator Bash allow-list's aws ssm get-parameter rule is narrowed to
    specific non-admin parameter paths, explicitly excluding
    /ci/woodpecker/admin-api-token (§3.5).

Operator (Kristerpher Henderson):
[ ] Mint <repo-slug>-rw for TradeMasterAPI, project-avenue, moosequest-site,
    smarter, console (in that order — TradeMasterAPI first, matching where
    2026-08-29 actually hurt) — do not begin step 1 of the §3.3 minting
    procedure until Gate 1 is closed and verified (the gate covers the whole
    procedure, not only the storage step 4).
    Gates 2 and 3 do not gate minting; they gate session invocation and
    allow-listing. (moosequest-site's registry entry exists only in the
    operator's local, uncommitted registry.toml today, §3.7.)
[ ] Record each identity in vault-kit's registry.toml (MooseQuest-LLC/vault-kit).
[ ] Commit scripts/provision-secret.sh to MooseQuest-LLC/vault-kit and
    review it there, or retire it (§3.2: it is local and uncommitted as of
    2026-10-30). Likewise decide whether the local-only registry.toml entries
    (moosequest-site, d-record) are committed, so §3.7's count and the
    upstream file agree.
[ ] Confirm whether the sre-agent identity holds create/edit on /MooseQuest/
    ([ADR-0130](https://internal-docs.raxx.app/architecture/adr/0130-vault-multi-agent-machine-identity.html) design doc §6.2) and, if so, narrow it once Gate 1 and Gate 2
    are closed, at least one -rw identity is minted, and the wrapper's
    allow-list entry has landed (§3.3) — NOT when Gate 2 closes alone: that
    would remove the only existing vault-write grant with no replacement.
[ ] Confirm each identity's negative test: cannot read/write outside its own
    project + path, AND the shared read identity cannot read
    /MooseQuest/identities/<repo-slug>-rw/ (§3.3 step 6; this re-confirms the
    Gate 1 check made before step 1, it does not replace it).
[x] ADR accepted 2026-10-30 (see Status) and the Status flipped to Accepted.
[ ] Authorization artifact still outstanding: an approval comment under your
    own GitHub identity on [PR #4567](https://github.com/raxx-app/TradeMasterAPI/pull/4567), or a cited AskUserQuestion transcript
    reference here (the original gate for flipping Status; the flip was made
    on the operator's merge directive without either — see Status).

Control-session (sibling PRs, not this one):
[x] wp-pipeline.sh (cancel|restart) and ci-agent-recycle.sh landed in #4566
    (2026-08-30) as scripts/ops/wp-pipeline.sh and
    scripts/ops/ci-agent-recycle.sh.
[ ] Their credential-isolation and IAM-role prerequisites (Gate 3 above) —
    NOT satisfied as built; they stay gating conditions on allow-listing.
[ ] Land repo-secret-write.sh (or the rvault set / provision-secret.sh
    equivalent) per §3.3.
[ ] Land the operator Bash allow-list entries per §3.5's table, narrowed as
    described above.

Verification (either party):
[ ] rvault check in a session with no admin credential exported confirms the
    session cannot read/write outside its repo's own scope, and that no
    ambient env var carries an admin client_id/secret — existence check
    only, never a value dump.
[ ] A session with only the shared read identity confirms it cannot read
    /MooseQuest/identities/<repo-slug>-rw/ for any minted identity.

10. Revisit when


Cross-references