# 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](../../../../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.