Error Reference

Error categories, HTTP status codes, JSON error envelope shapes, and how errors surface through the hoziron-cli CLI.

Two JSON error envelope shapes

The API returns errors in two different shapes depending on which layer rejected the request. Both are JSON with a top-level "error" key, but the value differs — check the type of error before parsing it as an object.

1. Application/business-logic errors (routing, agents, workflows, catalog, config, memory, PII — everything that goes through hoziron_core::CoreError):

{
  "error": {
    "category": "ValidationError",
    "message": "server.listen '...' is not a valid socket address",
    "details": { "field": "server.listen" }
  }
}

category is the Rust ErrorCategory variant name (see table below). details is a free-form string map with context (field names, entity IDs) — always present, may be empty. Built by error_response() in crates/surfaces/hoziron-api/src/routes.rs.

2. Auth-middleware / rate-limit errors (401, 403, 429 raised by the shared HTTP middleware before a request reaches application code — these never construct a CoreError at all):

{ "error": "forbidden", "message": "Role 'viewer' cannot perform 'agent:create'" }

Here error is a short string code ("unauthorized", "forbidden", "too_many_requests", "internal_error"), not an object — there is no error.category. too_many_requests also carries a retry_after_secs field. Built by crates/infra/hoziron-http/src/responses.rs, shared across the API, registry, and every surface using the common auth middleware.

Practical implication: don't write a client that unconditionally reads error.category/error.message as if the shape is uniform — for shape 2, error is a plain string and the human-readable text is at the sibling key message, not nested under error. (The bundled hoziron-cli CLI's HTTP client currently reads body["error"] ["message"] uniformly; for shape 2 responses this indexes into a JSON string, evaluates to null, and the CLI prints a generic "Unknown error" instead of the real message — a known quirk of the current client, not a documented API contract you should rely on.)

HTTP Status Codes

CodeMeaningCommon cause
200OKSuccess (also used for DuplicateInvocation — an idempotent replay is not an error)
201CreatedResource created
204No ContentDeleted / accepted with no body
400Bad RequestMalformed JSON, ValidationError
401UnauthorizedInvalid or missing API key/JWT (auth-middleware shape)
403ForbiddenInsufficient role (auth-middleware shape), or PermissionDenied/TrustPolicyViolation (CoreError shape)
404Not FoundNotFound — agent, workflow, competency, or resource doesn't exist
429Too Many RequestsAuth rate limiting (brute-force protection), or RateLimited
502Bad GatewayProviderError — LLM provider unreachable or errored
503Service UnavailableMemoryViolation
500Internal Server ErrorEvery other ErrorCategory not explicitly mapped (see below)

error_response() category → status mapping

Only these ErrorCategory variants get a specific status code; every other variant falls through to 500 Internal Server Error:

ErrorCategoryStatus
NotFound404
ValidationError400
PermissionDenied403
TrustPolicyViolation403
MemoryViolation503
ProviderError502
RateLimited429
DuplicateInvocation200
everything else500

Full ErrorCategory enum

hoziron_core::ErrorCategory — every application-level error carries exactly one of these:

VariantMeaning
NotFoundEntity not found (agent, competency, skill, etc.)
InvalidStateState-machine violation (invalid lifecycle transition)
MissingDependencySkill not installed, competency not available
PermissionDeniedCapability check failed
ValidationErrorMalformed config, missing/invalid fields
DuplicateEntity already exists (competency already installed, etc.)
SizeLimitExceededMessage too long, manifest too large
TimeoutExecution loop, workflow step timeout
ProviderErrorLLM provider error — model unavailable, rate limit, unreachable
ToolErrorTool execution error
PiiErrorPII processing error
MemoryViolationCross-agent memory access attempted, or SQLite connectivity issue
RateLimitedPer-agent invocation limit exceeded
DuplicateInvocationIdempotency key already processed — cached result returned, not an error
TrustPolicyViolationAgent attempted a tool outside its [trust] allowed_tools boundary
AuthenticationErrorInvalid credentials, expired token (currently only raised from the OIDC validation path — most 401s go through the auth-middleware shape instead, not this category)
AuthorizationErrorValid credentials but no role mapping (OIDC path only — most 403s go through the auth-middleware shape instead)
InitErrorPlatform initialization / config-load error (bad TOML, legacy config sections, licence/boot failures)
InternalUnmapped internal error, wrapped generically

There is no TriggerMismatch variant — agents do not declare accepted invocation sources; see invocation-model.md.

CLI error surfacing

The hoziron-cli CLI has its own error enum (bin/hoziron-cli/src/error.rs), deliberately independent of hoziron_core/kernel error types (kernel errors must never leak past the core boundary — see the Kernel Isolation Rule):

CliError variantMeaning
ConfigFile I/O, parse failure, missing fields in local CLI config
NetworkHTTP request failed, timeout, DNS — also wraps every non-2xx API response, with the message extracted from the response body (see the envelope-shape caveat above)
InternalUnexpected state, I/O, serialization
InvalidInputBad arguments, missing required values
ShuttingDownServer is shutting down
AgentNotFoundAgent not found
LlmDriverLLM driver error

Exit codes: 0 success, 1 any CliError, 2 argument-parsing error (clap), 130 SIGINT (Ctrl+C).


Related: