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
| Code | Meaning | Common cause |
|---|---|---|
| 200 | OK | Success (also used for DuplicateInvocation — an idempotent replay is not an error) |
| 201 | Created | Resource created |
| 204 | No Content | Deleted / accepted with no body |
| 400 | Bad Request | Malformed JSON, ValidationError |
| 401 | Unauthorized | Invalid or missing API key/JWT (auth-middleware shape) |
| 403 | Forbidden | Insufficient role (auth-middleware shape), or PermissionDenied/TrustPolicyViolation (CoreError shape) |
| 404 | Not Found | NotFound — agent, workflow, competency, or resource doesn't exist |
| 429 | Too Many Requests | Auth rate limiting (brute-force protection), or RateLimited |
| 502 | Bad Gateway | ProviderError — LLM provider unreachable or errored |
| 503 | Service Unavailable | MemoryViolation |
| 500 | Internal Server Error | Every 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:
ErrorCategory | Status |
|---|---|
NotFound | 404 |
ValidationError | 400 |
PermissionDenied | 403 |
TrustPolicyViolation | 403 |
MemoryViolation | 503 |
ProviderError | 502 |
RateLimited | 429 |
DuplicateInvocation | 200 |
| everything else | 500 |
Full ErrorCategory enum
hoziron_core::ErrorCategory — every application-level error carries
exactly one of these:
| Variant | Meaning |
|---|---|
NotFound | Entity not found (agent, competency, skill, etc.) |
InvalidState | State-machine violation (invalid lifecycle transition) |
MissingDependency | Skill not installed, competency not available |
PermissionDenied | Capability check failed |
ValidationError | Malformed config, missing/invalid fields |
Duplicate | Entity already exists (competency already installed, etc.) |
SizeLimitExceeded | Message too long, manifest too large |
Timeout | Execution loop, workflow step timeout |
ProviderError | LLM provider error — model unavailable, rate limit, unreachable |
ToolError | Tool execution error |
PiiError | PII processing error |
MemoryViolation | Cross-agent memory access attempted, or SQLite connectivity issue |
RateLimited | Per-agent invocation limit exceeded |
DuplicateInvocation | Idempotency key already processed — cached result returned, not an error |
TrustPolicyViolation | Agent attempted a tool outside its [trust] allowed_tools boundary |
AuthenticationError | Invalid credentials, expired token (currently only raised from the OIDC validation path — most 401s go through the auth-middleware shape instead, not this category) |
AuthorizationError | Valid credentials but no role mapping (OIDC path only — most 403s go through the auth-middleware shape instead) |
InitError | Platform initialization / config-load error (bad TOML, legacy config sections, licence/boot failures) |
Internal | Unmapped 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 variant | Meaning |
|---|---|
Config | File I/O, parse failure, missing fields in local CLI config |
Network | HTTP 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) |
Internal | Unexpected state, I/O, serialization |
InvalidInput | Bad arguments, missing required values |
ShuttingDown | Server is shutting down |
AgentNotFound | Agent not found |
LlmDriver | LLM driver error |
Exit codes: 0 success, 1 any CliError, 2 argument-parsing error
(clap), 130 SIGINT (Ctrl+C).
Related: