Errors
Unified Harness Protocol, version 2026-10-19
A client's error handling is only as good as the server's error reporting. This chapter defines one envelope, a closed set of codes, and which failures are worth retrying.
1. The error envelope
Every non-2xx response MUST have this body:
{
"error": {
"type": "invalid_request_error",
"code": "harness_not_found",
"message": "No harness with id 'chrn_deadbeef'.",
"param": "metadata.harness_id",
"detail": null
}
}
| Field | Type | Required | Meaning |
|---|---|---|---|
type |
string | yes | Broad class; see §2 |
code |
string | yes | Specific, machine-readable reason; see §3 |
message |
string | yes | Human-readable, one sentence, no stack trace |
param |
string | null | yes | Dotted path to the offending field, when there is one |
detail |
object | null | yes | Structured extra context, or null |
Rules:
messageMUST be safe to show a user. It MUST NOT contain credentials, internal hostnames, file paths, or stack traces.codeMUST come from §3 where one applies. A server MAY define additional codes for conditions this specification does not cover, and MUST namespace them with a vendor prefix (acme_…) so a future version of this specification cannot collide with them.- A server MUST NOT return
200with an error inside. The failure of a task is reported as a response withstatus: "failed"and HTTP200, because the request succeeded; the failure of a request is a non-2xx with this envelope. Those are different events and MUST NOT be conflated.
Compatibility note. A server MAY additionally include a top-level
detailstring alongsideerror, for clients written against implementations that predate this envelope. That field is deprecated, carries no information not inerror.message, and will be removed in a future version.
2. Error types
type |
HTTP | Meaning |
|---|---|---|
invalid_request_error |
400, 404, 409, 413, 422 | The request was wrong. Retrying it unchanged will fail again. |
authentication_error |
401 | The credential is missing, malformed or unknown. |
permission_error |
403 | Authenticated, but not allowed to do this. |
rate_limit_error |
429 | Too many requests, or a quota is exhausted. |
harness_error |
200 (in a failed response) | The harness ran and failed. |
server_error |
500, 502, 503, 504 | The server failed. Retrying MAY succeed. |
3. Error codes
3.1 Request and routing
| Code | HTTP | Meaning |
|---|---|---|
unsupported_protocol_version |
400 | UHP-Version names a version this server cannot serve. detail.supported lists what it can. |
invalid_input |
400 | The body could not be parsed, or a field has the wrong type. |
harness_not_found |
404 | No such harness in the caller's scope. |
response_not_found |
404 | No such response in the caller's scope. |
session_not_found |
404 | No such session in the caller's scope. |
plugin_not_found |
404 | The harness has no plugin of that name. |
file_not_found |
404 | No such file in the caller's scope. |
session_expired |
404 | The session existed but is past its retention. |
harness_mismatch |
409 | The task named a different harness than the session it continues. |
session_busy |
409 | A task is already running in this session. |
harness_busy |
409 | A harness that retires its sessions with it has a task in progress; it is not retired mid-task (Harnesses §5.3). |
plugin_conflict |
409 | Two plugins share a name, or a component name collides across the harness and its plugins. detail.component, detail.name and detail.between say which. |
file_too_large |
413 | The upload exceeds the server's limit. detail.max_bytes states it. |
model_unavailable |
422 | The requested model cannot be served for this harness. |
model_not_available |
422 | A harness create or update named a default_model its base does not run (Harnesses §5.1). |
unsupported_base |
422 | The requested harness base is not supported by this server. |
environment_not_found |
404 | No such environment in the caller's scope, on a read, a harness write or a task (Environments §8). |
environment_not_ready |
409 | A task on an environment with no active version, or activating an unfinished build. |
environment_busy |
409 | A build is running: a second build or a delete must wait. |
environment_exists |
409 | The name's slug is already another environment's mount path. |
environment_invalid |
422 | A name that is no path segment, a file path that escapes the root, or an import that is neither archive nor repository. |
environment_unavailable |
502 | The server's build or storage backend did not answer. |
memory_not_found |
404 | No such memory, or one the caller holds no privilege on: the two are one answer (Memories §12). |
memory_forbidden |
403 | The caller sees the memory and lacks the privilege the operation needs. |
memory_record_not_found |
404 | No such record in this memory. |
memories_not_attached |
404 | The agent holds no memory: nothing was granted to it. |
memory_invalid |
422 | A parent that would make a cycle, two default entries, a record with no content, a query whose parameters do not match. |
memory_unsupported |
422 | An operation the memory's provider does not implement at all. A partial answer is reported in degraded, not as this. |
memory_busy |
409 | A move or a delete while a consolidation runs. |
memory_unavailable |
502 or 503 | The memory's provider did not answer, or is not connected. |
plugin_invalid |
422 | The package is not a valid Agent Plugins package: no manifest, a fatal manifest error, a path that escapes the root, or a name that contradicts the manifest. detail.path and detail.reason say where. |
unsupported_plugin_schema |
422 | The manifest's $schema names an Agent Plugins version this server does not install. detail.supported lists what it does. |
unsupported_transport |
422 | A plugin declares an MCP server whose transport cannot be run for this harness base. detail.transport and detail.base say which. |
model_not_available and model_unavailable are refused at different moments and are not
interchangeable. model_not_available refuses a request that writes a harness (a create or update
whose default_model its base does not run), before any task exists; model_unavailable refuses a
task whose model cannot be served for its harness, when the task is sent. The first tells the client
to fix the harness, the second to send the task with another model.
3.2 Authentication and limits
| Code | HTTP | Meaning |
|---|---|---|
missing_credential |
401 | No bearer token. |
invalid_credential |
401 | The token is malformed, unknown, or revoked. |
insufficient_scope |
403 | The credential cannot perform this operation. |
rate_limited |
429 | Slow down. Retry-After SHOULD be set. |
quota_exhausted |
429 | A budget is spent; retrying now will not help. |
3.3 Execution
| Code | Where | Meaning |
|---|---|---|
harness_error |
failed response | The harness ran and could not complete the work. |
harness_unavailable |
503 | No capacity to run this harness right now. |
provider_error |
failed response | The upstream model provider refused or failed. |
timeout |
failed response | The task exceeded its wall-clock budget. |
cancelled |
cancelled response | The client cancelled it. |
preview_unavailable |
501 | This server does not render previews. |
preview_failed |
502 | Conversion of this file failed. |
4. Retrying
| Situation | Retry? |
|---|---|
server_error (500, 502, 503, 504) |
Yes, with exponential backoff and jitter |
rate_limited |
Yes, after Retry-After |
quota_exhausted |
No — nothing will change until the quota does |
session_busy |
Yes, once the in-flight task is terminal |
Any invalid_request_error |
No — fix the request |
authentication_error |
No — fix the credential |
A task that reached failed |
Only with the client's own judgment; it consumed real resources |
Retries of POST /v1/responses MUST carry an Idempotency-Key. Without one, a retry after a
timeout runs the task a second time — and the first may still be running, editing the same files.
This is the single most damaging mistake a UHP client can make; see Tasks §6.
5. Timeouts
A client SHOULD set generous timeouts. Agent tasks routinely run for minutes; a 30-second HTTP timeout will cancel healthy work.
- Streaming: no total timeout. Use an inactivity timeout instead — a server SHOULD emit a comment
line (
: keep-alive) at least every 30 seconds so the client can distinguish a working agent from a dead connection. - Non-streaming: at least the task's
timeout_seconds, plus margin. - A client that gives up MUST NOT assume the task stopped. It has not. Cancel it explicitly.