BBramaDocs
Browse documentation

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.