open-standards/wai/extensions/deployment.openapi.json
Transaction Science 09e4fb9a1b
All checks were successful
wai-app and Create / wai-app · does this change touch it (push) Successful in 12s
joule-moq-room room service / joule-moq-room · does this change touch it (push) Successful in 41s
joule-moq-remote host agent / joule-moq-remote · does this change touch it (push) Successful in 47s
joule-moq-web browser client / joule-moq-web · does this change touch it (push) Successful in 34s
WAI breaking changes and the conformance runner / WAI breaking · does this change touch WAI (push) Successful in 19s
wai-audit collector and player integration / does this change touch it (push) Successful in 25s
wai-fleet service / does this change touch it (push) Successful in 16s
wai-quantum for JavaScript / does this change touch it (push) Successful in 19s
WAI operation catalogue / does this change touch it (push) Successful in 26s
wai-key-proxy / does this change touch it (push) Successful in 43s
wai-quantum energy-landscape jobs / does this change touch it (push) Successful in 15s
wai-quantum studio and desktop / does this change touch it (push) Successful in 29s
wai-ledger service / does this change touch it (push) Successful in 29s
wai-sink-android native sink library / does this change touch it (push) Successful in 32s
wai-app and Create / wai-app · tests, clippy, the line language's verifier (linux) (push) Successful in 1m0s
WAI Studio / does this change touch it (push) Successful in 18s
wai-sink-swift native sink package / does this change touch it (push) Successful in 1m11s
WAI breaking changes and the conformance runner / WAI breaking · wire, record and API against main · linux (push) Successful in 1m41s
wai-audit collector and player integration / collector · linux (push) Successful in 2m9s
wai-key-proxy / proxy · linux (push) Successful in 2m2s
joule-moq-remote host agent / joule-moq-remote, every target (linux-arm64) (push) Successful in 3m45s
wai-quantum for JavaScript / javascript · linux (push) Successful in 5m1s
wai-fleet service / service · linux (push) Successful in 4m15s
joule-moq-room room service / joule-moq-room, every target (push) Successful in 6m41s
repository checks / lockfiles, advisories, tests in CI, workflow hygiene, spec and section references, public text (push) Successful in 7m22s
wai-sink-android native sink library / the JNI layer and the platform-free half on the host JVM · linux-arm64 (push) Successful in 4m18s
wai-ledger service / service · linux (push) Successful in 3m55s
WAI Studio / the catalogue's actions, the unit tests and the core · linux (push) Successful in 1m29s
wai-app and Create / wai-app · Create, browser end-to-end (macos) (push) Successful in 6m10s
WAI breaking changes and the conformance runner / WAI breaking and conform · their tests, and the independent verifiers through wai conform · linux (push) Successful in 7m48s
joule-moq-remote host agent / joule-moq-remote, every target (macos) (push) Successful in 47s
wai-quantum energy-landscape jobs / reader, kernels, answers and the stated result · linux, release (push) Successful in 8m31s
wai-audit collector and player integration / collector, player integration and the browser end to end · macos (push) Successful in 1m56s
wai-quantum studio and desktop / studio · browser (push) Successful in 6m44s
wai-quantum energy-landscape jobs / native against WebAssembly, over many seeds (push) Successful in 8m14s
wai-fleet service / service · macos (push) Successful in 1m1s
WAI operation catalogue / the catalogue and every door · linux (push) Successful in 10m58s
wai-ledger service / service · macos (push) Successful in 1m1s
wai-quantum studio and desktop / desktop app · linux (push) Successful in 8m9s
wai-quantum energy-landscape jobs / the same bits · linux-arm64 (push) Successful in 13m54s
wai-quantum studio and desktop / desktop app · macos (push) Successful in 3m39s
wai-quantum energy-landscape jobs / the module under wasm32-wasip2 (wasmtime) (push) Successful in 14m4s
wai-quantum energy-landscape jobs / the same bits · macos (push) Successful in 7m31s
WAI Studio / Studio in Chromium and Firefox · macos (push) Successful in 7m59s
wai-sink-android native sink library / every ABI, the AAR, and the instrumented tests in a headless emulator · macos (push) Successful in 8m53s
WAI Studio / Studio in WebKit · linux (push) Successful in 16m57s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (push) Successful in 17m59s
wai-sink-swift native sink package / libwai slices, desktop tests, simulator tests · macos (push) Successful in 11m32s
wai-quantum studio and desktop / desktop app · windows (push) Successful in 10m30s
wai deploy: a project's built tree on any deployment target, by its digest, with the target's signed receipts
What a deployment is (wai/extensions/deployment.md §3): dist/ as wai build
writes it, its build record and every file the record lists, named by the
SHA-256 of the record's bytes. A client before it sends a tree, and a target
before it holds one, hold the record to §3.3 and each file to its SHA-256 and
size.

