BBramaDocs

Brama changelog: unreleased subscription and credential changes

Unreleased: subscriptions and credentials

Committed changes that carry no release coordinate yet and concern the subscription pool, the credentials in it, and the sign-ins that keep them alive. The remaining unreleased changes are on current changes; published releases are on 0.4.46 to 0.4.13, 0.2.77 to 0.2.42 and 0.2.5 to 0.2.2.

Unreleased: a refusal names the automatic sign-in failure that keeps the pool empty

A best request refused because its members await a sign-in said only awaiting sign-in: <the provider's refusal>, while the automatic sign-in that renews them had already failed on its Weles host for a reason only readiness reported. The refusal now ends with ; the automatic sign-in that renews them failed: <member>: <code>: <detail> for every such member whose failed attempt still stands, and the refusal of a pool whose every member is burnt says the automatic Weles sign-in that renews it failed: <code>: <detail> in place of renewed by the automatic Weles sign-in. The sign-in journal is read only once the request is refused, so a pool that serves pays nothing for it. See errors.

Unreleased: the Claude Code request shape is one capture

The release, Agent SDK and client versions, beta list, Stainless headers and the fingerprint and checksum parameters a Claude Code subscription request carries were separate constants, so one could change without the others. They are now one CLAUDE_CODE capture naming the Claude Code release they were read from, the Stainless headers in the order that release sends them, its client deadline header included as captured (Claude Code).

Unreleased: Brama buys a Claude Code account when the pool is spent

A brama maintain pass reads every claude-code account's plan from the provider; when all of them are spent and the pool holds fewer accounts than the operator allows (claude_code_accounts_max in src/subscription_dispatch/acquire/numeric-provenance.json), Weles buys one on the tier the pool's accounts hold and the gateway proves its grant. brama subscription acquire, POST /v1/admin/subscription-pool/acquire and Brama Desktop's Buy account now run the same decision on demand and name the account or number that stopped it. brama subscription hand-over claude-code --harness omp and Desktop's Sign into omp sign every pool account omp lacks into omp through its own login, completed by Weles, so no grant is shared.

Unreleased: the launcher reads every preloaded secret by role

The service launcher no longer names a vault item. Each model-router bearer is the token of the item that plays weles-model-router, wisent-backend-model-router, wisent-app-model-router or brama-desktop-model-router; each product's request-sign secret is read from the item playing <product>-agent-auth (wisent-app-agent for Jeden's runtime identity); and the Weles sign-in bearer from the item playing brama-weles-reauth, in the launcher and in a sign-in that runs without BRAMA_WELES_REAUTH_TOKEN. A role is the tag stado:role:<role>; no item in a role, or two, is refused at start with the role and its tag. brama launcher role-field --router ROUTER ROLE FIELD replaces item-field. Before a release carrying this starts anywhere, the vault owner's Skarbiec must register the stado:role: tag namespace and each of those items must carry its role, or the two required model-router clients stop the gateway at start.

Unreleased: the console bearer is read by role, not by item

--bearer-item <item>#<field> on brama maintain and on brama subscription list|refresh|attribute|reinstate|disown is replaced by --bearer-role <ROLE>: the console bearer is the token field of the vault item that plays that role, read through stado credentials get --role. A command line, a schedule or a page no longer names a vault item, so replacing or renaming the item changes nothing there, and an item the caller's consumer is not granted is refused by Stado with its own sentence instead of being the one a page happened to name. The old flag is refused by the parser as unknown.

brama probe follows: --bearer-role <ROLE#FIELD> and --signing-role <ROLE#FIELD> replace --bearer-item and --signing-item, and each secret is the field of the one live item tagged stado:role:<ROLE>, found through the entitlements router with the same lookup the launcher uses.

Unreleased: pool accounts are recorded in the vault the gateway reads

brama subscription attribute wrote each member's stated account through whatever skarbiec binary the operator's own machine resolved. On a machine that reads the fleet vault over the network that is a retired local copy: the command printed recorded, the fleet vault kept no brama:account tag, and Weles refused every automatic sign-in with subscription_identity_missing before a browser opened, so no Google prompt ever reached a phone. With --gateway (or --gateway-consumer and --bearer-role) the command now asks the gateway itself, over POST /v1/admin/subscription-pool/attribute, to record the accounts in the vault it serves from; the route answers 403 to a caller that is not the operator console. Brama Desktop's Subscription Pool screen calls the same route from Record accounts. The renewal sweep no longer discards a failed account write: it logs subscription_account_unrecorded with the subscription and the vault's error.

Unreleased: a subscription the runtime policy does not name is reported, not dropped

