remora/docs/spec/api/README.md

4.4 KiB

Remora API contract semantics

OpenAPI 3.2.0 describes HTTP operations and JSON Schema Draft 2020-12 describes WebSocket frames. The API contract version is 1.0.0; additive fields require clients to tolerate unknown response fields only after a contract version that permits them, while incompatible changes use a new major path or media type.

Idempotency

Every public mutation requires Idempotency-Key. The identity is caller subject, operation, and key. The server canonicalizes the semantic request, including the authorization target, with RFC 8785 and stores its SHA-256 fingerprint and complete response atomically with the mutation. The default retention is 24 hours and is never shorter than the advertised Idempotency-Expires-At.

The same identity and fingerprint replays the original status, resource, and body after duplicate or ambiguous delivery. A different fingerprint returns idempotency_conflict and cannot affect either request. Naturally idempotent deletion and cancellation semantics still use this request identity.

Internal claim uses job ID plus operation_id because no attempt exists yet. Renew, progress, and outcome use attempt ID, fencing token, worker incarnation, and operation_id. Each operation identity retains a request fingerprint. An exact replay has the original effect; a changed worker, fence, payload, stale, or non-authoritative tuple returns precondition_failed without mutation. First observation is accepted only when the corresponding JobProtocol action is enabled. Later eligibility changes do not alter a stored response, while a fresh operation is evaluated against current eligibility.

RemoraApiMutation models this durable operation ledger around an abstract successful JobProtocol effect. It does not redefine job phases or attempt-authority transitions.

Authorization and errors

Public operations apply the authorization rules in the architecture. List is filtered before pagination. A missing and an unauthorized resource both return not_found; callers cannot probe job existence. Cursors and event replay are bound to the authenticated subject and filter.

ApiError fields are an allowlist. Messages and field diagnostics never carry credential values, bearer material, URL userinfo, raw upstream response bodies, internal stack traces, or unredacted configured endpoints.

Jobs, pages, and results

Job.phase uses the exact JobProtocol phase vocabulary; this contract does not define phase transitions. lifecycle, current authoritative attempt, current matching progress, reusable-checkpoint projection, result, and failure are separate fields. Progress is present only when its attempt ID and fence match the authoritative attempt in the same projection.

Job pages order by (created_at DESC, id DESC). The first request fixes a membership snapshot, authorization/filter fingerprint, and high-water mark. next_cursor is an opaque integrity-protected keyset cursor for that snapshot; new jobs do not enter later pages. Invalid, altered, cross-subject, or cross-filter cursors return the corresponding structured cursor error.

A result endpoint returns only a visible CommittedResult. Tombstoned, unauthorized, absent, and never-committed results are not exposed. The hard delete endpoint returns the durable deletion resource established by the architecture.

Worker failure outcomes accept only attempt-reportable runtime classes and do not accept a caller-supplied retryable flag. The service derives the durable disposition from the class mapping in the architecture. Validation, cancellation, and lost-attempt projection use their dedicated boundaries.

WebSocket replay

An HTTP job or list snapshot carries event_cursor at its consistency watermark. The client connects to /v1/events with that cursor. Server frames are ordered by increasing sequence for a connection and carry an opaque next cursor. Delivery is at least once; clients deduplicate event IDs and ignore a job revision no newer than the applied revision.

Replay applies current authorization before delivery. If a cursor is absent, invalid, expired, authorization-incompatible, or no longer available after coordination loss, the server sends resync_required; the client obtains a new HTTP snapshot and reconnects from its cursor. Progress frames for a stale attempt/fence are suppressed before emission and ignored defensively by the client.