The deployment API (§4, wai/extensions/deployment.openapi.json), which names
no host: GET /v1/deployments, PUT /v1/deployments/files/{sha256},
POST /v1/deployments, POST /v1/deployments/promote and
POST /v1/deployments/rollback, twelve refusal codes, and for each list,
create, promotion and rollback a wai.deployment-receipt/1 signed by the
target's Ed25519 key over its RFC 8785 form (§5): the operation, the
request's nonce, the target's count of changes, the deployment named, the
live deployment after and before, and the digest of what it holds.
wai::deployment (feature ed25519) reads records and signs and verifies
receipts for both sides.

The reference target is wai_deploy_target, a small service of its own in
wai-rs (feature quickstart), not a /_wai/deploy surface of wai dev: wai dev
cannot start without the relay and room service of other crates, and its
/_wai/ routes keep Studio's rules for a page. A target that serves nothing
else is tested alone, in this repository's CI. It keeps files by SHA-256,
written whole, its state written whole in place of the one before and held
to every record and file again on start, and serves the live deployment.

wai deploy (deployment.create), wai deployment list, promote <digest> and
rollback: placed client, preview, in a build with quickstart. wai deploy
builds, or with --prebuilt takes dist/ as it is, holds the tree to its
record before anything is sent, sends only what the target lacks, and with
--promote makes it live. Each takes --target, waits for --yes before it
builds, writes or sends anything, sends nothing with --dry-run, verifies
every receipt and holds it to its request, and keeps the receipts of each
change in .wai/deployments/<target>/ with its own operation receipt naming
them. wai --help's start list gains deploy, before verify. ops.json and
Studio's actions are regenerated (each operation bound to the command line);
mcp.json is unchanged, as no deployment operation is an MCP tool.

tests/cli_deploy.rs, in wai-catalogue.yml, runs the four against the
reference target and plants the faults the text refuses, each refused beside
a control that is not: a file of dist/ changed, added and removed after the
build; bytes put under another SHA-256; a record not in its canonical form;
a promotion of a digest the target does not hold, and of a file's SHA-256;
a rollback with none before; a replayed answer, an edited receipt, a list
that is not its receipt's state; and a kept receipt changed, refused by wai
verify. A rollback makes live exactly the deployment live before (history of
three); --dry-run leaves the target as it was, byte for byte; the OpenAPI
document's routes, operations, codes and receipt members are the
implementation's. tests/cli_ops.rs gains the three --dry-run samples.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-10 12:43:01 -05:00

695 lines
20 KiB
JSON