The runtime policy is generated from the vault's own tags when a release is installed on a host. A subscription added to the vault after that install is not in it, and the launcher stage that builds the boot catalogue dropped such an item without a word: the gateway simply had no such account. Accounts could be in the vault, tagged correctly and listed by stado route capability brama, while the gateway answered no working subscription model for signed agent and brama subscription refresh claude-code reported no capability route maps this resource to a vault item and field for each — the first visible trace, hours after boot, of a decision made at startup.

The stage now writes one line per dropped subscription naming the resource, what the gateway will answer later, and the remedy — install the release again on that host so the policy is regenerated from the vault's current tags — and ends the boot log with the counts: how many the policy does not name, and how many are served. cargo test --test release_subscription_catalog runs the shipped stage over a real vault listing and a real policy document and reads back the catalogue it writes.

The gateway says the same thing about itself while it runs: readiness now compares the vault's fully tagged subscriptions against the catalogue this process was started with, lists each account the runtime policy does not name under unroutable_accounts with that remedy, and answers traffic can be served, but the vault holds a subscription this process was not started with: the runtime policy does not name it. A process started without a boot catalogue reports nothing, because there every account would look unnamed. cargo test --test pool_policy_unnamed seeds a real isolated Skarbiec vault with two subscriptions and reads the census through the real entitlements router.

Unreleased: a Claude Code grant travels in Claude Code's own shape

With the temperature gone, the deployed gateway's first best request as tama was still refused: Anthropic answered 429 with the one-word body Error for claude-code/claude-fable-5-1 and then for claude-sonnet-4-6, while claude-haiku-4-5 had answered the import probe minutes earlier and omp was being answered on the same account for the same models. The grant was fine; the request was not Claude Code's. A request that presents a Claude Code grant now carries what Claude Code carries — its user agent and beta list, x-app: cli, and a system prompt opened by the billing attestation with a checksum of the body and the sentence You are Claude Code, Anthropic's official CLI for Claude. — on the buffered and streaming paths alike (how a request presents it).

Unreleased: an omitted temperature is not invented

The first Claude completion through the deployed gateway after the pool was repaired was refused by the provider: claude-code/claude-opus-4-7 refused (provider_failure: `temperature` is deprecated for this model.), and the same for claude-opus-4-8. The caller had sent no temperature; the gateway defaulted it to 0.7 on every inbound format and sent that default to every provider as a real setting. A temperature now reaches the provider only when the request stated one, on the chat, Anthropic messages and Responses wires and on every provider dialect; a stated value is bounded as before. The gateway's own probes still pin zero, because a probe's answer should not vary (shared validation).

Unreleased: one vault listing answers every reader

Every reader of the vault's inventory — per-agent discovery, the console's pool, the readiness sweep's unroutable-account census, the sign-in loop and each credential write's tag lookup — shelled the entitlements router's bare list, and the router decrypts and parses the whole vault to answer it. With hundreds of items in the vault and the readiness sweep and the sign-in loop asking concurrently every half minute, the router alone held most of a gigabyte and half a core beside a saturated gateway, until the control host's own object store closed connections for want of memory while the fleet's releases were writing to it. Concurrent callers now share the one listing in flight instead of each starting a router; nothing is served after it returns, because the vault changes under this gateway by hands it does not see (skarbiec delete, a vault sync, an operator's set) and a listing served afterwards would answer with an account the vault no longer holds — the usage story deletes a real item mid-run and reads it as forgotten on the next call, and a ten-second window in 0.4.29 failed it. The per-agent subscription cache above it is unchanged. 0.4.32 (0.4.29 and 0.4.30 were claimed by linux builds submitted with a builder script that refused its own work root, 0.4.31 by the window; Stado 0.21.15 fixed the builder).

Unreleased: rotation reads the refusal's class, not the provider's words

A refused provider answer now carries failure_kind (rate_limited, authentication, dependency_unavailable, provider_failure), taken from the HTTP status, or from the error object's code when a Responses stream fails after its 200. Subscription rotation refreshes and rotates on authentication, and moves to the next account on rate_limited; any other refusal condemns the route only. The provider's sentence no longer decides either, so a request error that mentions OAuth or a limit leaves the account alone. A credential refused again right after a forced refresh is still retired, and a refresh the provider disowns is still recorded as needing a sign-in.

Unreleased: shared second-factor recovery and observed approvals

CLI, Desktop and background sign-ins use one recovery path. Weles can answer an offered Google phone challenge when no authenticator seed is held. A structured google_2fa_material_missing refusal orders enrolment for the same resolved subscription and login; only a confirmed worker run and independent seed readback permit a new sign-in and credential refresh. An existing authenticator is not rotated.

The saved verdict and pool retain the observed second-factor method and Weles run. Unknown evidence is no longer inferred as “no second factor” from a successful grant. CLI and Desktop distinguish a missing seed from a failed seed inspection. Source changes alone are not a passing real authentication run.

