Released 0.2.77 to 0.2.42¶
Published releases 0.2.77, 0.2.76 and 0.2.42. Each entry describes the contract as it stood when that release was cut; where a later change replaced that contract, the current behaviour is on unreleased changes. Older releases are on 0.2.5 to 0.2.2.
0.2.77¶
The managed backend identity is derived from the four required backend aliases, so optional route names cannot make its startup allowlist contradict the server contract. A mismatch reports the expected and observed names without printing credentials.
0.2.76¶
The managed launcher and server use the single wisent-backend/chat alias and the weles alias. That chat alias is now named wisent-backend
(unreleased).
Existing route configuration can be reviewed and adopted¶
brama adopt, first-use --adopt-* options and Brama Desktop share one Brama-owned preview/apply operation for schema-1 inference-route
registries. It discovers only configured provider acquisitions and secret-free Skarbiec subscription identities, requires exact route selection, validates the complete import before one
atomic route-file write, keeps existing aliases unless told otherwise, and reports imported, unchanged, conflicting and rejected aliases. It never copies credentials, activates a
subscription, or performs inference. An operator moving an older registry to the current document shape uses brama routes migrate.
0.2.42 - 2026-08-27¶
Re-cut of 0.2.41, whose immutable release coordinate was half-published while the fleet object store was recovering from an outage; the changes below are 0.2.41's, unchanged.
brama subscription sign-in closes the runbook's repair loop¶
The runbook's repair for a burnt credential — sign the subscription in again, then refresh — had no command. brama subscription sign-in <provider> --reason
"<why>" is that command: on the host Weles runs on it asks Weles's worker API to drive one named sign-in row (--login-item, or the single row the provider has;
two or more are refused by name, never guessed between), confirms Weles echoed the exact row asked for, runs the same refresh brama subscription refresh runs, and reports
signed_in/failed with the refresh verdict nested beside it. Every run is journaled as subscription_sign_in with its reason, verbatim. No browser opens on
the calling machine and no credential material is read or printed.
Skarbiec owns capability route mapping¶
Brama no longer writes capability-routes.json or chooses provider credential fields. At startup it asks the bundled Skarbiec binary to reconcile identity routes from the live
vault; Skarbiec validates the item schema, preserves every existing mapping, records a backup and audit entry, and reports ambiguous items without guessing.
Wisent accounts can route models through their own provider API keys¶
POST /v1/account/subscriptions accepted credentials for every supported remote provider, including OpenAI and Anthropic, instead of limiting the account surface to Claude Code,
Codex and Kimi subscriptions. The verified Wisent session owns the stored coordinate; plaintext crosses only the request and Skarbiec write pipe and is never returned. Canonical
provider/model calls made with that session use only the account's active credential, buffered and streaming alike, and GET /v1/models combines public metadata with
availability discovered through those keys. Deployment credentials, another user's keys and loopback local-openai are never eligible. That account surface is now the one
subscription pool capability (unreleased).
best became a selector over the caller's own subscription models¶
A signed best call could answer 503 credential_unauthorized with "no 'codex' credential could be redeemed for agent; a capability, read grant, or this
installation's trust material is missing" and attempts: 0, in the same second that the same agent's kimi subscription was serving 200 on a canonical route.
best was resolved as an alias — one configured provider route, here codex/gpt-5.3-codex-spark — so one refused redemption ended the request and the eleven
other subscription models the caller held were never asked.
best became a selector rather than a route. A selector ranks models the caller's own subscriptions provide: the configured model leads, because an operator naming a model is
naming a preference, and behind it stand that caller's remaining subscription models in the order the ledger already ranks them. That is model selection inside one request, not a set of
destinations attached to an alias: an alias resolves to exactly one route and its refusal is what the caller receives
(unreleased). Every ranked selector shares one loop — best, any,
any-vision-capable and task:<name>, buffered and streaming alike — and a refusal that empties a provider's whole credential pool retires that provider for
the rest of the request, so its remaining models are skipped unasked instead of collecting the identical sentence. That distinction is carried as structure rather than re-parsed from the
message, so a provider that answered and refused one model on its own merits still leaves its other models eligible, and the model budget is spent on provider round trips that actually
happened. A failed request names everything it walked past — "no working subscription model for signed agent; codex refused (...), kimi refused (...)" — and
attempts counts what was really attempted, so 0 again means no provider was ever asked.
A donation that carries no credential is refused instead of banked¶
A provider's -primary vault item — a heavily used codex account — held a browser context options document
(deviceScaleFactor, extraHTTPHeaders, recordHar, recordVideo, viewport) instead of a credential. The gateway's own
plan-usage read reported that verbatim, so the coordinate redeemed perfectly and the thing at it was a re-authentication trajectory's configuration object rather than a credential; the
ledger read active throughout, because storing a donation is what clears needs_reauthorization. The write paths bounded a donated credential by length alone (1..8000
characters, which any JSON document satisfies) and wrote it to the single coordinate that provider's -primary subscription is read from, so a donation carrying anything else
destroyed a working credential and recorded the destruction as a successful sign-in. A donation is now reduced to a bearer before anything is written, by the same reduction the request path
performs, so nothing a request could have presented is refused; a document that carries no credential answers 400 naming the shape it had and the fields it could have carried,
and the stored credential is left exactly as it was. A failed write answers as this installation's fault, which is a different repair and a different answer.
The launcher asserts a capability route for every banked subscription¶
A signed codex/gpt-5.6-sol call as wisent-app could answer 503 credential_unauthorized with attempts: 0 in the same minute that
the same agent's kimi subscription served 200. Nothing was wrong with the credential: a banked subscription is spendable only when the vault holds the item, the signed policy
allows its resource, the routes table maps that resource to an item and a field, and issuance succeeds — and that routes table was the one of the four written by a helper somebody had
to remember to run, so a freshly banked seat had no coordinate and capability-issue refused it by name.
start-with-skarbiec.sh now asserts the table at every start from the host's own contents, through the packaged provision-capability-routes.py. It is additive: an
existing entry is never repointed or removed, the coordinate is the item's own id, and the field is taken only when the item carries exactly one, which is a fact rather than a choice. An
ambiguous item is named and skipped rather than guessed at, and a failed provisioning pass is reported without stopping a gateway that can still serve every other subscription. Adding a
coordinate widens nothing: redemption is still authorised by the workload key the vault registers and by the recipients the item carries, never by this table.
SKARBIEC_CAPABILITY_ROUTES_FILE is exported unconditionally rather than only when the file already exists, because the gateway's read-grant path resolves coordinates through that
variable while the authority resolves its own path beside the vault, and an unset variable left the two readers disagreeing about which table is in force.
Readiness performs the act instead of reading a declaration about it¶
/readyz could answer ready: true all morning on a gateway that could serve nothing: every live probe refused (codex
503 credential_unauthorized, claude-code 429 all bounded 'claude-code' credentials unavailable for agent, kimi 429 no active 'kimi' credential for
agent). Both halves the endpoint had were declarations — a provider credential obtained once at the top of the check, and a catalogue entry proving a model was listed at some
point. /readyz keeps those halves and adds a third that performs the act: for every active subscription of every configured request-sign agent it redeems the credential at the
same broker boundary a request redeems at (one attempt per subscription, no model call, the credential dropped unread) and reports { id, provider, redeemable, reason }. The
reason is the request path's own sentence, written once and used by the buffered path, the streaming path and this probe, so one broken chain cannot read as two different faults depending on
which surface an operator looked at, and ready is false when any active subscription is not redeemable.
The endpoint also reports the accounts nothing could see. On the always-on host provider:kimi:brama-sub-wisent-app-kimi-primary sat in the vault at revision 144, state active,
with an empty tag list, because the pre-fix Skarbiec binary that host ran rewrote the item without its tags on every rotation. Subscription discovery finds an account by its
brama:agent:<id> tag, so losing that tag removed a paid, working, unexpired account from the fleet, and the listing that lost it is the one thing that can never report it
missing. /readyz reads the vault through the same entitlements router the gateway shells and names every subscription account carrying no brama:agent: tag; such an
account is reported unroutable with the sentence a request for that provider receives, and it makes the gateway not ready. No secret crosses this endpoint: it answers with names, booleans
and refusal sentences, and stays cheap enough to poll.
The subscription pool can be read and refreshed from the CLI¶
Browser automation across the company stopped for most of a working day because this pool was empty: both codex subscription credentials were burnt at once, every best-aliased
call answered 429 subscription_unavailable, and the product would not say which of the two possible reasons was true. The gateway had known since its first refresh sweep —
the ledger carried needs_reauthorization against both grants with the provider's own sentence beside them — and the only way to reach that was to grep
brama-always-on.err and read timestamps by hand. Repairing it was worse: a burnt grant is never inside the refresh timer's skew window, so the timer that exists to replace grants
was precisely the thing that would not touch these.
brama subscriptions list reports the pool as the gateway sees it: one row per subscription with its provider, its state (live, expired,
burnt or unknown), the provider's stated expiry, and the refusal standing in its way in the words of whatever refused it. It is read-only in the strict sense: no
provider is contacted, no capability is redeemed, and the ledger is read without being written back. Rows come from the deployment's subscription listing joined to the ledger, because either
source alone hides accounts the other holds; that listing is now brama subscription list.
brama subscription refresh <provider> --reason <text> runs the same refresh the timer runs, for one provider, now, and reports whether a credential was obtained; it
shares the sweep's code path, so a grant cannot come back alive here and dead there and every refusal is classified once. --reason is required, because rotating a grant
invalidates the previous refresh token, and it is journaled beside the verdict. An attempt that cannot proceed says which of three reasons it was — no usable subscription in the pool, a
provider whose credentials are API keys and have no refresh path, or no usable credential source in this environment — rather than reporting a broken account. A retired subscription is
left alone, both commands support --json, and neither can print credential material.
A credential that never existed is no longer reported as a busy provider¶
Running the first-use journey on a workstation with no capability and no read grant produced 429 capacity_error, retryable: true, and "all bounded 'codex'
credentials unavailable for agent" without any provider being asked anything: the vault produced nothing to present, which is a broken authorization chain — a missing capability,
a missing read grant, or missing installation trust material — and no amount of waiting mends one. That reason is now its own answer, 503 authorization_error with code
credential_unauthorized and retryable: false, saying which of the three links is missing. The other two keep their verdicts: a provider that refused every credential
answers subscription_reauthorization_required, and a genuinely exhausted pool answers 429 subscription_unavailable, the only one of the three worth waiting out. This
was the third time the same shape was fixed here, so classification is keyed on which link broke rather than on the sentence the last layer happened to write.
Callers can stream, and a committed stream is never re-run¶
Brama answered every generation in one piece, which made the gateway unusable for anything interactive. POST /v1/chat/completions now streams when the request says
"stream": true, as text/event-stream carrying chat.completion.chunk frames closed by data: [DONE], with a periodic keep-alive comment frame
so an idle proxy does not close a stream that is legitimately silent while the model thinks. The retry contract is what needed deciding, and the boundary is the provider's response status:
before the stream commits, everything documented in CORE.md still applies — bounded model candidates for a selector, bounded credentials each, one
forced OAuth refresh on an auth refusal, the whole-request deadline, and a refusal is an ordinary error document because the caller has received nothing. After it commits, nothing is retried
at all: a provider that fails, stalls between reads past the read window, or ends without its terminal event ends the caller's stream without one either — no data: [DONE]
— because a second attempt on another credential would duplicate both the bill and the emitted text. The whole-request budget stops applying at that same moment, because a total budget
cannot tell a model that is thinking from a socket that has died and the per-read window can. Subscription spend is recorded once per stream whichever way it ends, including a stream the
caller abandoned: the tokens the provider reported and the plan windows its headers carried are ledger facts regardless of who stopped listening. Kimi streams carry no token meter, because
its coding endpoint's field set is pinned and publishes none; that absence is recorded as absence rather than filled in with an estimate.
Anthropic Messages and OpenAI Responses are accepted at the ingress¶
Brama spoke one dialect, OpenAI chat completions, so every client speaking Anthropic Messages or OpenAI Responses — which includes the agent runtimes whose subscriptions this gateway
holds — needed a translating proxy in front, holding one more copy of the routing policy. POST /v1/messages and POST /v1/responses are served directly, buffered
or streamed, in the caller's own format: Anthropic message_start, content_block_*, message_delta and message_stop events, or
response.* events closed by response.completed. They are one workflow with the chat endpoint, not three: client identity, model allowlist, alias resolution, selector
semantics, caller-scoped subscription ownership, attempt bounds, ledger accounting and the error contract are shared line for line, and only the shape of the request and the answer differs.
Each still requires a canonical provider/model route or a supported selector, because a bare vendor model name is not a routing decision Brama is willing to guess. Inbound
translation keeps only what a provider-neutral request can hold: stop sequences, cache-control hints, reasoning options, stored-response identifiers and non-function tool types are accepted
and dropped rather than approximated.
Routing reads the plan windows it already collects¶
Brama had been reading each subscription's plan windows from the providers' own free usage reports and then choosing which subscription to spend by shuffling, so an account the provider said
was 95 percent spent was as likely to be picked as an idle one and a fleet with four Claude subscriptions still walked into 429s it could have seen coming. Selection now orders candidates by
what their plans have left, from the ledger and at no provider cost: a selector orders its model candidates by the freest usable subscription behind each, and an explicit route orders its
bounded credentials the same way. Chance only breaks exact ties, so accounts at equal utilization still decorrelate. A window whose own reset instant has passed counts as empty, because the
provider's clock says it rolled; a subscription with no reading counts as free, because its first call writes the reading that corrects the placement. Quality still outranks quota: inside one
task: score, plan headroom is the tiebreak, not the ranking.
One agent stays on one account for the length of a window¶
Consecutive requests from one agent could land on a different subscription each time, and two things were lost without anybody being told: a provider's prompt cache lives behind one account,
so scattering an agent's turns across a pool threw the cache away on every turn, and one conversation's spend was smeared across every account the agent owned instead of accumulating where an
operator could read it. The credential that served an agent is remembered per provider and tried first on that agent's next request, until the tightest window it reported resets — five
hours when the provider named no reset, capped at a day. It is a preference and never a grant: consulted after eligibility, skipped for a credential inside a block or reporting a full window,
never overrides billingTarget, and never widens the bounded attempt count. It lives in the serving process only, because a pin whose window has passed is worth nothing to the
process that starts next.
A credential is refreshed before it dies, and a refused one says so¶
Brama refreshed an OAuth grant at exactly one moment: inside a request, after the local expiry said the token was spent or after the provider rejected it. Nothing happened for a subscription
no request reached, so four credentials whose refresh tokens their providers had already disowned sat in the vault reading as active for five days while every request that touched them failed.
Brama now refreshes every active subscription credential expiring within BRAMA_CREDENTIAL_REFRESH_SKEW_SECS on the BRAMA_CREDENTIAL_REFRESH_INTERVAL_SECS cadence,
single-flighted per subscription so a slow refresh is never started twice; refreshing spends no plan quota, because a token endpoint is not a metered endpoint. Every refresh failure is
classified rather than logged and forgotten: a definitive refusal (invalid_grant, invalid_token, a revoked or unauthorized refresh token, or a 401/403 that is not a
transport failure) is recorded against that subscription, which is left alone until a sign-in replaces it, while a transport failure changes nothing and is retried by the next sweep. A
refreshed grant that cannot be written back to the vault is a failed refresh rather than a success with a warning attached: the rotated grant is dropped rather than spent from memory, because
the provider has already invalidated the one still stored. A subscription row therefore carries a credential object — state (active,
needs_reauthorization, disabled), the provider's own sentence as cause, and recorded_at_ms, expires_at_ms and
refreshed_at_ms when known — absent while nothing has been recorded, and a ledger written by an older gateway still loads.
Plan usage is read from the provider, not bought with a completion¶
A provider states its plan windows in the headers of an answer, so a window used to exist only for the account that happened to serve a request: of seven subscriptions on the fleet exactly
one reported a plan and the other six were indistinguishable blanks. The first fix spent one deliberately minimal completion per active subscription every quarter hour, buying the statistic
with the quota it was measuring; that timer is gone, and with stock configuration no timer performs a quota-consuming request at all. Brama reads each provider's own free usage report instead:
claude-code from GET /api/oauth/usage on api.anthropic.com, codex from GET /backend-api/wham/usage on chatgpt.com,
and kimi from GET /coding/v1/usages on api.kimi.com. A provider that publishes no report is recorded as publishing none, which is a fact about the vendor
rather than an error. Each subscription is read at most once per BRAMA_PLAN_USAGE_TTL_SECS, spread by up to a quarter either way from the subscription's own id so seven accounts on
one host never fan out into one burst against a provider that rate-limits usage reads per address, and single-flighted per subscription; the sweep that notices aged-out rows runs on
BRAMA_PLAN_USAGE_SWEEP_SECS. A failed read never blanks a row: the last good reading is kept and served with stale true, and is discarded once it is older than
BRAMA_PLAN_USAGE_RETENTION_SECS.
A row therefore says where its newest window came from and how fresh it is: usage_source is provider, traffic or probe, and
stale is true once the newest reading has aged past the freshness window. Together with the instant each reading was taken (recorded_at_ms), when the record last
changed (observed_at_ms) and the newest check verdict (probe, carrying source: usage_report or completion), a reader can tell
apart the reasons a plan can be blank — the provider publishes none, nothing has ever gone through this account, or the credential is being refused. The paid check still exists, because
a free report cannot say whether a provider will actually serve a credential: it became the explicit operator action
POST /v1/admin/subscriptions/:agent_id/:subscription_id/probe, reachable by the desktop console's identity, which spends one minimal completion against one named subscription and
returns the verdict it recorded as probe. A subscription inside a recorded rate-limit block is refused with 409 rather than probed, and the probe rotates to no other
credential and retires nothing. Plan usage is now read through one capability (unreleased).
Local inference yields to fleet work¶
A deployment-owned alias could name the same TheDrummer/Cydonia-24B-v4.3 model on Featherless that local vLLM serves, so fleet work could be pointed at the hosted copy while the
local one was busy. Under the current contract such an alias names one of the two and its refusal is what the caller receives. Featherless is a native OpenAI-compatible direct provider, and
per-installation Skarbiec policy derives its direct-provider grants from the authoritative Brama control document instead of an unrelated subscription list.
OAuth credentials recover from provider rejection¶
Subscription access grants can be rejected before their local expiry claim. Brama forces one refresh-token exchange after a provider authentication failure, persists the rotated credential, and retries the request once. An authentication failure that survives the refresh moves to the next bounded credential of that same subscription pool and is recorded as an authentication failure rather than as a 15-minute rate-limit block.
Probierz vision calls use the Probierz identity¶
The probierz-model-router bearer is bound to probierz, whose dedicated request-signing secret is loaded from probierz-agent-auth. The two Codex
subscription routes admit that identity, so any-vision-capable evaluations no longer fail after capture with a missing x-agent-id header or borrow another product's
signing identity.
Readiness now answers for the credential chain¶
GET /health is liveness and always was: its own body says dependencies: not_probed. It was nevertheless what every deploy check in this repository used as proof that
a release worked, and it answers ok from a gateway whose every capability redemption is being refused; the failure surfaces only when a person asks
a model a question. GET /readyz is new and public: it redeems one capability per configured provider and returns 503 naming the providers that failed, carrying no
secret beyond the provider name and whether its credential could be obtained. Deploy checks and monitors read it; /health proves only that the process is running. A refused
redemption is also classified honestly: it was reported as 429 capacity_error with code subscription_unavailable and retryable: true, while Skarbiec was
saying capability is not issued, has expired, has no uses left, or its authorization id does not match. It is now 503 authorization_error, code
credential_unauthorized, retryable: false, because no amount of waiting repairs an authorization id that does not match.
Released under the wrong name¶
v0.2.9, v0.2.10, v0.2.11, v0.2.12 and v0.2.13 were cut from trees that already declared 0.3.0, so five published
releases carry that breaking change under patch-looking names. Anyone who upgraded to one of them has a service that will not start until bin/provision-skarbiec-trust has run, and
the version number gave no warning. A published coordinate is immutable, so the names cannot be corrected: each of the five release notes now opens with what the artifact actually contains and
what the operator must do. scripts/baseline.py no longer aborts when it meets a release whose tree disagrees with its name — it reports it, skips it, and keeps looking
— because before that fix one mis-named release froze the baseline eight releases behind.
Security¶
- The release archive no longer contains any signing key. Until then the release build ran
scripts/generate-skarbiec-config.mjs, so every download of one archive carried the same Ed25519 workload proof seed inetc/brama-skarbiec/brama-proof.key, alongside a signed ten-yearpolicy.jsongranting provider authentication and request signing, atrust.jsonvouching for both, and aworm-receiptstub that discarded audit records while the policy pinned its digest. The launcher pointed at exactly that directory, which is always present in a published archive. - Trust material is generated per installation by
bin/provision-skarbiec-trust, andbin/start-with-skarbiecrefuses to start while any of it is missing rather than reaching for a shared copy. The registry pins the absolute path and SHA-256 of the binary allowed to redeem a capability, which is knowledge a build machine does not have, so generating it on the host is also the only way that pin can be correct.
Changed¶
- Incompatible.
etc/brama-skarbiecin the archive holds onlysubscriptions.jsonandrecipient-public-keys.asc; the generator ships aslibexec/generate-skarbiec-config.mjsand is not executed at build time. An installation that relied on the bundled trust material must runbin/provision-skarbiec-trustonce and re-grant the capabilities that were bound to the discarded key. Under the pre-one policy a breaking change advancesMINORfrom the published0.2.5, so this is0.3.0.
Fixed¶
POST /v1/chat/completionsaccepts OpenAI-compatibletool_choiceand carries forced tool selection through OpenAI Chat, OpenAI Responses, Anthropic Messages and Google GenerateContent adapters instead of rejecting the request as an unknown field.- Documentation asserted that no immutable public release existed while five were published.
README.md,ONBOARDING.md,RELEASE.md,examples/README.mdandexamples/recovery/upgrade-and-rollback.mdgive the download-and-verify install path instead of sending every reader to build from source. - Documentation no longer restates which release is newest, which source version is current, or which version an example targets: every such literal went stale within hours of being written.
Prose points at the two machine-maintained records —
released-surface.jsonand the GitHub Releases list — andCargo.tomlstays the single source of the source version. RELEASE.mdrecords that a tag alone is not a release, because a tagged revision whose release workflow did not finish leaves a coordinate with nothing installable behind it; this has happened twice, atv0.2.0and again atv0.2.6. It also records the real archive contents, including the bundled Skarbiec entitlements router and the launcher's trust material, and states that bundled material as a known gap instead of claiming that credentials are never shipped.released-surface.jsonrecorded0.2.4after0.2.5was published; regenerated withscripts/baseline.py --write.
Qualification¶
No test, test suite, smoke test, provider call, or deployment validation was created, modified, or executed while preparing these changes, because explicit human testing consent was not provided.