Error format
Typed Z4JError failures and unexpected server failures use the small z4j
envelope below. The brain does not emit RFC 7807 Problem Details. FastAPI owns
some responses before z4j's middleware can normalize them: request-schema
validation, unknown routes, and a directly raised HTTPException retain
FastAPI's {"detail": ...} shape.
{ "error": "validation_failed", "message": "request body failed validation (2 field(s))", "request_id": "01H...", "details": { "errors": [ {"loc": "body.email", "type": "value_error.email"}, {"loc": "body.role", "type": "enum"} ] }}| Field | Meaning |
|---|---|
error |
Stable short identifier. Branch on this from API clients. |
message |
Human-readable summary. Operator-friendly English; do not parse. |
request_id |
The same value as the X-Request-Id response header. Quote it when filing a bug. |
details |
Per-error context. A Pydantic ValidationError raised inside application code is redacted to {"errors": [{"loc": "...", "type": "..."}, ...]}. FastAPI request validation uses its own detail list and may echo the rejected input; do not assume that path is secret-redacted. |
HTTP status mapping
Section titled “HTTP status mapping”The brain raises typed Z4JError subclasses; the error middleware maps them to HTTP status codes:
| Status | Exception class | Typical error codes |
|---|---|---|
| 401 | AuthenticationError, SignatureError |
unauthenticated, invalid_signature |
| 403 | AuthorizationError |
forbidden, plus mfa_reverify_required and mfa_enrollment_required for MFA gates; ip_denied when a source-address allowlist refuses the request (details.surface names the surface, the body is otherwise the same); project_inactive when a long-poll agent of an archived project calls |
| 404 | NotFoundError |
not_found |
| 409 | ConflictError |
conflict; the dead-letter listing instead answers 409 as a FastAPI detail object when no online agent advertises the engine |
| 422 | ValidationError, InvalidFrameError |
validation_error, invalid_frame; application-raised Pydantic validation uses validation_failed, while FastAPI request validation uses detail |
| 426 | ProtocolError |
protocol_incompatible or protocol_version_mismatch; mapped but not currently emitted by an HTTP route |
| 429 | RateLimitExceeded |
rate_limited for the MFA account lockout and the setup attempt budget; the per-IP throttles instead return FastAPI's {"detail": "too many requests; retry in N seconds (bucket)"} with no error field |
| 502 | AdapterError |
adapter_error; mapped but normal adapter failures arrive asynchronously as a failed command result. The dead-letter listing, which waits for its command, answers 502 as a FastAPI detail object when the agent refuses the listing or returns something that is not a page |
| 503 | AgentOfflineError |
agent_offline; local-registry command dispatch can emit this, while PostgreSQL coordination can instead leave a command pending until its deadline; schedule trigger reports no online agent as 404 |
| 504 | CommandTimeoutError |
command_timeout; mapped but no HTTP endpoint raises it -- the sweeper marks the command timed out for later polling. The dead-letter listing answers 504 as a FastAPI detail object naming the command when the agent does not answer within its wait bound |
| 500 | ConfigError, RedactionConfigError, and any untyped exception |
config_error, redaction_config_error; an untyped exception gets internal_error (with request_id for log correlation; the exception class name is deliberately not surfaced) |
Tracing
Section titled “Tracing”Responses that pass through the request-id middleware include an
X-Request-Id header, and z4j error envelopes repeat it as request_id. An
early body-size rejection is an exception: it can return 413 before that
middleware runs, with no header and a null body field. Its body is
{"error": "payload_too_large", "message": "request body exceeds the configured maximum", "request_id": null, "details": {}},
where details may instead carry a short reason. FastAPI detail
responses do not add a request_id body field. Pass the header when it is
present when filing a bug or scanning logs.
Audit trail for denials
Section titled “Audit trail for denials”Authentication, authorization, not-found, and validation failures on
mutating schedule routes (POST, PUT, PATCH, DELETE) are offered to a
bounded best-effort audit queue so IDOR-enumeration attempts can leave a
forensic breadcrumb. Reads are not covered, nor are other API families. Queue
overflow or a persistence failure can drop the breadcrumb without changing the
client's error response. Rows that are written participate in the HMAC chain;
structured log lines alone are not HMAC-chained.