Unreleased: an imported member names its account, so Weles can sign it in when its grant dies

Weles resolves an automatic sign-in from the subscription item's account_ref and a Skarbiec login item for that principal. Every member the pool gained through import or sync carried the harness's account in its verdict and none in its item: /readyz reported subscription_identity_missing for held-claude-code-<account slug>, wisent-app-claude-secondary, wisent-app-claude-tertiary and wisent-app-kimi-primary, and the loop that exists to replace a burnt grant could not start. A stored grant now carries the account the harness recorded as the item's account_ref, written beside the credential and preserved on every later write; a caller that knows no account leaves what the item carries. The next thing that blocks the sign-in is the one the login item lacks — on this fleet, an authenticator seed, which stado credentials seed-freshness reports as seed_field_empty for every Claude, Codex and Kimi login. 0.4.28.

Unreleased: traverse every eligible subscription

Buffered and streaming requests no longer stop at a fixed prefix of the eligible credential list. Unblocked accounts remain first; every skipped blocked account contributes its recorded refusal and reset to the exhausted pool result. A rate-limited account later in the list is therefore not hidden behind earlier authorization failures. Explicit billing targets still select only the named credential. Rotation stops after a successful response, a non-rotatable provider refusal or the first committed stream byte.

Unreleased: a gateway that has obtained one direct credential says so before the subscription sweep ends

The readiness sweep has two halves: one capability per direct provider, then every agent's subscription discovery and one redemption per active subscription. Until the whole sweep ended /readyz answered readiness check has not completed, which is 503, and the second half takes as long as Skarbiec and every provider take. A candidate still inside it when Stado's 90-second readiness window closed was quarantined (candidate did not become ready within 90s) and the host kept serving the previous release — one that still rotated borrowed grants, which went on to revoke an operator's own session on one of those accounts after the release meant to stop it had been refused a rollout by the gateway it replaces. The first half now publishes its own verdict the moment it is known: ready when one direct provider credential was obtained, degraded either way, reason a direct provider credential was obtained; the subscription sweep is still running; the full report replaces it when the sweep ends. 0.4.25.

Unreleased: a Claude grant rides as the Agent SDK, the way the harness Anthropic answers sends it

Anthropic answers a Claude request that does not look like its own harness's with Third-party apps now draw from your extra usage, not your plan limits, while the harness on the same account is answered on plan limits in the same minute. Brama still sent an older Claude Code CLI shape; the harness Anthropic answers sends Claude Code 2.1.165 as the Claude Agent SDK 0.3.165: user agent claude-cli/2.1.165 (external, local-agent, agent-sdk/0.3.165), entrypoint local-agent, the sentence "You are a Claude agent, built on Anthropic's Claude Agent SDK.", Claude Code's agent beta list, the Stainless client headers and a fresh x-client-request-id. Brama now sends that shape, byte for byte where the provider can see it. Members the pool held for accounts a harness also held died with invalid_grant -- Refresh token not found or invalid — and so did the harness's own copies, under the operator's open session: Brama had refreshed the shared grants and the provider revoked the refresh tokens the harness held. A grant taken from a harness is now borrowed: Brama spends it as it stands and never rotates it, the pool report names the harness in credential.borrowed_from, a forced refresh disowns the member with this grant is borrowed from omp, which refreshes it itself, and brama subscription sync hands every borrowed grant over on every pass — the gateway answers unchanged while the harness has not rotated it, and stores the new grant when it has, so the pool follows the harness instead of dying behind it.

Unreleased: every account the machine holds joins the pool by itself

The accounts below were added by hand, one import at a time, and the operator's answer was that this is the opposite of how it should work: the pool has to see every account the machine holds without a person naming it. brama subscription sync --reason <why> [--gateway <origin>] (since removed) lists what every harness holds, derives one stable member id per account (brama-sub-held-<provider>-<account>), leaves every id the pool already holds to Brama's own refresh, and imports the rest through the same path import takes, one verdict per grant. Brama Desktop runs the sweep from the runtime it bundles, once when its gateway is connected and every thirty minutes after, and its Subscription pool screen says what the last sweep did, with Sweep now beside it.

Unreleased: an imported grant for an id the pool does not hold yet joins the pool

Every Claude call through best is refused with all bounded 'claude-code' credentials unavailable for agent when the pool holds one rate-limited account while the machine holds more with quota that no pool member names. brama subscription import --gateway answered 404 subscription not found for any id but the one already there, so the only way to add an account was a sign-in window. An import for an id the pool does not hold yet now creates that member under the named provider - the CLI sends the provider with the grant, storing the grant creates and tags its vault item, and brama subscriptions lists it at once. POST /v1/admin/subscription-pool/grant takes provider for the same reason and refuses without it, or with one whose grants the refresh path cannot renew. The first import through the deployed gateway then answered no capability route maps resource `provider:claude-code:brama-sub-wisent-app-claude-secondary` while stado route capability brama listed that route: the gateway read the operator’s routes table file itself, and a route Skarbiec declares from an item’s own tags — every one a sign-in or an import creates — was never in that file. The gateway now asks Skarbiec (route resolve) for the coordinate a resource stands for, the same answer the authority gives, and reports Skarbiec’s own sentence when nothing declares it.

