Errors¶
Brama speaks two error vocabularies to two audiences, and the split is
deliberate. What a client reads is the HTTP error body — a stable type and
code that predate the fleet vocabulary and do not change. What an operator
reads is the wisent-errors envelope, logged beside the client body with the
fleet's code, failure point, and impact. Where the two readings differ, both
are on the line; neither stands in for the other.
The client contract¶
Every public error uses this body:
{
"error": {
"message": "bounded human-readable detail",
"type": "stable_class",
"code": "stable_code",
"retryable": false,
"attempts": <provider calls the dispatch counted>
}
}
retryable is stated explicitly, and attempts bounds any replay the caller
considers: selector and credential replay never exceeds it. A refusal made by a
dispatch states the provider calls it counted, zero included; a refusal Brama
made before any dispatch (authentication, transport, a malformed request, an
alias it cannot serve) asked no provider and carries no attempts key.
Message text is diagnostic, not the machine contract.
Subscription reports also carry ok and errors. These are completed reports, so HTTP 200 can contain ok: false and partial rows. The errors array contains standard Wisent envelopes with failure_point, error_code, detail, and context identifying the account and attempt time. Discovery failures are not empty inventories; expired measurements are not current; missing metrics are not zero. CLI reports exit non-zero and desktop clients show the same errors beside retained stale readings.
| HTTP | Code | Meaning | Caller action |
|---|---|---|---|
| 400 | invalid_request |
malformed JSON, route, selector, or limit | Correct the request; do not retry unchanged |
| 401 | unauthenticated |
bearer or HMAC missing/invalid | Repair or rotate the exact identity |
| 403 | forbidden |
valid identity lacks model/agent/path authority; the message names the client and which one: the model and the models its credential is scoped to, the path a model-scoped credential may not reach, or the agent the bearer is bound to against the agent that signed. A refused model on a workload credential also names the Skarbiec capability it lacks (call:brama#<model>), says that call:brama with no route issues every route, and gives the stado credentials token mint command that issues it on the vault owner |
Correct grant or request; do not substitute identity |
| 404 | subscription_not_found |
owned lifecycle target does not exist | Refresh inventory or correct ID |
| 409 | state_conflict |
requested mutation conflicts with current state | Read current state before retry |
| 426 | secure_transport_required |
neither loopback nor trusted HTTPS peer | Use approved HTTPS ingress |
| 429 | provider_rate_limited |
bounded provider quota/rate attempts exhausted | Wait or choose an explicit authorized target |
| 429 | subscription_unavailable |
no usable agent credential in bound attempts; at least one is inside a recorded quota block | Wait for the provider's quota reset or choose an explicit authorized target |
| 502 | provider_failure |
provider returned permanent/malformed failure | Inspect provider classification; retry only if stated |
| 502 | decision_contract_violated |
a decision was answered outside the schema the caller declared: a label the question never named (read without surrounding whitespace), one label named twice, or no probability mass at all. The message names the route that answered; details carry alias, route, answered_by (for best-decision-model, the subscription model it selected), reason and reply, the reply as the model wrote it |
Point the alias at a route that can keep the contract; waiting does not reach it |
| 503 | alias_unserviceable |
the alias is declared or known and has no route this host can serve; details carry state and route |
Declare a route with brama routes set or PUT /v1/admin/routes, or issue the provider capability |
| 503 | credential_unauthorized |
redemption was refused, no capability, read grant, or trust material could produce a credential at all, or the credential produced carries no key | Repair the authorization chain; waiting does not reach it |
| 503 | subscription_reauthorization_required |
every bounded credential was refused by the provider; the refusal is retryable and wakes the gateway's own maintenance, which renews the grant or signs the member in through Weles by itself | Retry once the pass ends; nobody signs in by hand. Only a member in /readyz automatic_sign_in.blocked needs what that entry names |
| 503 | dependency_unavailable |
required catalog, broker, vault, or provider unavailable, or a provider request the transport did not carry; the message retains the transport's own cause. A request the provider never saw is sent once more, except to a host that never answered the connection (the system's connect wait ended, Operation timed out): that host is not waited on a second time, and the message names it |
Restore named dependency |
| 500 | internal_error |
Brama failed outside a classified dependency | Operator investigation required |
A pool can contain both a refused credential and an account that is only out of
quota. When no account serves the request, an observed quota refusal takes
precedence: Brama returns that provider's original 429 message rather
than claiming every account needs a new sign-in. A recorded quota block returns
429 subscription_unavailable without contacting that provider again.
When no account can recover by waiting for a quota, the authorization refusal is
503 subscription_reauthorization_required with retryable: true:
it wakes the gateway's own maintenance, which renews the grant or signs the member in
through Weles. Buffered and streaming requests use the same precedence.
The code a refusal is answered with comes from the class the refusal was
stated with where it was built — the provider's HTTP status for a provider
refusal, Brama's own reason for a refusal Brama makes — and never from the
words of its message. A message may change its wording; its code does not.
A walk over several candidates is answered with the most telling class it
met: a spent balance, then a rate window or a quota block a wait reaches,
then an authorization failure, and otherwise
429 subscription_unavailable.
A selector request (best, any, task:<name>)
whose every listed member the ledger already refuses is refused before any
vault child or provider is asked, with the message
every subscription is refused by the ledger before any is read: <member>: <reason>; …
and attempts: 0. The reason per member is the ledger's own: awaiting sign-in: <the provider's refusal>
for a grant the provider disowned or an operator retired (503 subscription_reauthorization_required),
paid balance spent, recorded until <UTC instant>: <the provider's refusal>
(502 provider_quota_exhausted, not retryable) and rate limited, the block lifts at <UTC instant>: <the provider's refusal>
(429 provider_rate_limited, retryable); the class answered is the most telling one among the members, in the
walk's own precedence. A member the ledger does not refuse is read and discovered; when that read fails the message
begins could not discover native provider models: instead. When a member awaiting sign-in has an
automatic sign-in attempt that failed and still stands, either message ends with
; the automatic sign-in that renews them failed: <member>: <code>: <detail>; …, the
failure code and detail that attempt recorded (for a Weles refusal, the stage and Weles's own words), so the
cause of a pool nobody can renew is in the refusal itself. A pool every member of which needs a sign-in is
refused with no model: every member of the subscription pool needs a new sign-in, so no request was sent:,
and each member there reads the automatic Weles sign-in that renews it failed: <code>: <detail>
in place of renewed by the automatic Weles sign-in while such a failure stands.
A refusal that says every credential was rejected by the provider ends with
automatic sign-in: <member>: <state>; …, one state per recorded member of the
provider: retired, never ran, last <result> at <UTC instant>, or
failed (<code>: <detail> (attempted at <UTC instant>)) while that failure still stands. A failure
stops standing once the member's credential is renewed after the attempt, including an attempt refused before
any run started. A member inside a recorded block carries
, held until <UTC instant> by the provider's answer: <the provider's refusal>, so a member that
only waits for its spent window is not read as one that needs a sign-in.
The operator envelope¶
src/core/failure.rs is the one place Brama's own vocabulary becomes the
fleet's, using the wisent-errors crate for everything derivable and
deciding nothing itself. Every envelope carries service brama, one failure
point, an impact, and the reason the layer below gave, verbatim — the field
shape and a captured example are in concepts/envelope
and concepts/failure-point:
| Failure point | Raised when |
|---|---|
brama.dispatch.model-selection |
choosing a model for a request |
brama.dispatch.credential-selection |
choosing which credential pays |
brama.dispatch.bounded-rotation |
every bounded credential has been tried |
brama.dispatch.credential-block |
a rate-limited answer put a credential in a block |
brama.providers.provider-call |
a provider API call answered with a failure |
brama.gateway.oauth-refresh |
the provider refused an OAuth refresh |
brama.gateway.oauth-refresh.stored-document |
the stored credential cannot be refreshed by anybody (not an object, or no refresh token); only a sign-in replaces it |
brama.gateway.credential-persist |
the vault would not store a refreshed grant |
brama.gateway.credential-redeem |
a capability yielded no usable credential |
brama.core.model-request |
one routed model request, at the HTTP edge |
Derivation is centralised: severity, retryability, and outage always come from the crate, never from a call site. Where an upstream HTTP status exists, the fleet code is classified from that status by the crate's catalogue; otherwise Brama's own kind is translated exactly once:
| Brama kind | Fleet code |
|---|---|
provider_authentication, unauthenticated, credential_unauthorized, subscription_reauthorization_required |
Auth |
provider_rate_limited, subscription_unavailable |
RateLimit |
dependency_unavailable |
InfraDown |
provider_quota_exhausted |
Config — a spent quota is not a busy provider; no wait repairs it, and the account it names is ours |
invalid_request |
NotFound — the route, selector, or evidence asked for does not exist |
context_length_exceeded |
Refused — the prompt is longer than the model takes (the provider answered 413, or its error object's code or type is context_length_exceeded); HTTP 400, retryable: false. A client promotes the route or compacts; no wait or other credential repairs it |
provider_failure, internal_error |
Unknown |
Every arm preserves what the client contract already said about retrying:
retryable kinds map to retryable codes and permanent ones do not. Ordinary HTTP
admission errors retain their existing body. Subscription reports expose their
Wisent envelopes in errors so partial failures are visible without
reading server logs; the same reasons remain in the usage ledger.
Where errors are not allowed¶
Logs and error bodies never include bearer values, HMAC signatures,
capability IDs, raw credentials, request bodies, prompt text, raw provider
payloads, or donated secrets. /readyz reports a refusal in the words of
whatever refused it — provider name and verdict only, no secret in the body
(http-api). The full state and recovery contract behind each
code is CORE.md.