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:
ms2k 2026-07-14 11:50:10 +09:00
commit dfd60cf104
3 changed files with 316 additions and 73 deletions

117
docs/api/README.md Normal file
View 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.

138
docs/api/events.schema.json Normal file
View file

@ -0,0 +1,138 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://remora.invalid/api/events.schema.json",
"title": "Remora WebSocket frame",
"oneOf": [
{"$ref": "#/$defs/JobChanged"},
{"$ref": "#/$defs/DeletionChanged"},
{"$ref": "#/$defs/ResyncRequired"},
{"$ref": "#/$defs/Heartbeat"}
],
"$defs": {
"OpaqueId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9_-]{7,127}$"
},
"Cursor": {
"type": "string",
"minLength": 16,
"maxLength": 2048
},
"Attempt": {
"type": "object",
"additionalProperties": false,
"required": ["attempt_id", "fence"],
"properties": {
"attempt_id": {"$ref": "#/$defs/OpaqueId"},
"fence": {"type": "integer", "minimum": 1}
}
},
"Progress": {
"type": "object",
"additionalProperties": false,
"required": ["attempt_id", "fence", "completed_bytes", "completed_members"],
"properties": {
"attempt_id": {"$ref": "#/$defs/OpaqueId"},
"fence": {"type": "integer", "minimum": 1},
"completed_bytes": {"type": "integer", "minimum": 0},
"total_bytes": {"type": ["integer", "null"], "minimum": 0},
"completed_members": {"type": "integer", "minimum": 0},
"total_members": {"type": ["integer", "null"], "minimum": 0}
}
},
"ServerBase": {
"type": "object",
"required": ["schema_version", "kind", "event_id", "sequence", "cursor", "occurred_at"],
"properties": {
"schema_version": {"const": 1},
"kind": {"type": "string"},
"event_id": {"$ref": "#/$defs/OpaqueId"},
"sequence": {"type": "integer", "minimum": 1},
"cursor": {"$ref": "#/$defs/Cursor"},
"occurred_at": {"type": "string", "format": "date-time"}
}
},
"JobChanged": {
"allOf": [
{"$ref": "#/$defs/ServerBase"},
{
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "kind", "event_id", "sequence", "cursor", "occurred_at", "job_id", "job_revision", "phase", "lifecycle", "authoritative_attempt", "progress"],
"properties": {
"schema_version": {"const": 1},
"kind": {"const": "job_changed"},
"event_id": {"$ref": "#/$defs/OpaqueId"},
"sequence": {"type": "integer", "minimum": 1},
"cursor": {"$ref": "#/$defs/Cursor"},
"occurred_at": {"type": "string", "format": "date-time"},
"job_id": {"$ref": "#/$defs/OpaqueId"},
"job_revision": {"type": "integer", "minimum": 1},
"phase": {"enum": ["Queued", "Running", "RetryWait", "Cancelling", "Succeeded", "Failed", "Cancelled"]},
"lifecycle": {"enum": ["visible", "soft_deleted", "hard_deleting", "hard_deleted"]},
"authoritative_attempt": {"oneOf": [{"$ref": "#/$defs/Attempt"}, {"type": "null"}]},
"progress": {"oneOf": [{"$ref": "#/$defs/Progress"}, {"type": "null"}]}
}
}
]
},
"DeletionChanged": {
"allOf": [
{"$ref": "#/$defs/ServerBase"},
{
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "kind", "event_id", "sequence", "cursor", "occurred_at", "job_id", "operation_id", "state"],
"properties": {
"schema_version": {"const": 1},
"kind": {"const": "deletion_changed"},
"event_id": {"$ref": "#/$defs/OpaqueId"},
"sequence": {"type": "integer", "minimum": 1},
"cursor": {"$ref": "#/$defs/Cursor"},
"occurred_at": {"type": "string", "format": "date-time"},
"job_id": {"$ref": "#/$defs/OpaqueId"},
"operation_id": {"$ref": "#/$defs/OpaqueId"},
"state": {"enum": ["queued", "running", "retry_wait", "blocked", "completed"]}
}
}
]
},
"ResyncRequired": {
"allOf": [
{"$ref": "#/$defs/ServerBase"},
{
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "kind", "event_id", "sequence", "cursor", "occurred_at", "reason"],
"properties": {
"schema_version": {"const": 1},
"kind": {"const": "resync_required"},
"event_id": {"$ref": "#/$defs/OpaqueId"},
"sequence": {"type": "integer", "minimum": 1},
"cursor": {"$ref": "#/$defs/Cursor"},
"occurred_at": {"type": "string", "format": "date-time"},
"reason": {"enum": ["cursor_absent", "cursor_invalid", "cursor_expired", "authorization_changed", "history_unavailable"]}
}
}
]
},
"Heartbeat": {
"allOf": [
{"$ref": "#/$defs/ServerBase"},
{
"type": "object",
"additionalProperties": false,
"required": ["schema_version", "kind", "event_id", "sequence", "cursor", "occurred_at"],
"properties": {
"schema_version": {"const": 1},
"kind": {"const": "heartbeat"},
"event_id": {"$ref": "#/$defs/OpaqueId"},
"sequence": {"type": "integer", "minimum": 1},
"cursor": {"$ref": "#/$defs/Cursor"},
"occurred_at": {"type": "string", "format": "date-time"}
}
}
]
}
}
}

