OpenWOP openwop.dev

Status: Stable · v2.1.0 (2026-09-11) · RFC 0171 §B.

Why this exists

Every error a v2 host returns is a row in one registry. A client routes on error, never on message, and a code that is not registered is not a protocol error. The registry is the single source for the envelope schema, the HTTP status, and retriability.

The registry

spec/v2/errors.json holds one row per code: { code, httpStatus, retriable, details, since, deprecated? } plus the provenance fields statusSource and source. It registers 96 codes. schemas/v2/error-envelope.schema.json is GENERATED from it and MUST NOT be edited by hand.

A host MUST return a registered code, or a vendor code, in every error response. A vendor code MUST match ^(?!openwop\.)[a-z][a-z0-9](-[a-z0-9]+)\.[a-z][a-z0-9_]*$ with its first segment an org registered in spec/v2/declaration.json; openwop. is reserved. The registry grows by the closed-enum rule in overview.md §0: a producer MUST NOT emit an unregistered member, and a consumer MUST accept an unknown registered member and MUST NOT act on it.

The envelope

Every error response body MUST be { error, message, details? } and nothing else (additionalProperties: false). error is the registered code or a vendor code; message is a non-empty string; details is an object whose shape is the row's details schema. A row whose details is null accepts any object; when a row registers a schema, the generated envelope becomes a oneOf discriminated on error. Contextual data (conflict refs, trace ids, validation paths) MUST live under details, never at a new top level. The same envelope is the per-id error of bulkCancelRuns (runs.md). When present, details.correlationId MUST be a non-empty string.

x-openwop-http-status and x-openwop-retriable in the generated schema mirror the registry; a host MUST answer with the registered status.

Retry timing

Retry timing lives in the Retry-After header only. details.retryAfter, details.retryAfterMs and details.retryAfterSeconds are not part of v2 and a host MUST NOT emit them. A 429 rate_limited response MUST set Retry-After. The retriable rows are residency_unavailable, rate_limited, internal_error, pack_registry_unreachable, runner_unavailable.

One code per state

An interrupt has one code per state: a token or run-scoped resolve against an interrupt that is already resolved, or whose run is cancelled or completed, MUST return 409 interrupt_already_resolved; a signed token past its expiresAt MUST return 410 interrupt_expired; a token whose alg or kid the host does not accept MUST return 401 interrupt_token_invalid (see interrupt.md, identity.md). The idempotency mismatch code is idempotency_key_mismatch only (idempotency.md).

Codes by HTTP status

Generated from spec/v2/errors.json (97 codes; retriable and statusSource are in the registry).

CodeStatus
connection_provider_unresolved400
connector_action_unresolved400
credential_scope_unsupported400
delegation_chain_cyclic400
delegation_chain_too_long400
idempotency_key_invalid400
interop_version_unsupported400
oauth_provider_unsupported400
oauth_scope_unsupported400
pack_dependency_cycle400
pack_engine_unsupported400
pack_integrity_failure400
pack_kind_invalid400
pack_lockfile_incomplete400
pack_peer_dependency_missing400
pack_peer_dependency_undefined400
pack_signature_invalid400
pack_validation_failed400
protocol_version_mismatch400
schedule_horizon_exceeded400
sub_chain_cycle400
sub_chain_depth_exceeded400
unsupported_stream_mode400
until_in_past400
validation_error400
webhook_url_rejected400
audience_mismatch401
connector_auth_expired401
credential_revoked401
delegation_expired401
identity_unresolvable401
identity_unverified401
interrupt_token_invalid401
key_revoked401
sender_constraint_missing401
unauthenticated401
credential_forbidden403
delegation_scope_amplified403
forbidden403
force_engine_version_forbidden403
id_tenant_mismatch403
mock_provider_forbidden403
pack_namespace_unauthorized403
run_forbidden403
sandbox_capability_denied403
sandbox_escape_attempt403
workspace_membership_required403
credential_not_found404
interrupt_not_found404
not_found404
pack_version_not_found404
replay_source_missing404
signature_not_available404
protocol_version_unsupported406
connection_provider_conflict409
envelope_correlation_conflict409
idempotency_in_flight409
idempotency_key_mismatch409
interrupt_already_resolved409
pack_dependency_conflict409
pack_integrity_mismatch409
replay_diverged_at_refusal409
replay_memory_snapshot_unavailable409
run_already_active409
run_terminal409
run_state_conflict409
version_conflict409
workspace_conflict409
interrupt_cancelled410
interrupt_expired410
workspace_too_large413
payload_too_large413
unsupported_media_type415
capability_not_provided422
capability_required422
credential_required422
envelope_invalid422
envelope_refusal422
fork_point_invalid422
envelope_truncation_unrecoverable422
loop_limit_exceeded422
mcp_mrtr_rounds_exceeded422
pack_runtime_requirement_unmet422
recursion_limit_exceeded422
residency_unavailable422
run_timeout422
sandbox_memory_exceeded422
sandbox_timeout422
token_budget_exceeded422
client_version_unsupported426
rate_limited429
event_type_unmapped500
internal_error500
pack_load_failure500
credential_unavailable501
pack_registry_unreachable503
runner_unavailable503