Unreleased: a grant any harness already holds is taken, not asked for again

The harnesses beside a gateway are often signed into the accounts the pool needs long before the pool is. Four harnesses sign accounts in on an operator's machine - omp, Claude Code, Codex CLI, Kimi Code - and each keeps the grant its own way. brama subscription held (since removed) lists what every one of them holds, never the grants; brama subscription import <provider> --from <harness> --subscription-id <id> --reason <why> (since removed) reads that harness's own store read-only, rewrites the grant into the document the refresh path reads for claude-code, codex or kimi, refuses one it could not renew before storing anything, stores it as the subscription's credential, marks the row signed in, proves it with one minimal completion, and journals the verdict with the harness it came from. With --gateway and the console's bearer on stdin the same command hands the grant to a gateway elsewhere through POST /v1/admin/subscription-pool/grant; Brama Desktop's Take a grant a harness holds… runs it from the runtime it bundles, so the desktop reads no store and knows no format.

Running that against the deployed gateway found the admin family unreachable in production: every /v1/admin/* path answers only a brama-desktop identity with no allowlist, such an identity can only come from the boot table, and the launcher never put one there - the authority resolves a workload bearer with the routes it was issued, never without them. Brama Desktop and the terminal alike were answered 401 model_bearer_unrecognized; only the test harness, which seeds the console into the table, ever reached the family. The launcher now projects brama-desktop-model-router#token - the item the desktop acquires - into the boot table as brama-desktop, optional like the renewal client, and says on its stderr when it could not read it (who the admin family answers).

Every grant that reaches Brama without a Weles sign-in - pasted code, harness, console - is now proved by a probe instead of a refresh. A refresh rotates the refresh token and revokes the one it replaced, so proving a grant the harness still holds would have killed the harness's copy and failed every session the operator had open on that account at its next refresh; the probe spends one completion on the access token both sides hold and rotates nothing. The sweep rotates the grant later, as the access token nears its end, and the harness's copy goes stale then - which the import page says plainly.

Unreleased: a Claude account is signed in by hand, without Weles

brama subscription sign-in-manual claude-code --subscription-id <id> --reason <why> (since replaced by sign-in --by hand) prints the provider's authorize page, waits for the code the operator pastes after logging in with their own browser, exchanges it at the provider's token endpoint, stores the grant as that subscription's credential in the shape the refresh path reads, and proves it with one completion. The page carries the client id, scopes, loopback redirect and PKCE challenge that omp auth-broker login anthropic sends, so the grant is the same kind the automatic renewal already keeps alive; the manual step is not repeated until the provider revokes the refresh token.

Brama Desktop does the same in two steps, because the gateway is usually not on the machine the person logs in from: POST /v1/admin/subscription-pool/sign-in-manual returns the page and keeps the PKCE verifier in the gateway under a sign_in_id for 900 seconds; POST /v1/admin/subscription-pool/sign-in-manual/:sign_in_id takes the paste and ends in the same exchange, store and proving refresh (HTTP API). Both are brama-desktop only. They have since become POST /v1/admin/subscription-pool/sign-in with "by": "hand" and …/sign-in/:sign_in_id.

A pool whose every Claude subscription holds no credential — Skarbiec items carrying a document with no grant — while the only working account is signed into the harness on the operator's laptop, and every sign-in Brama offers goes through Weles, which needs a login row and a browser host that account does not have, is a gateway every product is told to use with nothing to answer with while the harness beside it has. The runbook entry A request answers 429 subscription_unavailable now names this command as the repair for that state.

Unreleased: the request path leaves a disowned grant alone

A request that ranks an agent's subscriptions no longer reads a grant the ledger already records as needs_reauthorization: the sweep left such a grant alone until a sign-in replaced it, and the request path now does the same, reporting it as awaiting_sign_in instead of paying for a router child process, a broker redemption and a refused refresh on every call. A document with no refresh token could be refused several times a minute for days while callers waited. A credential the vault refuses outright is remembered for sixty seconds, the window a failed model discovery already had.

Two events attribute the time a request spends before the provider answers: credential_obtained (resource, path, elapsed_ms) once per managed credential, and subscription_models_discovered (subscriptions, read, awaiting_sign_in, failed, elapsed_ms) once per ranked request. The runbook entry A request is slow before the provider answers reads them.