Separate protocol authority from mutable API contracts
Move API artifacts out of docs/spec, define concrete execution and deletion refinements, and document TLA+ abstraction boundaries so architecture prose cannot become a competing protocol specification.
This commit is contained in:
parent
78be27871c
commit
dfd60cf104
1 changed files with 316 additions and 73 deletions
117
docs/api/README.md
Normal file
117
docs/api/README.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# Remora API implementation contract
|
||||
|
||||
These files are mutable implementation artifacts. They describe the current
|
||||
HTTP operations and WebSocket frames but are not authoritative over
|
||||
[`Remora.tla`](../protocol/Remora.tla) or
|
||||
[`architecture.md`](../spec/architecture.md). Conflicts are corrected here.
|
||||
|
||||
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, or payload
|
||||
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.
|
||||
|
||||
Renewal follows `RenewLease`: it may extend an existing live coordination lease
|
||||
after durable authority changes. It never creates or restores a claim. Progress
|
||||
and outcome operations require current fenced authority and return
|
||||
`precondition_failed` for stale or non-authoritative tuples.
|
||||
|
||||
A successful claim returns the attempt authority and the attempt's
|
||||
`ExecutionEnvelope`. The worker verifies that their job, attempt, fence, and
|
||||
worker incarnation agree before source or target I/O. Opaque resolved-source
|
||||
and checkpoint references identify durable streamable state; they are not
|
||||
reconstructed from coordination data.
|
||||
|
||||
[`RemoraApiMutation`](../protocol/RemoraApiMutation.tla) models this durable
|
||||
operation ledger around an abstract successful `JobProtocol` effect. It does
|
||||
not redefine job phases or attempt-authority transitions. Its `Rejected`
|
||||
response abstracts the endpoint-specific `idempotency_conflict` and
|
||||
`precondition_failed` codes enforced by the executable contract.
|
||||
|
||||
## 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.
|
||||
|
||||
A deletion whose exact remaining work cannot progress because target access or
|
||||
configuration is unavailable enters `blocked` with a redacted problem. It
|
||||
retains the deletion obligation and can return to active processing when the
|
||||
dependency is restored. Owner access remains authorized through the concrete
|
||||
target retained in the tombstone.
|
||||
|
||||
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; there is no
|
||||
subscription frame. Server frames are ordered by increasing `sequence` for a
|
||||
connection and carry an opaque next cursor. Delivery is at least once, and
|
||||
clients deduplicate event IDs.
|
||||
|
||||
`job_changed` is an invalidation with an attempt-aware display hint, not a
|
||||
resource replacement or patch. A client compares `job_revision` with the last
|
||||
HTTP snapshot revision and fetches a new authorized snapshot when it is newer.
|
||||
It advances its applied revision only from that snapshot. Matching progress may
|
||||
be displayed provisionally while the fetch is pending.
|
||||
|
||||
`deletion_changed` is likewise an invalidation. Its state is a display hint;
|
||||
the deletion resource remains the authoritative HTTP projection.
|
||||
|
||||
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.
|
||||
Loading…
Reference in a new issue