964
docs/api/openapi.yaml Normal file
View file

@ -0,0 +1,964 @@
openapi: 3.2.0
$self: https://remora.invalid/api/openapi.yaml
jsonSchemaDialect: https://spec.openapis.org/oas/3.2/dialect/2025-09-17
info:
title: Remora API
version: 1.0.0
description: Mutable implementation contract for public job management and service-authenticated worker operations.
servers:
- url: /api
security:
- oidc: []
paths:
/v1/jobs:
post:
operationId: submitJob
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/JobSubmission'
responses:
'202':
description: Accepted, or the original accepted response for the same key and fingerprint.
headers:
Idempotency-Expires-At:
$ref: '#/components/headers/IdempotencyExpiresAt'
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'409':
$ref: '#/components/responses/IdempotencyConflict'
default:
$ref: '#/components/responses/Error'
get:
operationId: listJobs
parameters:
- $ref: '#/components/parameters/Cursor'
- $ref: '#/components/parameters/Limit'
- name: include_deleted
in: query
schema:
type: boolean
default: false
responses:
'200':
description: Authorized keyset page bound to one snapshot and filter.
content:
application/json:
schema:
$ref: '#/components/schemas/JobPage'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}:
parameters:
- $ref: '#/components/parameters/JobId'
get:
operationId: getJob
responses:
'200':
description: Current authorized job projection.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/result:
parameters:
- $ref: '#/components/parameters/JobId'
get:
operationId: getJobResult
responses:
'200':
description: Visible committed manifest-rooted result.
content:
application/json:
schema:
$ref: '#/components/schemas/ResultDescriptor'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/PreconditionFailed'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/cancel:
parameters:
- $ref: '#/components/parameters/JobId'
post:
operationId: cancelJob
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'202':
description: Cancellation intent accepted or prior response replayed.
headers:
Idempotency-Expires-At:
$ref: '#/components/headers/IdempotencyExpiresAt'
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/soft-delete:
parameters:
- $ref: '#/components/parameters/JobId'
post:
operationId: softDeleteJob
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'200':
description: Job hidden, or prior response replayed.
headers:
Idempotency-Expires-At:
$ref: '#/components/headers/IdempotencyExpiresAt'
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/restore:
parameters:
- $ref: '#/components/parameters/JobId'
post:
operationId: restoreJob
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'200':
description: Soft-deleted job restored, or prior response replayed.
headers:
Idempotency-Expires-At:
$ref: '#/components/headers/IdempotencyExpiresAt'
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/hard-delete:
parameters:
- $ref: '#/components/parameters/JobId'
post:
operationId: hardDeleteJob
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'202':
description: Durable deletion operation created or replayed.
headers:
Idempotency-Expires-At:
$ref: '#/components/headers/IdempotencyExpiresAt'
content:
application/json:
schema:
$ref: '#/components/schemas/DeletionOperation'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/revoke:
parameters:
- $ref: '#/components/parameters/JobId'
post:
operationId: revokeJobExecutionGrant
description: Administrator-only revocation of an accepted execution grant.
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'202':
description: Durable cancellation intent recorded or prior response replayed.
headers:
Idempotency-Expires-At:
$ref: '#/components/headers/IdempotencyExpiresAt'
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
default:
$ref: '#/components/responses/Error'
/v1/jobs/{job_id}/attempts:
parameters:
- $ref: '#/components/parameters/JobId'
get:
operationId: listJobAttempts
parameters:
- $ref: '#/components/parameters/Cursor'
- $ref: '#/components/parameters/Limit'
responses:
'200':
description: Durable attempt history ordered by fencing token.
content:
application/json:
schema:
$ref: '#/components/schemas/AttemptPage'
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/Error'
/v1/deletions/{operation_id}:
parameters:
- name: operation_id
in: path
required: true
schema:
$ref: '#/components/schemas/OpaqueId'
get:
operationId: getDeletionOperation
responses:
'200':
description: Current deletion operation.
content:
application/json:
schema:
$ref: '#/components/schemas/DeletionOperation'
'404':
$ref: '#/components/responses/NotFound'
default:
$ref: '#/components/responses/Error'
/v1/events:
get:
operationId: connectEvents
parameters:
- name: cursor
in: query
description: Opaque authorization-bound replay cursor from an HTTP snapshot or prior frame.
schema:
type: string
minLength: 16
maxLength: 2048
responses:
'101':
description: WebSocket frames conform to events.schema.json. An absent, invalid, expired, or unauthorized cursor yields resync_required.
default:
$ref: '#/components/responses/Error'
/internal/v1/jobs/{job_id}/claim:
parameters:
- $ref: '#/components/parameters/JobId'
post:
operationId: claimJob
security:
- serviceAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ClaimRequest'
responses:
'200':
description: Allocated authority or replay of the same claim operation.
content:
application/json:
schema:
$ref: '#/components/schemas/ClaimResponse'
default:
$ref: '#/components/responses/Error'
/internal/v1/attempts/{attempt_id}/renew:
parameters:
- $ref: '#/components/parameters/AttemptId'
post:
operationId: renewAttempt
security:
- serviceAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AttemptRenewRequest'
responses:
'204':
description: Existing live coordination lease renewed or duplicate operation accepted; durable authority is unchanged.
'409':
$ref: '#/components/responses/PreconditionFailed'
default:
$ref: '#/components/responses/Error'
/internal/v1/attempts/{attempt_id}/progress:
parameters:
- $ref: '#/components/parameters/AttemptId'
post:
operationId: reportAttemptProgress
security:
- serviceAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AttemptProgressRequest'
responses:
'204':
description: Current progress accepted or duplicate ignored.
'409':
$ref: '#/components/responses/PreconditionFailed'
default:
$ref: '#/components/responses/Error'
/internal/v1/attempts/{attempt_id}/outcome:
parameters:
- $ref: '#/components/parameters/AttemptId'
post:
operationId: reportAttemptOutcome
security:
- serviceAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AttemptOutcomeRequest'
responses:
'200':
description: Durable outcome projection or replay of the same operation.
content:
application/json:
schema:
$ref: '#/components/schemas/Job'
'409':
$ref: '#/components/responses/PreconditionFailed'
default:
$ref: '#/components/responses/Error'
components:
securitySchemes:
oidc:
type: openIdConnect
openIdConnectUrl: https://identity.invalid/.well-known/openid-configuration
serviceAuth:
type: mutualTLS
parameters:
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
description: Subject- and operation-scoped key retained for at least 24 hours.
schema:
type: string
pattern: '^[A-Za-z0-9._~-]{16,128}$'
Cursor:
name: cursor
in: query
schema:
type: string
minLength: 16
maxLength: 2048
Limit:
name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 200
default: 50
JobId:
name: job_id
in: path
required: true
schema:
$ref: '#/components/schemas/OpaqueId'
AttemptId:
name: attempt_id
in: path
required: true
schema:
$ref: '#/components/schemas/OpaqueId'
headers:
IdempotencyExpiresAt:
description: Earliest time at which the server may forget this key.
schema:
type: string
format: date-time
responses:
Error:
description: Structured error with user-safe detail only.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ApiError'
NotFound:
description: Missing or unauthorized resource; the response does not distinguish them.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ApiError'
IdempotencyConflict:
description: The key exists with a different canonical request fingerprint.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ApiError'
PreconditionFailed:
description: Durable authority, fence, phase, or lifecycle precondition failed.
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ApiError'
schemas:
OpaqueId:
type: string
pattern: '^[A-Za-z0-9][A-Za-z0-9_-]{7,127}$'
Target:
type: object
additionalProperties: false
required: [store, bucket, prefix]
properties:
store:
type: string
minLength: 1
maxLength: 128
bucket:
type: string
pattern: '^(?!.*\.\.)[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$'
prefix:
type: string
maxLength: 1024
description: NFC-normalized target segments; semantic validation follows architecture.md.
TargetOverride:
type: object
additionalProperties: false
minProperties: 1
properties:
bucket:
type: string
pattern: '^(?!.*\.\.)[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$'
prefix:
type: string
maxLength: 1024
description: NFC-normalized target segments; semantic validation follows architecture.md.
StagingTarget:
type: object
additionalProperties: false
required: [store, bucket, prefix]
properties:
store:
type: string
minLength: 1
maxLength: 128
bucket:
type: string
pattern: '^(?!.*\.\.)[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$'
prefix:
type: string
minLength: 1
maxLength: 1024
description: Exact attempt-scoped internal prefix selected by the service.
HttpSource:
type: object
additionalProperties: false
required: [kind, url]
properties:
kind:
const: http
url:
type: string
format: uri
maxLength: 8192
credential_ref:
type: string
maxLength: 512
ObjectStoreSource:
type: object
additionalProperties: false
required: [kind, store, bucket, key]
properties:
kind:
const: object_store
store:
type: string
minLength: 1
maxLength: 128
bucket:
type: string
minLength: 3
maxLength: 63
key:
type: string
minLength: 1
maxLength: 4096
version_id:
type: string
maxLength: 1024
credential_ref:
type: string
maxLength: 512
HuggingFaceSource:
type: object
additionalProperties: false
required: [kind, repository_type, repository_id, revision]
properties:
kind:
const: hugging_face
repository_type:
type: string
enum: [model, dataset, space]
repository_id:
type: string
pattern: '^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$'
revision:
type: string
minLength: 1
maxLength: 256
include:
type: array
maxItems: 1024
uniqueItems: true
items:
type: string
minLength: 1
maxLength: 1024
credential_ref:
type: string
maxLength: 512
Source:
oneOf:
- $ref: '#/components/schemas/HttpSource'
- $ref: '#/components/schemas/ObjectStoreSource'
- $ref: '#/components/schemas/HuggingFaceSource'
discriminator:
propertyName: kind
mapping:
http: '#/components/schemas/HttpSource'
object_store: '#/components/schemas/ObjectStoreSource'
hugging_face: '#/components/schemas/HuggingFaceSource'
JobSubmission:
type: object
additionalProperties: false
required: [source]
properties:
source:
$ref: '#/components/schemas/Source'
target:
description: Fields omitted here are taken from the configured default target before authorization.
$ref: '#/components/schemas/TargetOverride'
label:
type: string
minLength: 1
maxLength: 200
JobPhase:
type: string
enum: [Queued, Running, RetryWait, Cancelling, Succeeded, Failed, Cancelled]
JobLifecycle:
type: string
enum: [visible, soft_deleted, hard_deleting, hard_deleted]
AttemptAuthority:
type: object
additionalProperties: false
required: [attempt_id, fence, worker_incarnation]
properties:
attempt_id:
$ref: '#/components/schemas/OpaqueId'
fence:
type: integer
minimum: 1
worker_incarnation:
$ref: '#/components/schemas/OpaqueId'
Progress:
type: object
additionalProperties: false
required: [attempt_id, fence, completed_bytes, completed_members, observed_at]
properties:
attempt_id:
$ref: '#/components/schemas/OpaqueId'
fence:
type: integer
minimum: 1
completed_bytes:
type: integer
minimum: 0
total_bytes:
type: [integer, 'null']
minimum: 0
completed_members:
type: integer
minimum: 0
total_members:
type: [integer, 'null']
minimum: 0
observed_at:
type: string
format: date-time
ProgressValue:
type: object
additionalProperties: false
required: [completed_bytes, completed_members]
properties:
completed_bytes:
type: integer
minimum: 0
total_bytes:
type: [integer, 'null']
minimum: 0
completed_members:
type: integer
minimum: 0
total_members:
type: [integer, 'null']
minimum: 0
CheckpointProjection:
type: object
additionalProperties: false
required: [reusable, validated_bytes, validated_members]
properties:
reusable:
type: boolean
validated_bytes:
type: integer
minimum: 0
validated_members:
type: integer
minimum: 0
JobFailure:
oneOf:
- type: object
additionalProperties: false
required: [class, retryable, message]
properties:
class:
type: string
enum: [validation, authorization, source_not_found, source_changed, cancelled]
retryable:
const: false
message:
type: string
maxLength: 500
safe_details:
type: object
additionalProperties:
type: [string, integer, boolean, 'null']
- type: object
additionalProperties: false
required: [class, retryable, message]
properties:
class:
type: string
enum: [transient_source, transient_target, integrity, capacity, internal, lost_attempt]
retryable:
const: true
message:
type: string
maxLength: 500
safe_details:
type: object
additionalProperties:
type: [string, integer, boolean, 'null']
AttemptRuntimeFailure:
type: object
additionalProperties: false
required: [class, message]
properties:
class:
type: string
enum: [authorization, source_not_found, source_changed, transient_source, transient_target, integrity, capacity, internal]
message:
type: string
maxLength: 500
safe_details:
type: object
additionalProperties:
type: [string, integer, boolean, 'null']
ResultDescriptor:
type: object
additionalProperties: false
required: [kind, manifest, member_count, total_bytes]
properties:
kind:
const: manifest
manifest:
$ref: '#/components/schemas/ObjectIdentity'
member_count:
type: integer
minimum: 1
total_bytes:
type: integer
minimum: 0
ObjectIdentity:
type: object
additionalProperties: false
required: [store, bucket, key]
properties:
store:
type: string
bucket:
type: string
key:
type: string
version_id:
type: string
checksum:
type: string
Job:
type: object
additionalProperties: false
required: [id, owner, label, revision, phase, lifecycle, source, target, authoritative_attempt, progress, checkpoint, result, failure, created_at, updated_at, event_cursor]
properties:
id:
$ref: '#/components/schemas/OpaqueId'
owner:
$ref: '#/components/schemas/OpaqueId'
label:
type: [string, 'null']
minLength: 1
maxLength: 200
revision:
type: integer
minimum: 1
phase:
$ref: '#/components/schemas/JobPhase'
lifecycle:
$ref: '#/components/schemas/JobLifecycle'
source:
$ref: '#/components/schemas/Source'
target:
$ref: '#/components/schemas/Target'
authoritative_attempt:
oneOf:
- $ref: '#/components/schemas/AttemptAuthority'
- type: 'null'
progress:
oneOf:
- $ref: '#/components/schemas/Progress'
- type: 'null'
checkpoint:
oneOf:
- $ref: '#/components/schemas/CheckpointProjection'
- type: 'null'
result:
oneOf:
- $ref: '#/components/schemas/ResultDescriptor'
- type: 'null'
failure:
oneOf:
- $ref: '#/components/schemas/JobFailure'
- type: 'null'
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
event_cursor:
type: string
minLength: 16
JobPage:
type: object
additionalProperties: false
required: [items, next_cursor, event_cursor]
properties:
items:
type: array
items:
$ref: '#/components/schemas/Job'
next_cursor:
type: [string, 'null']
event_cursor:
type: string
minLength: 16
AttemptRecord:
type: object
additionalProperties: false
required: [id, fence, worker_incarnation, state, progress, result, failure, created_at, updated_at]
properties:
id:
$ref: '#/components/schemas/OpaqueId'
fence:
type: integer
minimum: 1
worker_incarnation:
$ref: '#/components/schemas/OpaqueId'
state:
type: string
enum: [AttemptAuthorized, AttemptExecuting, AttemptStopped, AttemptCommitted]
progress:
oneOf:
- $ref: '#/components/schemas/Progress'
- type: 'null'
result:
oneOf:
- $ref: '#/components/schemas/ResultDescriptor'
- type: 'null'
failure:
oneOf:
- $ref: '#/components/schemas/JobFailure'
- type: 'null'
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
AttemptPage:
type: object
additionalProperties: false
required: [items, next_cursor]
properties:
items:
type: array
items:
$ref: '#/components/schemas/AttemptRecord'
next_cursor:
type: [string, 'null']
DeletionProblem:
type: object
additionalProperties: false
required: [class, message]
properties:
class:
type: string
enum: [authorization, target_unavailable, configuration, internal]
message:
type: string
maxLength: 500
DeletionOperation:
type: object
additionalProperties: false
required: [id, job_id, state, problem, created_at, updated_at]
properties:
id:
$ref: '#/components/schemas/OpaqueId'
job_id:
$ref: '#/components/schemas/OpaqueId'
state:
type: string
enum: [queued, running, retry_wait, blocked, completed]
problem:
oneOf:
- $ref: '#/components/schemas/DeletionProblem'
- type: 'null'
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
ApiError:
type: object
additionalProperties: false
required: [code, message, request_id]
properties:
code:
type: string
enum: [invalid_request, unauthenticated, forbidden, not_found, idempotency_conflict, precondition_failed, cursor_invalid, cursor_expired, rate_limited, unavailable, internal]
message:
type: string
maxLength: 500
request_id:
$ref: '#/components/schemas/OpaqueId'
fields:
type: array
maxItems: 100
items:
type: object
additionalProperties: false
required: [pointer, code]
properties:
pointer:
type: string
maxLength: 500
code:
type: string
maxLength: 100
ClaimRequest:
type: object
additionalProperties: false
required: [operation_id, worker_incarnation]
properties:
operation_id:
$ref: '#/components/schemas/OpaqueId'
worker_incarnation:
$ref: '#/components/schemas/OpaqueId'
ExecutionEnvelope:
type: object
additionalProperties: false
required: [id, job_id, attempt_id, fence, worker_incarnation, source, resolved_source_ref, target, staging_target, execution_grant_ref, checkpoint_ref]
properties:
id:
$ref: '#/components/schemas/OpaqueId'
job_id:
$ref: '#/components/schemas/OpaqueId'
attempt_id:
$ref: '#/components/schemas/OpaqueId'
fence:
type: integer
minimum: 1
worker_incarnation:
$ref: '#/components/schemas/OpaqueId'
source:
$ref: '#/components/schemas/Source'
resolved_source_ref:
oneOf:
- $ref: '#/components/schemas/OpaqueId'
- type: 'null'
target:
$ref: '#/components/schemas/Target'
staging_target:
$ref: '#/components/schemas/StagingTarget'
execution_grant_ref:
$ref: '#/components/schemas/OpaqueId'
checkpoint_ref:
oneOf:
- $ref: '#/components/schemas/OpaqueId'
- type: 'null'
ClaimResponse:
type: object
additionalProperties: false
required: [authority, execution]
properties:
authority:
$ref: '#/components/schemas/AttemptAuthority'
execution:
$ref: '#/components/schemas/ExecutionEnvelope'
AttemptMutation:
type: object
required: [operation_id, fence, worker_incarnation]
properties:
operation_id:
$ref: '#/components/schemas/OpaqueId'
fence:
type: integer
minimum: 1
worker_incarnation:
$ref: '#/components/schemas/OpaqueId'
AttemptRenewRequest:
unevaluatedProperties: false
allOf:
- $ref: '#/components/schemas/AttemptMutation'
AttemptProgressRequest:
unevaluatedProperties: false
allOf:
- $ref: '#/components/schemas/AttemptMutation'
- type: object
required: [progress]
properties:
progress:
$ref: '#/components/schemas/ProgressValue'
AttemptOutcomeRequest:
unevaluatedProperties: false
allOf:
- $ref: '#/components/schemas/AttemptMutation'
- type: object
required: [outcome]
properties:
outcome:
oneOf:
- type: object
additionalProperties: false
required: [kind, result]
properties:
kind:
const: success
result:
$ref: '#/components/schemas/ResultDescriptor'
- type: object
additionalProperties: false
required: [kind, failure]
properties:
kind:
const: failure
failure:
$ref: '#/components/schemas/AttemptRuntimeFailure'