Credentials and subscriptions¶
Six symptoms that all end at one question: which credential was supposed to pay for this request, and what refused it? Read the refusal sentence first — each one names a different repair, and none of them is repaired by waiting. The error vocabulary is in errors; the rest of the runbook is at runbook.
A request answers 503 credential_unauthorized¶
The dispatch sentence behind it is usually
direct '<provider>' credential is unavailable — no capability, read
grant, or standalone credential could produce a secret for that provider at
final use. It is retryable: false on purpose: waiting does not reach an
authorization that does not match, and the refusal is what the caller
receives. Check, in order: /readyz (denied[]), the launcher's
provisioned trust material (bin/provision-skarbiec-trust was run once on
this installation), and whether the alias's route names a provider this host
was ever granted (alias_provider_capability_absent in the log at startup:
"no provider capability was issued for this route; the alias will not
serve").
An alias in that condition reports state capability_absent in
brama aliases and in GET /v1/aliases, and a completion naming it
answers 503 alias_unserviceable with the state and the route in
details.
A request answers 429 subscription_unavailable¶
The dispatch sentences, from narrowest to widest (concepts/entitlement):
no active '<provider>' credential for agent— the signed agent owns no eligible credential for the provider the route names.no active stateless provider models for signed agent— a selector found no candidate at all.no working subscription model for signed agent— the selector walked its whole bounded candidate list; the body'sattemptscounts the provider calls actually paid for. After every provider it walked past (<provider> refused (…)) it names every pool member whose provider had no candidate route at all, as<member> (<provider>) not tried: <cause>, where the cause is its recorded status, its retirement, the ledger's standing refusal, the last model discovery's refusal, or a discovery that published no model for it — so an account of another provider is never silently absent from the answer.all bounded '<provider>' credentials unavailable for agent— every bounded credential was tried or is inside a recorded block.
First command: brama subscription list. 0 of 0 subscription credentials
are live is the whole diagnosis — sign an account in. Rows present but
refused carry last_redeem_error in the words of whatever refused
(credential cause first, then a block still in force, then the newest failed
check; a lapsed block is deliberately not reported). A gateway that holds no
usable credential signs itself in — through Weles on its own host with
subscription login, or by
hand. Taking the grant a coding harness on some machine already holds is not
offered: a provider issues one OAuth pair per sign-in and revokes it when a
second holder refreshes, so the borrowed copy signed that machine out, and a
gateway repaired that way depended on a workstation staying in the fleet. A member
left over from that arrangement is given back with
subscription disown.
When Weles cannot drive the account, the sign-in is done by hand: the person logs in with their own browser and pastes the code back. Where Brama runs, on a terminal:
$ brama subscription login claude-code --by hand --subscription-id <id> --reason "<why>"
It prints the page to open, waits for the pasted code, stores the grant and
proves it with one completion; exit 0 with result: signed_in is the proof
(cli). From another machine, such
as a workstation that holds no vault of its own, name the serving gateway so the grant
lands in the vault it reads:
$ brama subscription login claude-code --by hand --subscription-id <id> --reason "<why>" \
--gateway-consumer <consumer> --bearer-role <role>
Run there without a gateway, the command refuses before printing the page and names
these options. Brama Desktop’s Sign in by hand on the selected account
does the same against the gateway; result: signed_in on the screen is the proof.
A subscription reads burnt¶
burnt covers both recorded dead states — a grant the provider disowned
and a subscription somebody retired — and last_redeem_error says which. A
definitive provider refusal (invalid_grant, invalid_token,
revoked, unauthorized_client, or an unexplained 401/403) is recorded
as credential.state: needs_reauthorization with the provider's own
sentence as cause, and the credential is left untouched until a sign-in
replaces it — the refresh timer will not resurrect it, and a 30-minute block
holds the dispatcher off it. Repair: on the host Weles runs on, one command
does the whole sequence — the sign-in through Weles and the refresh that
proves it:
$ brama subscription login <provider> --by weles --reason "<why>"
Exit 0 with result: signed_in is the proof
(cli).
Where Weles is not reachable, sign the subscription in by hand — for
claude-code with brama subscription login --by hand, which runs the
proving refresh itself; otherwise through Desktop or a donation, then:
$ brama subscription refresh <provider> --reason "<why>"
Exit 0 with result: refreshed is the proof; result: failed quotes
each refusal in the provider's own words. A retirement outranks everything: a
retired subscription is never refreshed.
subscription refresh reports it attempted nothing¶
The command exits with an attempt count of zero and this detail:
detail: no usable `<provider>` subscription is in this deployment's pool, so
no credential source is configured to refresh: one has to be signed in and
stored in the vault before this command has anything to act on
Not a fault in the refresher — the pool is empty for that provider. The same sentence answers a provider with no OAuth path. Sign an account in first; the walkthrough shows the full sequence (walkthrough-subscriptions).
An account vanished from every listing but keeps paying¶
The tag-loss case: discovery finds an account by its brama:subscription
mark, so an item that loses it stops existing for every caller and every screen
while its credential stays valid. Nothing that lists subscriptions can say so
— the listing is the thing that lost it. The one reporter is /readyz:
unroutable_accounts[] names the item id, provider (null when the item carries no brama:provider: tag), and coordinate (items
stripped bare are recognised by the provider:<provider>:<id>
coordinate alone). Repair in the vault: restore brama:subscription,
brama:provider:<provider>, brama:id:<id>. No
brama:agent: tag is needed for routing: every subscription serves every
caller.
A provider refuses its own credential¶
A direct route whose provider answers 401 returns
502 provider_failure with the provider's sentence behind the kind
prefix; the body's attempts field counts the single provider call that
was made:
{"error":{"code":"provider_failure",
"message":"provider_authentication: Incorrect API key provided: ...", ...}}
Distinguish it from 503 credential_unauthorized: the 502 means Skarbiec
(or the standalone map) produced a credential and the provider
rejected it — the repair is at the provider account, not the authorization
chain. Because the alias names one route, that provider's answer is the
answer the caller reads. A provider 429 surfaces as
429 provider_rate_limited with retryable: true; when the
provider named the window's reset, a recorded block marks that account unusable
until then, and requests that need it are refused in the meantime. A refusal
without a reset records no block: the next request asks the account again.
A request is slow before the provider answers¶
Two log lines say where the time between routing_decision and
routing_complete went. credential_obtained is written once
per managed credential, with resource, path
(configured, issued or read-grant — which step of
the cascade answered) and elapsed_ms: the whole cascade, which is a
child process to the entitlements router and a broker redemption.
subscription_models_discovered is written once per request that
ranks the shared subscription pool, with subscriptions, read
(credentials read from the vault this time), awaiting_sign_in
(left alone because the ledger says a sign-in is owed), failed
and elapsed_ms. A request that spent nine seconds with
elapsed_ms small on both lines spent them at the provider.
The request path trusts the ledger the way the refresh sweep does. A grant
the provider disowned — credential.state: needs_reauthorization,
with the provider's sentence as cause — is not read from the vault
and not refreshed again until a sign-in replaces it; it appears in
subscription_models_discovered as awaiting_sign_in. Before
this, every request that ranked the account paid for that read and for the
refusal — a document with no refresh token could be refused several times a
minute for days while every caller waited on it. A credential the vault refuses outright
is remembered for sixty seconds, the same window a failed model discovery is.
Concurrent cold catalogue requests for the same subscription share one discovery. The joiner reads the existing success and failure caches after that discovery finishes; unrelated accounts do not share it. This shares model metadata, never a credential for inference. In subscription_models_discovered, the request that performed the read reports read=1 and the joiners report read=0 for that account.
Why an account cannot be signed in¶
Brama resolves a subscription from Skarbiec and delegates authentication to Weles. Weles reads the matching login material from Skarbiec and performs OAuth on the Stado-selected host, without provider CLIs or copied local sessions. No second account catalogue or manually added login tag is required.
The pool's automatic_sign_in.last_attempt records the outcome.
For a failure, inspect failure.code, failure.stage,
failure.http_status, failure.run_id and
source_revision. The detail is the service's actual response, not
an explanation inferred from a missing configuration field. Readiness and
Desktop show this same observation.
| Code | Observed operation |
|---|---|
subscription_identity_missing | The Skarbiec subscription has neither a principal nor an unambiguous reference to a login item. |
skarbiec_login_missing | No Skarbiec login item matches the principal the subscription declares. |
skarbiec_login_ambiguous | Several login items match; the response lists their identifiers instead of selecting an unrelated account. |
login_identity_changed, skarbiec_identity_changed | The account changed between resolution and credential use; no grant for the old identity is accepted. |
weles_unreachable | Weles did not answer: the request failed in transport, or the route in front of it (a Stado adapter) answered a server error with no JSON body, such as 502 upstream unavailable. The detail includes the operation, address, status and body or transport error. It is a dependency outage, not an operator action: the next maintenance pass asks again. |
account_resolution_response_invalid | Weles answered account resolution outside a server error with something that is not its JSON contract; the detail carries the status and the body as sent. |
oauth_provider_refused, provider_challenge_refused | The provider refused the OAuth or login operation. Its response and the Weles run are retained. |
credential_persist_refused, persisted_credential_refresh_failed | The login did not yield a usable persisted subscription credential; it is not reported as repaired. |
journal_unreadable | The journal holding the last sign-in could not be read, or its last failed record does not state its failure code and stage (an older record without structured operation evidence). The detail names which of the two is absent and when the attempt ran; it is not proof that a provider rejected the account, and no code is put in place of the one the record lacks. |
not_placed_host | Stado reported that this gateway runs on a different host than the declared placement. |
The repeat check uses the resolved Skarbiec account-data revision and the actual attempt result. It retains the refusal for unchanged, non-retryable provider challenges rather than repeatedly submitting passwords or codes. An identity-resolution failure is not recorded as a completed browser login.
A gateway whose Weles is down says so in /readyz: each member whose sign-in found
no Weles is listed under automatic_sign_in.blocked with weles_unreachable
and the address it tried, whether that address came from BRAMA_WELES_URL or from
Stado's service directory, and operator_action_required stays false for it. Qualify
it with cargo test --test readiness-weles-down -- --ignored --nocapture against an
isolated gateway whose pool the ledger refuses and whose BRAMA_WELES_URL names an
address nothing listens on.
An account nobody signs in — an API key, or a retired subscription — reports
applies: false and no word at all, so a healthy account is never
shown as awaiting something. Weles signs in claude-code,
codex and kimi.