Skip to content

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.

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)

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.

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.