{
"openapi": "3.1.0",
"info": {
"title": "WAI deployment API",
"version": "1",
"description": "The API a deployment target serves (extensions/deployment.md §4): files put by their SHA-256, a deployment held by its digest (the SHA-256 of its build record, dist/wai-build.json), the live deployment promoted and rolled back, each answer to a list, a create, a promotion and a rollback carrying a receipt signed by the target's key (§5). Any host that serves these routes under an address is a deployment target; the paths are under that address. Each operationId names the operation of the WAI toolchain that sends the request, where one does.",
"license": {
"name": "Apache-2.0",
"identifier": "Apache-2.0"
}
},
"paths": {
"/v1/deployments": {
"get": {
"operationId": "deployment.list",
"summary": "What the target holds: each deployment, the live one and every one made live, with a signed receipt",
"parameters": [
{
"name": "nonce",
"in": "query",
"required": true,
"description": "Repeated in the receipt: 32 to 128 lowercase hexadecimal digits, fresh for each request.",
"schema": {
"$ref": "#/components/schemas/Nonce"
}
}
],
"responses": {
"200": {
"description": "The deployments held, and the receipt whose state is them",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/List"
}
}
}
},
"400": {
"description": "A nonce that is not one (`invalid-request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
},
"post": {
"operationId": "deployment.create",
"summary": "A deployment held by its digest, once the target holds its build record and every file the record lists",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"digest",
"nonce"
],
"properties": {
"digest": {
"$ref": "#/components/schemas/Digest"
},
"nonce": {
"$ref": "#/components/schemas/Nonce"
}
}
}
}
}
},
"responses": {
"200": {
"description": "Already held: the receipt, the target unchanged",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Created"
}
}
}
},
"201": {
"description": "Held now: the receipt",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Created"
}
}
}
},
"400": {
"description": "A body that is not a create (`invalid-request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "Files it does not hold, each to be put by its SHA-256; the record itself is named by the path wai-build.json (`files-missing`)",
"content": {
"application/json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/Error"
},
{
"type": "object",
"required": [
"missing"
],
"properties": {
"missing": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Missing"
}
}
}
}
]
}
}
}
},
"422": {
"description": "A build record that is not a deployment's (§3.3), or a file it lists whose size is another (`tree-refused`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/v1/deployments/files/{sha256}": {
"put": {
"operationId": "deployment_file.create",
"summary": "A file held by its SHA-256: the bytes sent must hash to it",
"parameters": [
{
"name": "sha256",
"in": "path",
"required": true,
"schema": {
"$ref": "#/components/schemas/Digest"
}
}
],
"requestBody": {
"required": true,
"description": "The file's bytes, with Content-Length.",
"content": {
"application/octet-stream": {
"schema": {
"type": "string",
"contentMediaType": "application/octet-stream"
}
}
}
},
"responses": {
"200": {
"description": "Already held",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileHeld"
}
}
}
},
"201": {
"description": "Held now",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/FileHeld"
}
}
}
},
"400": {
"description": "A path that is not a SHA-256 (`invalid-request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"411": {
"description": "A body without Content-Length (`length-required`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"413": {
"description": "Larger than the target takes (its list's limits.file_bytes) (`too-large`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"422": {
"description": "Bytes that are not the SHA-256 named; nothing is kept (`digest-mismatch`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/v1/deployments/promote": {
"post": {
"operationId": "deployment.promote",
"summary": "A deployment the target holds made its live one",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"digest",
"nonce"
],
"properties": {
"digest": {
"$ref": "#/components/schemas/Digest"
},
"nonce": {
"$ref": "#/components/schemas/Nonce"
},
"expect_live": {
"description": "Where given, the request is refused unless this is the live deployment (null: none is live).",
"oneOf": [
{
"$ref": "#/components/schemas/Digest"
},
{
"type": "null"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Live: the receipt (changed false when it was live already, and nothing changed)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Changed"
}
}
}
},
"400": {
"description": "A body that is not a promotion (`invalid-request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"404": {
"description": "A digest the target does not hold (`deployment-not-held`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "expect_live is not the live deployment (`live-changed`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
},
"/v1/deployments/rollback": {
"post": {
"operationId": "deployment.rollback",
"summary": "The deployment live before the live one made live again",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"additionalProperties": false,
"required": [
"nonce"
],
"properties": {
"nonce": {
"$ref": "#/components/schemas/Nonce"
},
"expect_live": {
"description": "Where given, the request is refused unless this is the live deployment.",
"oneOf": [
{
"$ref": "#/components/schemas/Digest"
},
{
"type": "null"
}
]
}
}
}
}
}
},
"responses": {
"200": {
"description": "Rolled back: the receipt",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Changed"
}
}
}
},
"400": {
"description": "A body that is not a rollback (`invalid-request`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
},
"409": {
"description": "No deployment was live before the live one, or expect_live is not the live deployment (`nothing-to-roll-back`, `live-changed`)",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Error"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"Digest": {
"description": "A SHA-256, lowercase hexadecimal.",
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"Nonce": {
"type": "string",
"pattern": "^[0-9a-f]{32,128}$"
},
"Error": {
"type": "object",
"required": [
"error",
"message"
],
"properties": {
"error": {
"enum": [
"invalid-request",
"not-found",
"deployment-not-held",
"method-not-allowed",
"files-missing",
"live-changed",
"nothing-to-roll-back",
"length-required",
"too-large",
"digest-mismatch",
"tree-refused",
"internal"
]
},
"message": {
"type": "string"
}
},
"description": "A refusal (§4.6): its code, and what happened."
},
"Missing": {
"type": "object",
"required": [
"path",
"sha256",
"bytes"
],
"properties": {
"path": {
"type": "string"
},
"sha256": {
"$ref": "#/components/schemas/Digest"
},
"bytes": {
"type": [
"integer",
"null"
],
"minimum": 0
}
}
},
"FileHeld": {
"type": "object",
"required": [
"sha256",
"bytes",
"held"
],
"properties": {
"sha256": {
"$ref": "#/components/schemas/Digest"
},
"bytes": {
"type": "integer",
"minimum": 0
},
"held": {
"const": true
}
}
},
"Held": {
"type": "object",
"required": [
"digest",
"project",
"files",
"bytes"
],
"properties": {
"digest": {
"$ref": "#/components/schemas/Digest"
},
"project": {
"type": "object",
"required": [
"sha256"
],
"properties": {
"name": {
"type": "string"
},
"sha256": {
"$ref": "#/components/schemas/Digest"
}
}
},
"files": {
"type": "integer",
"minimum": 0
},
"bytes": {
"type": "integer",
"minimum": 0
}
}
},
"List": {
"type": "object",
"required": [
"format",
"live",
"history",
"held",
"serves",
"limits",
"receipt"
],
"properties": {
"format": {
"const": "wai.deployments/1"
},
"live": {
"oneOf": [
{
"$ref": "#/components/schemas/Digest"
},
{
"type": "null"
}
]
},
"history": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Digest"
},
"description": "Every deployment made live and not rolled back, oldest first; the last is the live one."
},
"held": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Held"
},
"description": "Every deployment held, sorted by digest."
},
"serves": {
"type": [
"string",
"null"
],
"description": "The path under the target's address at which it serves its live deployment, or null where it serves none."
},
"limits": {
"type": "object",
"required": [
"file_bytes"
],
"properties": {
"file_bytes": {
"type": "integer",
"minimum": 0
}
}
},
"receipt": {
"$ref": "#/components/schemas/Receipt"
}
}
},
"Created": {
"type": "object",
"required": [
"created",
"receipt"
],
"properties": {
"created": {
"type": "boolean"
},
"receipt": {
"$ref": "#/components/schemas/Receipt"
}
}
},
"Changed": {
"type": "object",
"required": [
"changed",
"receipt"
],
"properties": {
"changed": {
"type": "boolean"
},
"receipt": {
"$ref": "#/components/schemas/Receipt"
}
}
},
"Receipt": {
"type": "object",
"description": "Signed by the target (§5): Ed25519 by key over the RFC 8785 form of every member but signature.",
"additionalProperties": false,
"required": [
"format",
"op",
"key",
"nonce",
"seq",
"digest",
"live",
"previous",
"state",
"time",
"signature"
],
"properties": {
"format": {
"const": "wai.deployment-receipt/1"
},
"op": {
"enum": [
"deployment.list",
"deployment.create",
"deployment.promote",
"deployment.rollback"
]
},
"key": {
"description": "The target's Ed25519 public key.",
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"nonce": {
"$ref": "#/components/schemas/Nonce"
},
"seq": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991,
"description": "How many changes the target has made, this one included."
},
"digest": {
"description": "The deployment the request named; for a rollback, the one made live; null for a list.",
"oneOf": [
{
"$ref": "#/components/schemas/Digest"
},
{
"type": "null"
}
]
},
"live": {
"description": "The live deployment after the request.",
"oneOf": [
{
"$ref": "#/components/schemas/Digest"
},
{
"type": "null"
}
]
},
"previous": {
"description": "The live deployment before it.",
"oneOf": [
{
"$ref": "#/components/schemas/Digest"
},
{
"type": "null"
}
]
},
"state": {
"description": "The SHA-256 of the RFC 8785 form of {\"held\": [every digest held, sorted], \"history\": [every one made live, oldest first]} after the request.",
"type": "string",
"pattern": "^[0-9a-f]{64}$"
},
"time": {
"type": "string",
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$"
},
"signature": {
"type": "string",
"pattern": "^[0-9a-f]{128}$"
}
}
}
}
}
}