> For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt.

# Member Credentials

Member Credentials is the Metallo context that holds a member's offline consent
for native Site work and serves one fresh access token per native action. Each
Slurm job runs with the member's personal credentials; nothing is impersonated.
See [ADR-0035](/reference/docs/decisions/0035-local-aai-laboratory.md).

## Aggregate

`domain.consent.OfflineConsent` is one login's offline grant. A member has at most
one active consent, and a second login replaces it: the incoming offline token
becomes the Run's token and the earlier consent moves to `superseded` and waits
for the issuer to confirm its revocation. The other terminal states are `lost`
(the issuer refused the stored refresh token), `expired` (its measured window
ended) and `revoked` (the issuer confirmed the revocation; the row keeps no
sealed value).

The record never contains the refresh token. It carries a sealed value bound to
the consent identity, so a sealed value cannot open in another row. Only the
owner adaptor's sealer can open it, and no token reaches a log, a command, an
outcome or a reply.

## Operations

| Operation                                         | Handler                            | Rules                                                                                                                                                                                                                                                                                                                                                                     |
| ------------------------------------------------- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `POST /member-credentials/v1/consents`            | `MemberCredentials.accept`         | Login hand-off behind `MEMBER_CREDENTIALS_INTAKE_KEY`, on the Site API inside the cluster (the gateway forwards only `/v1/site/` paths); `recorded` or `unchanged`                                                                                                                                                                                                        |
| `POST /v1/site/commands/{commandId}/member-token` | `AttemptMemberAccess.issue`        | Runtime's per-action token for one unacknowledged Slurm Attempt of that member                                                                                                                                                                                                                                                                                            |
| Revocation sweep                                  | `MemberCredentials.revoke_pending` | Retries issuer revocation for ended consents every 60 seconds                                                                                                                                                                                                                                                                                                             |
| Consent refresh                                   | `MemberCredentials.refresh_idle`   | Every `MEMBER_CREDENTIALS_REFRESH_SECONDS`, refreshes every `active` consent due inside two intervals, a page at a time, of a member who still holds Project execution authority. The Site API connects that authority only with `SITE_API_EXECUTION_AUTHORITY_ENABLED=true`; without it nothing is refreshed and an unused consent ends after the seven-day idle timeout |

The token route lives under the command scope it serves, `/v1/site/commands/`:
the path names the command, the body names its Attempt only, and the Site and
environment are the ones the bearer proves. A Site API can therefore scope the
whole command prefix without a third route family. It is the
Runtime's own `commands` grant that covers the route (DD-0059 §8), so no third
native scope exists; a `psb_` bearer is accepted only with that scope and the
`slurm` environment, while a Kubernetes Runtime keeps its `site-control`
audience. The grant's Site must be the one this listener serves, the single setting
`SITE_API_SITE_ID` every native route reads (no fallback, and
`SSH_ACCESS_SITE`, when present, must name the same Site), and the command's
saved `attemptId` must match the request.

Every other case gets one refusal: another Site, another environment, an unknown
command, a command of another Attempt and an acknowledged command all answer
`403 member_token_refused` with the same body, so a caller cannot learn whether an
Attempt exists. A bearer outside the `commands` grant stops earlier with a plain
`403 forbidden`, which says nothing about any Attempt. A consent that is missing, lost, expired, superseded or revoked becomes
`ConsentRefused` with one fixed code. The Runtime records a terminal Attempt refusal for
`member_consent_missing`, `member_consent_lost`, `member_consent_expired` and
`member_consent_revoked`, after releasing repository access and cleaning its workspace.
Execution Core prevents automatic retries and includes a sign-in-again notice in the
Run's typed projection. Temporary owner and transport failures remain retryable.

Rotation happens under a per-member advisory lock and the row's `FOR UPDATE`. The
adaptor persists the new sealed value and generation before the caller uses the
access token, so a crash between the issuer's rotation and the commit leaves a
consent that is provably unusable rather than one that silently continues with a
stale token.

The refresh worker is what keeps an idle Run inside the issuer's idle window: without
it, a Run that performs no native action for longer than the idle timeout loses its
consent even though the 30-day maximum has not been reached (ADR-0035). It takes the
same per-member lock as a native action, so a refresh and an action never rotate the
same token twice; it keeps no access token; and it lists only an `active` consent whose
issuer-reported deadline is still ahead. Project execution authority comes from the
resource view's `member_generations`, which keeps a Party that left a Project as
inactive, so a member who left is never refreshed; when that evidence is unavailable
nothing is refreshed for that member and the next pass asks again. The worker runs in
the Site API, beside the routes. While the Run path is paused it has no such authority,
and with no Run to keep alive it only retries revocations.

## Configuration

| Variable                             | Purpose                                                                                                                                       |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `MEMBER_CREDENTIALS_INTAKE_KEY`      | Shared key with the login service; at least 32 characters                                                                                     |
| `MEMBER_CREDENTIALS_ISSUER`          | Expected issuer of every consent                                                                                                              |
| `MEMBER_CREDENTIALS_CLIENT_ID`       | The login client the offline consent belongs to                                                                                               |
| `MEMBER_CREDENTIALS_CLIENT_SECRET`   | Refreshes and revokes at the issuer's token endpoint                                                                                          |
| `MEMBER_CREDENTIALS_CA_FILE`         | Trust for the issuer                                                                                                                          |
| `MEMBER_CREDENTIALS_SEAL_KEY`        | Seals the stored refresh token; 32 bytes as 64 hex characters                                                                                 |
| `MEMBER_CREDENTIALS_REFRESH_SECONDS` | Sweep refresh interval; three days by default, and it must leave one interval of margin inside the seven-day idle timeout                     |
| `SITE_API_SITE_ID`                   | The Site API's one Site; every Runtime grant must name it. There is no fallback, and `SSH_ACCESS_SITE`, when present, must name the same Site |

All six `MEMBER_CREDENTIALS_*` values are required together, and the listener
refuses to start if it cannot name its Site; without an intake key it serves only
its other routes.

## Storage

`member_credentials` owns its PostgreSQL database and schema: one active row per
member (enforced by a partial unique index), the ended rows kept until the issuer
confirms revocation. It has no broker, no platform database and no Runtime access.

<details class="page-source"><summary>Page source</summary>

<a href="/source/services/metallo/contexts/member_credentials/README.md.txt">Source snapshot</a> · Revision `f928834c8`.

</details>
