# OpenWOP — full text for LLMs > OpenWOP (Workflow Orchestration Protocol) is an open-source protocol for multi-agent workflow orchestration: how a run of AI agents, tools and people starts, streams its events, stops for a person's approval, resumes and replays, over REST and Server-Sent Events, the same way on any host that implements it. Specification CC BY 4.0; code Apache 2.0. This file concatenates the site's guides, comparisons and the v2 specification (core and extensions) as markdown. The index is https://openwop.dev/llms.txt; RFCs are at https://openwop.dev/rfcs/. ## Guides ### Quickstart Source: https://openwop.dev/quickstart/ Run the v2 reference host on your machine, then use plain `curl` to discover what it supports, start a run, stream its events, answer an approval, pause and cancel a run, and fork one. Every command below was run against the v2 reference host from [`openwop/openwop-examples`](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/v2-reference), and every response shown is real output, trimmed. #### Four rules Every command below follows these four rules. - **Paths are unversioned.** A v2 operation lives at `/runs`, not `/v2/runs`. There is no version in the path ([versioning §1.2](https://openwop.dev/spec/v2/core/versioning.html)). - **Send `OpenWOP-Version: 2` on every request.** A host may prefer an older major, and a request with no header gets that one. Every response carries `OpenWOP-Version` naming the contract that produced it ([versioning §1.3–1.4](https://openwop.dev/spec/v2/core/versioning.html)). - **Ids are tenant-bound.** A `runId` looks like `/`. In a URL path the `/` must be escaped: send `%2F`, or the `~2F` form the host itself emits in links ([identity §5](https://openwop.dev/spec/v2/core/identity.html)). - **Presence is the claim.** The v2 discovery document has no `supported: false`. A host that supports a feature advertises a record for it; a host that does not, omits it ([capabilities §2](https://openwop.dev/spec/v2/core/capabilities.html)). #### Start a host You need Node 20 or later, `git`, `curl` and `jq`. ```bash git clone https://github.com/openwop/openwop-examples.git cd openwop-examples/examples/hosts/v2-reference npm install --legacy-peer-deps npm start ``` `--legacy-peer-deps` is required: the host pins the conformance suite and its contract package as exact peers, which npm's default resolver refuses. The host listens on `http://127.0.0.1:3838`, keeps its state in one SQLite file, and accepts a single development key. In a second terminal, set the two variables the rest of this guide uses. The OpenWOP CLI reads the same names. ```bash export OPENWOP_BASE_URL=http://127.0.0.1:3838 export OPENWOP_API_KEY=openwop-v2-dev-key ``` The reference host is a single process built to show the protocol, not to run production traffic. To use your own host instead, change these two values. #### Discover what the host supports Every host serves one discovery document at `/.well-known/openwop`. The `OpenWOP-Version` header selects which representation you get. ```bash curl -s "$OPENWOP_BASE_URL/.well-known/openwop" -H "OpenWOP-Version: 2" \ | jq '{protocolVersions, preferredVersion, implementation, lanes: [.auth.lanes[].lane]}' ``` ```json { "protocolVersions": ["1.11", "2.0"], "preferredVersion": "1.11", "implementation": { "name": "openwop-host-v2-reference", "version": "2.0.0-rc.1", "vendor": "openwop (reference example)" }, "lanes": ["api-key", "session", "saml", "scim", "workload"] } ``` `protocolVersions` lists every major the host serves; `2.0` is there, so v2 requests will work. `preferredVersion` is `1.11` because this host also serves the previous major, which is why you send the header. The `lanes` are the ways the host accepts credentials. Everything else at the root is either metadata or a capability record. To list the capability families this host advertises: ```bash curl -s "$OPENWOP_BASE_URL/.well-known/openwop" -H "OpenWOP-Version: 2" \ | jq -c '[to_entries[] | select(.value | type == "object" and has("status")) | .key]' ``` ```json ["limits","eventLog","interrupt","runList","replay","webhooks","idempotency","compensation","feedback","heartbeat","a2a","mcp","toolCatalog","workflowChainPacks","packs","sandbox","workspace","auth","conversationPrimitive","oauth","agents","supportedEnvelopes","schemaVersions","envelopeStrictness"] ``` Each record carries `status`, `since`, `witness` and the family's own facets. The `replay` record, for example, names the fork modes the host accepts: ```bash curl -s "$OPENWOP_BASE_URL/.well-known/openwop" -H "OpenWOP-Version: 2" | jq '.replay' ``` ```json { "status": "experimental", "since": "2.0", "until": "2.1", "witness": "witnessable-gated", "modes": ["replay", "branch"], "retention": { "days": 30 }, "effectSeamsManifest": "/host/effect-seams" } ``` Check a family's record before you use the feature. If the key is absent, the host does not offer it. Two negotiation rules are worth seeing once. Without the header, you get the host's preferred major: ```bash curl -si "$OPENWOP_BASE_URL/.well-known/openwop" | grep -i '^openwop-version' ``` ```text OpenWOP-Version: 1.11 ``` A major the host does not serve is refused with `406`, and the error lists what it does serve: ```bash curl -s "$OPENWOP_BASE_URL/.well-known/openwop" -H "OpenWOP-Version: 3" | jq . ``` ```json { "error": "protocol_version_unsupported", "message": "major 3 is not served by this host", "details": { "protocolVersions": ["1.11", "2.0"] } } ``` The discovery document also carries a standard `ETag`; send it back as `If-None-Match` and an unchanged document answers `304`. Read: [capabilities](https://openwop.dev/spec/v2/core/capabilities.html) for the record shape and the closed root, and [versioning](https://openwop.dev/spec/v2/core/versioning.html) for the negotiation rules. #### Authenticate Protocol operations take a bearer credential. How you obtain one depends on the lane: the reference host's `api-key` lane accepts the development key above, and a production host issues its own. Without a credential, the request is refused: ```bash curl -si -X POST "$OPENWOP_BASE_URL/runs" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"workflowId":"conformance-noop"}' ``` ```text HTTP/1.1 401 Unauthorized OpenWOP-Version: 2.0 WWW-Authenticate: Bearer Content-Type: application/json; charset=utf-8 {"error":"unauthenticated","message":"Authorization: Bearer is required"} ``` The host resolves every credential to a Subject, and that Subject owns the runs you create. Scopes include `runs:create`, `runs:read`, `runs:cancel`, `approvals:respond`, `artifacts:read` and the rest. Read: [identity](https://openwop.dev/spec/v2/core/identity.html) for lanes, Subjects, revocation and the id grammars. #### Create a run and read its snapshot `POST /runs` starts a workflow. `conformance-noop` is a one-node fixture the reference host ships; it completes almost at once. ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"workflowId":"conformance-noop","inputs":{}}' | tee run.json | jq . ``` ```json { "runId": "openwop-reference-tenant/hiCD9cCLBhv0yOQ4Y1PNNU8v", "status": "pending", "eventsUrl": "http://127.0.0.1:3838/runs/openwop-reference-tenant~2FhiCD9cCLBhv0yOQ4Y1PNNU8v/events", "statusUrl": "http://127.0.0.1:3838/runs/openwop-reference-tenant~2FhiCD9cCLBhv0yOQ4Y1PNNU8v" } ``` The `201` body is `{ runId, status, eventsUrl, statusUrl? }`. Save the run id in its path-safe form; `jq`'s `@uri` turns the `/` into `%2F`: ```bash RUN_ID=$(jq -r '.runId | @uri' run.json) ``` Then read the snapshot: ```bash curl -s "$OPENWOP_BASE_URL/runs/$RUN_ID" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" | jq . ``` ```json { "runId": "openwop-reference-tenant/hiCD9cCLBhv0yOQ4Y1PNNU8v", "workflowId": "conformance-noop", "status": "completed", "owner": { "tenant": "openwop-reference-tenant", "subject": { "issuer": "urn:openwop-host-v2-reference:api-key", "subjectId": "default", "tenant": "openwop-reference-tenant", "lane": "api-key", "kind": "user" } }, "eventLogSchemaVersion": 3, "engineVersion": 1, "compensationStatus": "none", "variables": {}, "startedAt": "2026-09-27T05:45:07.133Z", "completedAt": "2026-09-27T05:45:07.137Z" } ``` `owner.subject` records who created the run. `eventLogSchemaVersion: 3` marks a log written under v2. `Idempotency-Key` is optional but recommended. Keys must match `^[A-Za-z0-9._~-]{22,128}$`; a UUID fits, and a shorter key is refused with `400 idempotency_key_invalid`. Sending the same create twice with one key returns the original run rather than starting a second one, and the replayed response says so: ```text HTTP/1.1 201 Created {"runId":"openwop-reference-tenant/_gemiiE3se3ihi12wQhNIDlj", ...} HTTP/1.1 201 Created OpenWOP-Idempotent-Replay: true {"runId":"openwop-reference-tenant/_gemiiE3se3ihi12wQhNIDlj", ...} ``` The create body is closed: an unknown top-level key is `400 validation_error`, and a workflow that needs a capability the host does not advertise is `422 capability_required`. Read: [runs](https://openwop.dev/spec/v2/core/runs.html) for the run surface, the snapshot fields, cancel, pause and fork; [idempotency](https://openwop.dev/spec/v2/core/idempotency.html) for the key grammar and replay rules. #### Receive events A run is its append-only event log. Every event has a `sequence` that starts at `0` and only increases. There are three ways to read it. ##### Server-sent events Stream the run over SSE. `-N` stops `curl` buffering the stream: ```bash curl -sN "$OPENWOP_BASE_URL/runs/$RUN_ID/events?streamMode=updates" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" ``` ```text id: 0 event: run.started data: {"eventId":"a2yifnU3eMxIwiv4aFqSUDgW","runId":"openwop-reference-tenant/hiCD9cCLBhv0yOQ4Y1PNNU8v","type":"run.started","payload":{"workflowId":"conformance-noop","inputs":{},"transport":"rest",...},"sequence":0,...} id: 1 event: node.started data: {...,"type":"node.started","payload":{"nodeId":"noop","typeId":"core.noop","attempt":0},"sequence":1,...} id: 2 event: node.completed data: {...,"type":"node.completed","payload":{"nodeId":"noop","outputs":{},"durationMs":0},"sequence":2,...} id: 3 event: run.completed data: {...,"type":"run.completed","payload":{"outputs":{},"durationMs":4},"sequence":3,...} ``` Each frame's `id:` is the event's `sequence`, `event:` is its type, and `data:` is the full event. The host closes the stream after the run's terminal event. `streamMode` selects which events you receive: | Mode | You get | | --- | --- | | `updates` | Run and node transitions, suspensions and interrupt events. The default, and the one mode every host must implement. | | `values` | A full `state.snapshot` after each transition. Cannot be combined with other modes. | | `messages` | Token chunks from streaming AI nodes, for chat interfaces. | | `debug` | Every event in the log, including vendor events. | Combine the others with commas, such as `updates,messages`. A mode the host does not serve is `400 unsupported_stream_mode`, with `details.supported` listing the modes it does serve. Add `bufferMs` (0 to 5000) to receive events in `event: batch` frames. To resume after a disconnect, send the last `id:` you processed as `Last-Event-ID`. The host sends only later events: ```bash curl -sN "$OPENWOP_BASE_URL/runs/$RUN_ID/events" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Last-Event-ID: 2" ``` ```text id: 3 event: run.completed data: {...,"type":"run.completed","payload":{"outputs":{},"durationMs":4},"sequence":3,...} ``` ##### Polling Where SSE is not practical, long-poll with `afterSequence`. The response carries events with a higher `sequence`; omit the parameter to start from the first event. ```bash curl -s "$OPENWOP_BASE_URL/runs/$RUN_ID/events/poll?afterSequence=1" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ | jq '{lastSequence, status, isTerminal, events: [.events[] | {sequence, type}]}' ``` ```json { "lastSequence": 3, "status": "completed", "isTerminal": true, "events": [ { "sequence": 2, "type": "node.completed" }, { "sequence": 3, "type": "run.completed" } ] } ``` Feed `lastSequence` back as the next `afterSequence`, and stop when `isTerminal` is `true`. `timeout` (1 to 60 seconds, default 30) sets how long the host waits for new events. ##### Webhooks A host that advertises the `webhooks` family pushes matching events to your HTTPS endpoint, signed, with retries and a dead-letter sink. Register a subscription: ```bash curl -s -X POST "$OPENWOP_BASE_URL/webhooks" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{ "url": "https://my-app.example/openwop-webhook", "events": ["run.completed", "run.failed", "interrupt.requested"], "secret": "replace-with-a-long-random-secret" }' | tee webhook.json | jq . ``` ```json { "webhookId": "openwop-reference-tenant/ShVWniIstF0B_Io2x2c2vTDO" } ``` The response is only `{ webhookId }`: nothing returns a secret to you, so generate one yourself and send it in `secret`. The URL must be `https://` and publicly routable. The host refuses loopback, private and cloud-metadata addresses: ```json { "error": "webhook_url_rejected", "message": "url names a loopback, private, link-local or metadata host (webhooks.md §Egress)", "details": { "url": "https://localhost:9000/hook" } } ``` Unregister with the escaped id. The host answers `204` and makes no further delivery attempts for that subscription, including scheduled retries: ```bash curl -si -X DELETE "$OPENWOP_BASE_URL/webhooks/$(jq -r '.webhookId | @uri' webhook.json)" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" | head -1 ``` ```text HTTP/1.1 204 No Content ``` Each delivery is a POST whose body is `{ runId, workspaceId?, event }`, where `event` is the run event exactly as the stream carries it. Five headers come with it: `OpenWOP-Webhook-Id`, `OpenWOP-Event-Type`, `OpenWOP-Timestamp`, `OpenWOP-Signature` (`sha256=`) and `OpenWOP-Signature-Algorithm` (`v1`). Verify every delivery before you act on it: ```js import { createHmac, timingSafeEqual } from 'node:crypto'; // rawBody: the exact bytes received. headers: lower-cased header names. function verify(rawBody, headers, secret) { if (headers['openwop-signature-algorithm'] !== 'v1') return false; const ts = headers['openwop-timestamp'] ?? ''; if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // ±5 minutes const sig = (headers['openwop-signature'] ?? '').replace(/^sha256=/, ''); const expected = createHmac('sha256', secret).update(`${ts}.`).update(rawBody).digest('hex'); return sig.length === expected.length && timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex')); } ``` Delivery is at least once, so the same event can arrive more than once. Deduplicate on `(OpenWOP-Webhook-Id, runId, sequence)`. The TypeScript SDK ships the same check as `verifyWebhookSignature` in `@openwop/openwop/webhooks`. To see a delivery end to end you need a receiver the host can reach over public HTTPS, such as one behind a tunnel. The reference host's egress guard refuses a receiver on your own machine, as the spec requires. The function above was checked against a real delivery from the reference host with that guard relaxed for local testing; do not relax it on a host you expose. Read: [events](https://openwop.dev/spec/v2/core/events.html) for the envelope, stream modes, SSE frames and the poll cursor; [webhooks](https://openwop.dev/spec/v2/core/webhooks.html) for signing, durability and egress rules. #### Answer an interrupt A run that needs a human decision, an answer or an external signal suspends on an interrupt. `conformance-approval` suspends on an approval gate named `gate`: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"workflowId":"conformance-approval"}' > approval.json APPROVAL_ID=$(jq -r '.runId | @uri' approval.json) curl -s "$OPENWOP_BASE_URL/runs/$APPROVAL_ID/events/poll" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ | jq '{status, requested: (.events[] | select(.type == "interrupt.requested") | {nodeId, payload})}' ``` ```json { "status": "waiting-approval", "requested": { "nodeId": "gate", "payload": { "kind": "approval", "key": "openwop-reference-tenant/-FNm3JHnG_ZbKH2YNEwOamhF:gate:0", "data": { "artifactId": "gate", "artifactType": "conformance-artifact", "title": "Conformance approval", "actions": ["accept", "reject"], "description": "Conformance suite — please accept to complete the run." } } } } ``` The snapshot's `currentNodeId` also names the waiting node. Resolve it through the run-scoped surface, choosing one of the offered `actions`: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs/$APPROVAL_ID/interrupts/gate" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"resumeValue":{"action":"accept","decidedAt":"2026-09-27T12:00:00Z","feedback":"Looks good"}}' | jq . ``` ```json { "runId": "openwop-reference-tenant/-FNm3JHnG_ZbKH2YNEwOamhF", "nodeId": "gate", "status": "running" } ``` The run records `interrupt.resolved`, then `node.resumed`, `node.completed` and `run.completed`. An action outside `actions` is `400 validation_error`. A second resolve of the same interrupt fails, which is how two approvers racing each other are kept apart: ```text {"error":"interrupt_already_resolved","message":"the interrupt was already resolved, or its run is terminal"} HTTP 409 ``` Every interrupt kind uses this one resolve contract: `approval`, `clarification`, `external-event`, `custom`, the `conversation.*` kinds, `low-confidence` and `credential`. Approvals can also restrict who may decide (`approversList`) and require a quorum. A host should also offer a signed-token surface, `GET` and `POST /interrupts/{token}`, for approvers who do not hold a protocol credential. How the token reaches them, through a `callbackUrl` or an email link for example, is up to the host, so it is not shown here. Read: [interrupt](https://openwop.dev/spec/v2/core/interrupt.html) for the payload of each kind, the resolve surfaces, tokens and approver enforcement. #### Pause, resume and cancel Pause, resume, cancel and bulk cancel are core operations in v2, not optional extras. Start a run that waits for a minute: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"workflowId":"conformance-delay","inputs":{"delayMs":60000}}' > slow.json SLOW_ID=$(jq -r '.runId | @uri' slow.json) ``` Pause it. `drainPolicy` is `drain-current-node` (the default) or `immediate`: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs/$SLOW_ID:pause" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"drainPolicy":"immediate"}' | jq -c . ``` ```json {"runId":"openwop-reference-tenant/98w3lsHw-qcPAzKzD5V2UbAO","status":"paused"} ``` Resume it with `:resume`, and cancel it with `/cancel`: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs/$SLOW_ID:resume" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{}' | jq -c . curl -s -X POST "$OPENWOP_BASE_URL/runs/$SLOW_ID/cancel" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"reason":"done testing"}' | jq -c . ``` ```json {"runId":"openwop-reference-tenant/98w3lsHw-qcPAzKzD5V2UbAO","status":"running","resumedAt":"2026-09-27T05:45:10.488Z"} {"runId":"openwop-reference-tenant/98w3lsHw-qcPAzKzD5V2UbAO","status":"cancelling"} ``` `cancelling` means the cancel was accepted; the run emits `run.cancelled` when it finishes. Operations that do not fit the run's state are `409`: pausing a paused run is `run_state_conflict`, and cancelling a finished run is `run_terminal`: ```json {"error":"run_terminal","message":"a cancelled run cannot be cancelled","details":{"runStatus":"cancelled"}} ``` `POST /runs:bulk-cancel` takes `{ runIds, reason? }` (up to 100 ids) and answers one result per id, in order. #### Fork a run Forking creates a new run from any point in an existing run's log. A host that advertises `replay` serves it. **Replay** re-executes the workflow against the current code, holding the source's earlier events as fixed history. Use it to check that today's code reproduces what happened: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs/$RUN_ID:fork" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"mode":"replay"}' | jq . ``` ```json { "runId": "openwop-reference-tenant/48SVcRqaPNqiJrSvtFTp4vfn", "sourceRunId": "openwop-reference-tenant/hiCD9cCLBhv0yOQ4Y1PNNU8v", "fromSeq": 0, "mode": "replay", "status": "pending", "eventsUrl": "http://127.0.0.1:3838/runs/openwop-reference-tenant~2F48SVcRqaPNqiJrSvtFTp4vfn/events" } ``` `fromSeq` defaults to `0` for a replay. A replay must not repeat external side effects: a node that calls out resolves to the outcome the source run recorded, and fails closed if there is none. `GET /host/effect-seams` lists the seams the host guards: ```json {"seam":"http.fetch","guarded":true,"branchReFires":false} {"seam":"webhook.fanout","guarded":true,"branchReFires":true} ``` **Branch** starts a new, independent run from the state at `fromSeq`, optionally with different run options. Branch the approval run from just before its decision (sequence 2 is the `interrupt.requested` event), and take the other path: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs/$APPROVAL_ID:fork" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"mode":"branch","fromSeq":2,"runOptionsOverlay":{"tags":["branch-reject"]}}' \ | tee branch.json | jq . ``` ```json { "runId": "openwop-reference-tenant/4YYn25dH-SCQdLlfzBKpT4aN", "sourceRunId": "openwop-reference-tenant/-FNm3JHnG_ZbKH2YNEwOamhF", "fromSeq": 2, "mode": "branch", "status": "running", "eventsUrl": "http://127.0.0.1:3838/runs/openwop-reference-tenant~2F4YYn25dH-SCQdLlfzBKpT4aN/events" } ``` The branch waits at the gate again. Reject it this time: ```bash BRANCH_ID=$(jq -r '.runId | @uri' branch.json) curl -s -X POST "$OPENWOP_BASE_URL/runs/$BRANCH_ID/interrupts/gate" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{"resumeValue":{"action":"reject","decidedAt":"2026-09-27T12:05:00Z"}}' | jq -c . ``` ```json {"runId":"openwop-reference-tenant/4YYn25dH-SCQdLlfzBKpT4aN","nodeId":"gate","status":"failed"} ``` The source run still reads `completed`; the branch reads `failed` and carries the `branch-reject` tag. `runOptionsOverlay` is for branches only; a replay with an overlay is `400`. A `fromSeq` that is not in the source log is `422 fork_point_invalid`. The child run's `owner` is copied from the source. Read: [replay](https://openwop.dev/spec/v2/core/replay.html) for determinism, side-effect suppression and divergence events. #### Configure a run Run options travel in the create body as `configurable`, `tags` and `metadata`. In v2 `configurable` is a closed, nested object with a required `version: 1`, not a flat map of dotted keys: ```bash curl -s -X POST "$OPENWOP_BASE_URL/runs" \ -H "Authorization: Bearer $OPENWOP_API_KEY" \ -H "OpenWOP-Version: 2" \ -H "Content-Type: application/json" \ -d '{ "workflowId": "conformance-noop", "configurable": { "version": 1, "run": { "recursionLimit": 500, "runTimeoutMs": 120000 } }, "tags": ["quickstart", "q3-launch"], "metadata": { "requestedBy": "you@example.com" } }' ``` The snapshot returns them unchanged, and they cannot change after the run is created: ```json { "status": "completed", "configurable": { "version": 1, "run": { "recursionLimit": 500, "runTimeoutMs": 120000 } }, "tags": ["quickstart", "q3-launch"], "metadata": { "requestedBy": "you@example.com" } } ``` The sections are `run` (`recursionLimit`, `runTimeoutMs`, `maxLoopIterations`, `escalationThreshold`), `ai`, `distillation`, `budget` and `extensions`. An unknown key is refused: ```json {"error":"validation_error","message":"unknown configurable key ai.provider (closed schema)","details":{"path":"configurable.ai.provider"}} ``` ##### Bring your own key Model selection and BYOK go in the `ai` section. `credentialRef` is an opaque reference the host issued for a key it holds; key material never appears on the wire. ```json "configurable": { "version": 1, "ai": { "provider": "anthropic", "model": "claude-sonnet-5", "credentialRef": "secret_a3b9c2" } } ``` This needs a host that advertises the `aiProviders` family. `provider` must be listed in `aiProviders.providers`, and `credentialRef` must refer to a provider listed in `aiProviders.byok`, or the host answers `403 credential_forbidden`. The reference host advertises no AI provider, so it refuses the request above: ```json {"error":"validation_error","message":"ai.provider MUST be in aiProviders.providers — this host advertises no AI provider","details":{"path":"configurable.ai.provider"}} ``` Read: [runs §Run options](https://openwop.dev/spec/v2/core/runs.html) for every key and its limits. #### Write a node pack A node pack is a versioned, signed bundle of node types that workflows reference by `typeId`. A minimal v2 manifest: ```json { "name": "community.your-group.salesforce-tools", "version": "1.0.0", "kind": "node", "engines": { "openwop": ">=2.0 <3.0.0" }, "nodes": [ { "typeId": "community.your-group.salesforce.upsert", "version": "1.0.0", "category": "integration", "role": "side-effect", "capabilities": ["side-effectful"], "configSchemaRef": "schemas/upsert.config.json", "inputSchemaRef": "schemas/upsert.input.json", "outputSchemaRef": "schemas/upsert.output.json" } ], "runtime": { "language": "javascript", "entry": "dist/index.js", "format": "esm", "requires": ["net.outbound"] } } ``` This manifest validates against the v2 `node-pack-manifest` schema. Three rules to know: - **`engines.openwop` needs an explicit major ceiling.** It must match `>=X ` narrows the run. A filtered run against the reference host ends with a summary like this one (from suite 2.43.0; the counts depend on the suite version and the filter): ```text Test Files 17 passed | 106 skipped (123) Tests 27 passed | 472 skipped (499) [openwop-conformance] RFC 0148 §A dispositions — 44 requirement(s) recorded executed-pass 35 · blocked 6 · inapplicable 3 ``` A test that passes is not the same as a requirement that was checked. `inapplicable` means the requirement does not apply to your host; `blocked` means it was not measured. On a local run the webhook rows are `blocked` because the host's egress guard, correctly, refuses the suite's loopback receiver. A certification bundle with any `blocked` row does not certify. Hosts are measured against profiles, which are derived from the families you advertise rather than declared. `openwop-discovery-core` needs only the discovery document. `openwop-core-standard` needs the `interrupt`, `replay`, `webhooks`, `idempotency` and `eventLog` families and their 13 floor scenarios, and it is what an unqualified "OpenWOP conformant" claim means. To produce a signed bundle, add `--certify bundle.json` with `--host-build`, `--signing-key` and `--signing-key-id`. Read: [conformance](https://openwop.dev/spec/v2/core/conformance.html) for bundles and profiles, and the [conformance leaderboard](https://openwop.dev/conformance/) for published results. #### Use an SDK The 2.x SDKs speak v2 only. They send `OpenWOP-Version: 2.0` on every request. | Language | Package | Install | | --- | --- | --- | | TypeScript | `@openwop/openwop` 2.x | `npm install @openwop/openwop@2` | | Python | `openwop-client` 2.x | `pip install "openwop-client>=2,<3"` | | Go | `github.com/openwop/openwop-sdks/go/v2` | `go get github.com/openwop/openwop-sdks/go/v2` | The same run as above, in TypeScript: ```typescript import { OpenwopClient } from '@openwop/openwop'; const client = new OpenwopClient({ baseUrl: 'http://127.0.0.1:3838', apiKey: 'openwop-v2-dev-key', }); const caps = await client.discovery.capabilities(); console.log(caps.preferredVersion, caps.protocolVersions); const { runId } = await client.runs.create({ workflowId: 'conformance-noop' }); for await (const event of client.runs.events(runId)) { console.log(event.sequence, event.type); } ``` ```text 1.11 [ '1.11', '2.0' ] 0 run.started 1 node.started 2 node.completed 3 run.completed ``` The SDK escapes tenant-bound ids for you. For polling, `client.runs.pollEvents(runId, { afterSequence })` returns the same `{ runId, events, lastSequence, status, isTerminal }` shape as the raw call. Each SDK's README in [`openwop/openwop-sdks`](https://github.com/openwop/openwop-sdks) lists its methods, and `sdk/PARITY.md` there maps every v2 operation to its method in all three languages. #### Use the CLI The `openwop` CLI reads the same two environment variables and talks to the v2 run surface: ```bash npm install -g @openwop/cli openwop runs create conformance-noop --wait --json openwop runs list --limit 2 ``` ```text runId workflowId status createdAt ------------------------------------------------- ---------------- --------- --------- openwop-reference-tenant/dZKPp6swc3nVOXx5qs0ZNMfQ conformance-noop completed openwop-reference-tenant/btonV5DfkWaudv_uCfTjAjky conformance-noop completed ``` The [CLI page](https://openwop.dev/cli/) covers every command group. #### Try the reference application [`openwop/openwop-app`](https://github.com/openwop/openwop-app) is a full host with a React front end, and it runs at [app.openwop.dev](https://app.openwop.dev/). It advertises families the example host does not, such as `aiProviders`, so it can run model-backed workflows. The [install page](https://openwop.dev/install/) shows how to run it yourself. #### Next steps - [The v2 specification](https://openwop.dev/spec/v2/) — the core documents, in reading order. - [Error codes](https://openwop.dev/errors/) — every v2 error code with its HTTP status. - [Host implementers](https://openwop.dev/for/host-implementers/) — build a host and get it measured. - [The CLI](https://openwop.dev/cli/) — drive any host from your terminal. ### Frequently asked questions Source: https://openwop.dev/faq/ OpenWOP is a wire-level protocol, not a library or runtime. These are the questions people ask first when deciding whether it fits their work. Each answer links to the document that has the full rule. Last reviewed: 2026-09-27. #### Why not just use LangGraph or LangChain? LangGraph is a library; OpenWOP is a wire contract. LangGraph workflows depend on the Python runtime they were authored in — the engine, the checkpointer, and the serialization format are all internal to the library. OpenWOP defines what goes over the wire — REST endpoints, SSE event shapes, signed webhook payloads, BYOK secret resolution — so a workflow authored for one host can move to a different host without rewriting application code. If you only ever run on a single runtime, a library is the simpler choice. If you want your workflows portable across hosts (your own, your customers', a future managed service), the wire contract is the point. See [Where it sits, and what it isn't](https://openwop.dev/#context) for the longer comparison and the [comparison table](https://openwop.dev/#context) at a glance. #### Why not Temporal or AWS Step Functions? Both are excellent durable workflow engines, but each is the contract of one product. Temporal's API is the API of the Temporal server; Step Functions workflows run only on AWS. OpenWOP aims for one contract that many independent hosts implement, with a public conformance suite that checks them. To be clear about where that stands: every host on the [leaderboard](https://openwop.dev/conformance/) today is run by the steward or a steward-affiliated organization. No independent host exists yet. OpenWOP is also built for multi-agent execution (supervisor and worker agents, conversations, BYOK provider routing, replay-safe agent memory). With Temporal or Step Functions you would build those on top yourself. #### What does it actually cost a host to implement OpenWOP? Start small. The `openwop-discovery-core` profile only asks for a valid discovery document and correct version handling. The [v2 reference host](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/v2-reference) shows a complete implementation to copy from. Each capability you add brings its own surface and its own conformance scenarios: webhooks (HMAC signing), interrupts, BYOK (provider routing and redaction), audit-log signing, `production` (backpressure and retention). You can run those scenarios against your host with no wrapper code. v2 has three named profiles; their requirements are on [`/profiles/`](https://openwop.dev/profiles/). #### Why publish packs to `packs.openwop.dev` instead of npm or PyPI? Packs need a signature scoped to a namespace: an Ed25519 signature over the canonical `pack.json`, checked against a registry key that is allowed to sign that namespace. npm and PyPI don't provide that for arbitrary payloads. The OpenWOP registry serves the signed tarball, the detached signature and the publisher's public key. The [pack-consumer reference implementation](https://github.com/openwop/openwop-examples/blob/main/examples/hosts/postgres/src/pack-consumer.ts) shows a host verifying the signature at install time. You can also publish to a private registry or a tenant-scoped mirror. The [Packs](https://openwop.dev/spec/v2/core/packs.html) spec defines the discovery shape, so clients can point at any registry that serves `.well-known/openwop-registry.json`. #### How does BYOK work? A client picks the provider for each run in `RunOptions.configurable.ai`: a `provider`, a `model`, and a `credentialRef` naming a credential the host holds. The reference never carries key material ([Runs](https://openwop.dev/spec/v2/core/runs.html) §"Run options"). A node can also choose per call with `ctx.callAI({provider, ...})`. The host resolves the secret. It never logs it and never echoes it in events or debug bundles ([secret-leakage threat model](https://github.com/openwop/openwop/blob/main/SECURITY/threat-model-secret-leakage.md)). The SR-1 invariant on [`/security/`](https://openwop.dev/security/) is the public test that this holds. Hosts advertise their BYOK policy (`disabled`, `optional`, `required` or `restricted`) under `aiProviders`, so a client can check before it submits a run. #### Is OpenWOP suitable for non-AI workflows? Yes. The AI surfaces (provider routing, model capabilities, agent memory, reasoning events) are optional capabilities. A host can meet the `openwop-core-standard` profile (runs, events, interrupts and the rest of the core) without exposing any AI provider. The vocabulary leans toward agents because that is the main use today, but the wire format doesn't require it. #### How do I get notified about security advisories? Watch the [openwop/openwop repository on GitHub](https://github.com/openwop/openwop) for releases — that's the canonical channel. Every advisory is published as a tagged release with a `### Security` heading in CHANGELOG.md citing the advisory ID, the affected versions, and the migration path. Pre-disclosure embargo follows [`SECURITY.md`](https://openwop.dev/security/) §4 "Coordinated disclosure." [CHANGELOG.md](https://github.com/openwop/openwop/blob/main/CHANGELOG.md) is in the repository. RSS feed and email notification aren't on the roadmap; the GitHub release watch is the supported subscription path. ### Error codes Source: https://openwop.dev/errors/ The canonical OpenWOP v2 error envelope is `{ error, message, details? }` returned with the registered HTTP status. This page lists every code in the v2 error registry with its HTTP status and whether it is retriable. Generated from [`spec/v2/errors.json`](https://github.com/openwop/openwop/blob/main/spec/v2/errors.json) at build time, so it cannot drift from the corpus. Envelope shape: [`core/errors.md`](https://openwop.dev/spec/v2/core/errors.html). #### Envelope shape ```json { "error": "validation_error", "message": "Request body fails schema", "details": { "field": "workflowId", "reason": "missing" } } ``` - `error` — machine-readable identifier: a registered code or a vendor code. Clients route on `error`, never on `message`. Stable within a major; new codes land additively. - `message` — human-readable explanation, for people and logs only. Good practice (not a v2 rule): don't echo the offending input verbatim, since it can carry a prompt-injection payload or a credential. - `details` — optional structured context; contextual data (conflict refs, trace ids, validation paths) lives here, never at a new top level. The body carries nothing else (`additionalProperties: false`). The HTTP status code carries the primary classification; the `error` field disambiguates within the status class. A host MUST answer with the status the registry assigns to the code. Retry timing lives in the `Retry-After` header only — never in `details`. #### Codes by HTTP status #### Stability guarantees Per [Versioning & compatibility](https://openwop.dev/versioning/): - **New codes** land additively within a major. A client MUST accept an unknown code and MUST NOT act on it ([`overview.md`](https://openwop.dev/spec/v2/core/overview.html) §0); the HTTP status still classifies the response. - **Existing codes** never change their HTTP status mapping or their semantic meaning within a major; renaming a code is a major-version change. - **Removed codes** are a major-version change, with one narrow exception for retiring a code ([overview](https://openwop.dev/spec/v2/core/overview.html) §0a). A registry row can carry a `deprecated` marker. #### What this page is not This is not the normative source. The error envelope shape is defined in [`core/errors.md`](https://openwop.dev/spec/v2/core/errors.html); each code's preconditions, the events it pairs with (`envelope.retry.exhausted`, `cap.breached`, etc.), and any SR-1 redaction rules are documented in the source named in its "Defined in" column. When this page disagrees with the spec, the spec wins. This page exists as a reference card for client implementers — one place to grep when wiring error handling. ### Scenario walkthroughs Source: https://openwop.dev/scenarios/ Three end-to-end examples of OpenWOP runs, showing the workflow definition, the wire-level events the host emits, and what the client sees. Each scenario links back to the normative spec sections that govern its surface. Reading time: ~15 minutes total. Each scenario is independent — skim the one that matches what you're building. #### Scenario A — Single agent with human approval **What it shows:** the most common pattern — a supervisor agent makes a decision, asks for human confirmation, resumes when the human answers, and terminates. **Spec surfaces touched:** [run lifecycle](https://openwop.dev/spec/v2/core/runs.html), [interrupts](https://openwop.dev/spec/v2/core/interrupt.html), [stream modes](https://openwop.dev/spec/v2/core/events.html). ##### Workflow definition ```json { "id": "wf-approve-and-act", "version": 1, "nodes": [ { "id": "decide", "typeId": "core.orchestrator.supervisor" }, { "id": "ask", "typeId": "core.hitl.clarify" }, { "id": "act", "typeId": "core.http.request" } ], "edges": [ { "from": "decide", "to": "ask" }, { "from": "ask", "to": "act" } ], "channels": [ { "name": "decision", "type": "string" }, { "name": "approval", "type": "boolean" } ] } ``` ##### Wire trace Client creates the run: ```http POST /runs OpenWOP-Version: 2 Authorization: Bearer … Content-Type: application/json Idempotency-Key: ck-2026-05-21-001 { "workflowId": "wf-approve-and-act", "inputs": { "url": "https://api.example.com/charge" } } ``` Host responds: ```http HTTP/1.1 201 Created { "runId": "run-abc", "status": "pending", "eventsUrl": "/runs/run-abc/events" } ``` Client opens an SSE stream: ```http GET /runs/run-abc/events?streamMode=updates OpenWOP-Version: 2 Accept: text/event-stream ``` Events flow: ``` event: run.started { "runId": "run-abc", "ts": "2026-05-21T18:00:00Z" } event: runOrchestrator.decided { "agentId": "supervisor", "decision": { "kind": "ask-user", "prompt": "Charge $42 to example.com?" } } event: interrupt.requested { "kind": "clarification", "key": "ask-approval", "data": { "questions": [{ "id": "approval", "question": "Charge $42 to example.com?" }] } } ``` The run is now `waiting-input`. The host serializes its state and waits. ##### Human answers, run resumes ```http POST /runs/run-abc/interrupts/ask { "resumeValue": { "approval": true } } ``` Events continue: ``` event: interrupt.resolved { "nodeId": "ask", "interruptId": "…", "kind": "clarification" } event: node.started { "nodeId": "act", "typeId": "core.http.request" } event: node.completed { "nodeId": "act", "outputs": { "status": 200 } } event: run.completed { "outputs": { "status": 200 } } ``` The `runId` is stable across the pause; deep linking, replay, and fork all work against the same id. ##### What just happened, normatively 1. `POST /runs` with `Idempotency-Key` accepted per [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html). The same key on retry returns the existing run. 2. The orchestrator emitted `runOrchestrator.decided { decision: { kind: "ask-user" } }` per [RFC 0006](https://openwop.dev/rfcs/0006-orchestrator.html) §B–§C. 3. `interrupt.requested` with `kind: "clarification"` per [`interrupt.md`](https://openwop.dev/spec/v2/core/interrupt.html) §Payload. The run is in `waiting-input` status. 4. `POST /runs/{runId}/interrupts/{nodeId}` accepts the answer per [`interrupt.md`](https://openwop.dev/spec/v2/core/interrupt.html) §"Resolve surfaces." The host MUST validate the `resumeValue` against the interrupt's `resumeSchema`, when one is declared, before resuming. 5. SSE event ordering preserved across the suspension — `sequence` is strictly increasing per run, and a client that reconnects with `Last-Event-ID` receives every later event in log order per [`events.md`](https://openwop.dev/spec/v2/core/events.html). --- #### Scenario B — Multi-agent fan-out with envelope retries **What it shows:** a supervisor dispatches to two worker agents in sequence, each producing a typed envelope. The second agent's first envelope fails schema validation and the host retries automatically before the run continues. **Spec surfaces touched:** [multi-agent execution](https://openwop.dev/spec/v2/core/capabilities.html#multiagent), [AI envelope](https://openwop.dev/spec/v2/core/capabilities.html#aienvelope), [RFC 0032](https://openwop.dev/rfcs/0032-envelope-reliability-events.html) reliability events. ##### Workflow definition ```json { "id": "wf-research-and-summarize", "version": 1, "nodes": [ { "id": "supervise", "typeId": "core.orchestrator.supervisor" }, { "id": "research", "typeId": "core.openwop.agents.deep-research", "config": { "envelope": "research.findings" } }, { "id": "summarize", "typeId": "core.llm.chat", "config": { "envelope": "summary.brief", "model": "claude-sonnet-4-6" } } ], "edges": [ { "from": "supervise", "to": "research" }, { "from": "supervise", "to": "summarize" } ] } ``` `fanOutPolicy: "sequential"` is the default per [RFC 0022](https://openwop.dev/rfcs/0022-dispatch-input-output-mapping.html); see that RFC for the parallel variant. ##### Wire trace Run starts. Supervisor dispatches to `research`: ``` event: run.started event: runOrchestrator.decided { "agentId": "supervisor", "decision": { "kind": "next-worker", "nextWorkerIds": ["research", "summarize"] } } event: node.started { "nodeId": "research", "typeId": "core.openwop.agents.deep-research" } event: agent.toolCalled { "callId": "tc-001", "agentId": "research", "toolName": "web.search", "argsHash": "sha256:…" } event: agent.toolReturned { "callId": "tc-001", "agentId": "research", "toolName": "web.search", "status": "ok" } event: node.completed { "nodeId": "research" } ``` Supervisor dispatches to `summarize`. First attempt at the summary envelope fails schema validation: ``` event: node.started { "nodeId": "summarize", "typeId": "core.llm.chat" } ← attempt 1 emits a summary.brief envelope that fails schema validation event: envelope.retry.attempted { "nodeId": "summarize", "attempt": 2, "reason": "schema-violation" } event: node.completed { "nodeId": "summarize" } event: run.completed { "outputs": { … } } ``` ##### What just happened, normatively 1. `runOrchestrator.decided { decision: { kind: "next-worker", nextWorkerIds: [...] } }` is the supervisor decision per [RFC 0006](https://openwop.dev/rfcs/0006-orchestrator.html) §B–§C; the planner→worker handoff it drives is [RFC 0037](https://openwop.dev/rfcs/0037-multi-agent-execution-model.html) §B. 2. `core.dispatch` iterates `nextWorkerIds` sequentially per [RFC 0022](https://openwop.dev/rfcs/0022-dispatch-input-output-mapping.html) §D. The parent variable bag is the cross-worker handoff channel per §D. 3. The invalid first attempt triggers a host-side retry per [RFC 0032](https://openwop.dev/rfcs/0032-envelope-reliability-events.html) §B.1 — `envelope.retry.attempted` (SHOULD-tier; the first attempt emits nothing) carries the attempt count and the canonical `reason`. 4. If the retry budget were exhausted, the host would emit `envelope.retry.exhausted` (a MUST under RFC 0032 §B.2) and the run would fail with `envelope_invalid` (see [Error codes](https://openwop.dev/errors/), HTTP 422). Here the second attempt validated. 5. `agent.toolCalled` + `agent.toolReturned` pair via shared `callId` per [RFC 0002](https://openwop.dev/rfcs/0002-agent-identity-and-reasoning-events.html). On a host advertising `toolHooks.prePostEvents` ([RFC 0064](https://openwop.dev/rfcs/0064-tool-invocation-hooks-and-authorization.html)), the call carries `argsHash` — SHA-256 over the JCS-canonicalized args with secrets already redacted (SR-1), the content-free alternative to raw `inputs` — and the return carries `status`. --- #### Scenario C — Suspend, restart, replay-fork from checkpoint **What it shows:** a long-running workflow is interrupted by a host restart, comes back up from its event log, and the operator forks a new run from a historical checkpoint to test a policy change. **Spec surfaces touched:** [replay](https://openwop.dev/spec/v2/core/replay.html), [storage adapters](https://openwop.dev/spec/v2/core/persistence.html), [version negotiation](https://openwop.dev/spec/v2/core/versioning.html). ##### Setup A run started yesterday with the SQLite reference host. Mid-run, the host process restarts. ``` event: run.started { "runId": "run-xyz" } sequence 0 event: node.started { "nodeId": "fetch-data" } sequence 1 event: node.completed { "nodeId": "fetch-data" } sequence 2 event: node.started { "nodeId": "transform" } sequence 3 event: node.completed { "nodeId": "transform" } sequence 4 ← host process exits here ``` ##### Host comes back up On startup, the host scans its event log for runs in non-terminal status. For `run-xyz` it finds the last persisted event and continues, with no client action: ``` event: node.started { "nodeId": "publish", "typeId": "core.http.request" } sequence 5 event: node.completed { "nodeId": "publish" } sequence 6 event: run.completed { "outputs": { … } } sequence 7 ``` No `run.resumed` is emitted: that event marks the exit from `paused`, and a restart is not a pause. Every event carries its `sequence`, so a client whose stream dropped reconnects with `Last-Event-ID` set to the last sequence it saw and receives exactly the events after it — which is how it deduplicates if its downstream had partial state ([`events.md`](https://openwop.dev/spec/v2/core/events.html) §"SSE frames"). ##### Operator forks from a historical checkpoint A policy change in the `transform` node means the operator wants to re-run the second half of yesterday's pipeline with the new logic, without re-fetching data. ```http POST /runs/run-xyz:fork { "mode": "branch", "fromSeq": 4, "runOptionsOverlay": { "configurable": { "transformMode": "v2" } } } ``` Host responds: ```http HTTP/1.1 201 Created { "runId": "run-xyz-fork-1", "sourceRunId": "run-xyz", "mode": "branch", "fromSeq": 4, "status": "pending", "eventsUrl": "https://api.example.com/runs/run-xyz-fork-1/events" } ``` The new run inherits events 0..3 from the parent as fixed history (all of `fetch-data`, and the start of `transform`), then begins fresh execution from event 4 — `transform`'s result onward — with the new `transformMode: "v2"`. The original `run-xyz` is unchanged. ##### What just happened, normatively 1. `sequence` is strictly increasing per run and persisted logs are never renumbered ([`events.md`](https://openwop.dev/spec/v2/core/events.html) §"The envelope"). A host that accepted work MUST resume it after the accepting process dies, without client action, per [`persistence.md`](https://openwop.dev/spec/v2/core/persistence.html) §"Durable acceptance and recovery." 2. Recovery is not a pause: `run.resumed` is emitted only when a run leaves `paused` ([`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Pause and resume"), and it carries no restart cursor — the client's cursor is `Last-Event-ID`. 3. `POST /runs/{runId}:fork` is normative for replay-fork per [`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Fork" and [`replay.md`](https://openwop.dev/spec/v2/core/replay.html). Events with `sequence < fromSeq` are fixed history; events from `fromSeq` on are re-executed. In `replay` mode that prefix is byte-equivalent to the parent (§"Byte-equivalence of the prefix"); a `branch` like this one starts from the projected state at `fromSeq` with the caller's `runOptionsOverlay` and is not deterministic by design. 4. The fork carries a new `runId` so client-side deep links to the parent remain stable. 5. Per-region clock fields MAY differ on the fork (the fork executes "now", not at the parent's event times) per [`replay.md`](https://openwop.dev/spec/v2/core/replay.html) §"Byte-equivalence of the prefix" (RFC 0036 §E). --- #### What these scenarios skip Three things deliberately left out, all linked here for the curious: - **Auth handshake** — every request carries `Authorization: Bearer …`. See [Auth profiles](https://openwop.dev/spec/v2/core/identity.html) for OAuth2 / OIDC / mTLS variants. - **Webhook delivery** — the SSE stream above is the synchronous view; production hosts also deliver every event over HMAC-signed webhooks per [`webhooks.md`](https://openwop.dev/spec/v2/core/webhooks.html). - **BYOK secret resolution** — the `summarize` node in Scenario B used a host-managed Anthropic key. For BYOK, see the [`aiProviders`](https://openwop.dev/spec/v2/core/capabilities.html#aiproviders) `byok` facet and the `ai.credentialRef` run option in [`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Run options." #### Where to go next | Want to | Go to | |---|---| | See the actual workflow-definition schema | [`/schemas/workflow-definition.schema.json`](https://openwop.dev/schemas/workflow-definition.schema.json) | | Walk through the full REST endpoint catalog | [`/api/rest/`](https://openwop.dev/api/rest/) | | See the production-shaped host that does all this end-to-end | [Postgres reference host](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/postgres) | | Run scenario A against the live demo | [app.openwop.dev](https://app.openwop.dev/) | ### Community Source: https://openwop.dev/community/ OpenWOP is developed in the open on `github.com/openwop/openwop`. This page lists every place a question, design conversation, or bug report can land — and the cadence each channel runs on. #### Channels | Channel | Use it for | Cadence | |---|---|---| | [GitHub Discussions](https://github.com/openwop/openwop/discussions) | Design questions, RFC comments, "is this the right way?" threads | Acknowledged within 5 business days per `MAINTAINERS.md` | | [GitHub Issues](https://github.com/openwop/openwop/issues) | Bug reports, conformance failures, spec ambiguity | Acknowledged within 5 business days | | [Security advisories](https://github.com/openwop/openwop/security/advisories/new) | Coordinated vulnerability disclosure | Acknowledged within 3 business days per `SECURITY.md` | | Real-time chat | — | **Planned.** Subscribe to GitHub Discussions for now; the channel choice (Discord / Matrix / Zulip) is tracked in [ROADMAP.md](https://github.com/openwop/openwop/blob/main/ROADMAP.md). | > **Why no chat yet?** A real-time channel announces "we're here" without proving "we're a standard." Until the working-group charter (RFC 0038) ratifies and at least one non-steward maintainer joins, GitHub Discussions is the canonical async channel — Discord would currently signal a product, not a protocol. #### Code of conduct Every channel above runs under [`CODE_OF_CONDUCT.md`](https://github.com/openwop/openwop/blob/main/CODE_OF_CONDUCT.md). Enforcement and removal-for-cause processes live in [`MAINTAINERS.md`](https://openwop.dev/maintainers/) §"Removal for cause." #### RFC comment windows When a new RFC opens (status `Draft`) it carries a comment window declared in the RFC body — 7, 30, or 90 days depending on classification (normative addition, breaking change, safety-fix break) per [`GOVERNANCE.md`](https://openwop.dev/governance/) §"Spec change process." A waived window is recorded in [`MAINTAINERS.md`](https://openwop.dev/maintainers/) §"Bootstrap-phase RFC waivers." Subscribe to the [RFCs](https://openwop.dev/rfcs/) index to see windows open and close. #### Office hours **Not yet scheduled.** The maintainer set is too small to run a recurring slot honestly. When the working-group charter ratifies, the first scheduled action is a public office-hours cadence — that change lands here and in `CHANGELOG.md` § Governance. #### Reporting - **Spec ambiguity** → GitHub issue against `openwop/openwop`, label `spec`. - **Conformance scenario failure** → GitHub issue, label `conformance`, attach the run log. - **Schema break** → GitHub issue, label `schema`, attach the diff. - **Security vulnerability** → private advisory only (never the public tracker). See `SECURITY.md`. #### Adopting OpenWOP If you're shipping an OpenWOP-compatible host or workflow, open a PR adding a row to [`INTEROP-MATRIX.md`](https://openwop.dev/conformance/). The [roadmap](https://openwop.dev/roadmap/) tracks independent implementations. #### See also - [Maintainers](https://openwop.dev/maintainers/) — who runs the project, what they gate, how recruitment works. - [Governance](https://openwop.dev/governance/) — decision rules, role definitions, working-group path. - [Contributing](https://openwop.dev/contributing/) — change rules and the checks every pull request must pass. - [Code of Conduct](https://github.com/openwop/openwop/blob/main/CODE_OF_CONDUCT.md). ### The OpenWOP protocol Source: https://openwop.dev/protocol/ Everything that defines OpenWOP (the wire contract, how it changes, its security posture, and the project behind it) is linked from this page. #### Specification The normative wire contract. **v2** is the current major, cut 2026-09-05. - **[v2 spec corpus →](https://openwop.dev/spec/v2/)** — the normative core documents, plus the optional extension families. - **[REST API reference →](https://openwop.dev/api/rest/)** — every endpoint with request, response, and error envelope, rendered from `api/v2/openapi.yaml`. - **[Profiles →](https://openwop.dev/profiles/)** — the three named profiles a host can claim. Everything else is advertised as an individual [capability](https://openwop.dev/spec/v2/core/capabilities.html). #### Comparisons and positioning Where OpenWOP sits relative to adjacent agent protocols. - **[A2A vs MCP vs OpenWOP →](https://openwop.dev/comparisons/a2a-openwop-mcp/)** — a specification-level comparison of agent-to-agent collaboration, tool/context integration, and durable workflow orchestration. - **[Tool calling →](https://openwop.dev/tool-calling/)** — what tool calling standardizes, and the four surfaces OpenWOP adds on top of it: catalog, durable invocation events, fail-closed authorization, and effect idempotency. - **[OpenExO 3.0 and OpenWOP →](https://openwop.dev/comparisons/openexo-3-openwop/)** — an architecture thesis for using OpenWOP as the durable execution protocol beneath OpenExO 3.0, the Intelligence Stack, REWRITE, and Edge Twins. - **[Overview & axioms →](https://openwop.dev/spec/v2/core/overview.html)** — the six axioms, the claim vocabulary, and the rule deciding what is core and what is an extension. #### Conformance How a host proves it implements the spec. - **[Conformance leaderboard →](https://openwop.dev/conformance/)** — which hosts pass which scenarios. - **[Conformance suite →](https://github.com/openwop/openwop/tree/main/conformance)** — `@openwop/openwop-conformance`, the test suite behind every leaderboard row. - **[Error codes →](https://openwop.dev/errors/)** — the canonical error vocabulary every conforming host emits. #### RFCs How the protocol evolves. - **[RFC index →](https://openwop.dev/rfcs/)** — every RFC and its status. Within a major, changes are additive only. - **[RFC process →](https://openwop.dev/rfcs/0001-rfc-process.html)** — how an RFC moves from Draft to Accepted, and how comment windows work. #### Governance Who decides what gets in, and how. - **[Governance →](https://openwop.dev/governance/)** — decision rules, role definitions, the sole-steward operating rules, and the conditions for moving to a cross-vendor working group. - **[Maintainers →](https://openwop.dev/maintainers/)** — who the maintainers are, and how they are added or removed. - **[Contributing →](https://openwop.dev/contributing/)** — change rules (editorial, additive, safety fix, breaking), the checks every pull request must pass, and the DCO sign-off. #### Security What's promised, what's threat-modelled, and how to report a vulnerability. - **[Security posture →](https://openwop.dev/security/)** — threat model, disclosure policy, public invariants (CTI-1, SR-1, MCP-1). - **[GitHub Security Advisories →](https://github.com/openwop/openwop/security/advisories/new)** — coordinated disclosure channel for vulnerabilities. #### Versioning and roadmap What's stable, what's planned, what's tracked but not committed. - **[Versioning policy →](https://openwop.dev/versioning/)** — additive within a major, the 90-day safety-fix window, breaking changes only in major versions. - **[Changelog →](https://github.com/openwop/openwop/blob/main/CHANGELOG.md)** — every spec, schema, SDK, and reference-host change with its compatibility classification. - **[Roadmap →](https://openwop.dev/roadmap/)** — where the protocol is heading. Direction, not promises. #### See also - [Community](https://openwop.dev/community/) — channels, comment windows, and how to file an RFC. - [Implementing OpenWOP](https://openwop.dev/implement/) — the four role-specific entry points. - [Read the paper](https://doi.org/10.5281/zenodo.20576239) — *OpenWOP: A Vendor-Neutral Protocol for Durable, Portable Agentic Workflow Orchestration* (Zenodo, CC BY 4.0): the protocol-level argument, a reproducible cross-language portability result, and the full evidence artifact. ### Tool calling, and what OpenWOP adds to it Source: https://openwop.dev/tool-calling/ OpenWOP is additive to tool calling. It defines no model-facing function-calling format and replaces neither MCP nor A2A; it specifies the things those protocols deliberately leave to the harness — who was allowed to call, what a failure looks like on the wire, and what happens the second time the same call arrives. #### What tool calling standardizes, and what it leaves open In the agent world, "tool calling" is a **model-output convention**. The model emits a name plus JSON arguments, a harness dispatches it, and the result comes back as another turn. Function calling, tool use, and the Model Context Protocol (MCP) are variations on that shape. They standardize how a tool is *described* — name, description, JSON Schema — and how a single invocation is *framed*. MCP goes further and makes the tool list a server you can connect to. What none of them specify: - Who was allowed to make the call. - What happens if the same call arrives twice. - How a failure is distinguished from a success that returned nothing. - Whether replaying a conversation re-fires the side effect. That is fine for a chat loop. It is not fine for an engine that resumes after a crash. > **OpenWOP's position:** a tool call is a durable, authorized, idempotent effect on a run's event log — not a turn in a transcript. #### Four surfaces ##### Description is a projection, not a registry OpenWOP tools already lived behind five unrelated surfaces — node-pack typeIds, workflow-as-tool (`core.subWorkflow` and chains), MCP servers, connectors, and host-extension scopes — each discoverable only through its own mechanism. An agent handed a `toolAllowlist` of opaque `:` strings had no way to resolve them. The `toolCatalog` capability unifies all five behind one `ToolDescriptor` ([`tool-descriptor.schema.json`](https://openwop.dev/spec/v2/tool-descriptor.schema.json)): | Field | Field | Field | | --- | --- | --- | | `toolId` | `source` | `title` | | `description` | `inputSchema` | `outputSchema` | | `annotations` | `auth` | `egress` | | `approval` | `replayPolicy` | `safetyTier` | | `costHint` | `latencyHint` | | `toolId`, `source`, and `safetyTier` are required; `source` is exactly the five origins above (`node-pack`, `workflow`, `mcp`, `connector`, `host-extension`). `annotations` is the projection onto MCP `ToolAnnotations` ([RFC 0204](https://openwop.dev/rfcs/0204-host-mcp-client-returns-mcp-results.html)). When present it carries all four MCP hints, derived from the host's own `safetyTier`, `replayPolicy`, and `egress` rather than from MCP's defaults. The host assigns those three fields itself and MUST NOT copy them from an MCP server's `annotations`, which are untrusted; an MCP tool it has not classified is `safetyTier: "write"`. The catalog is read-only, authorization-scoped, and **non-disclosing**: an unauthorized tool id returns **404, not 403**. The catalog will not confirm that a tool exists to someone who may not use it. Descriptors carry requirement *flags* such as `auth.credentialRef`, never credential material. Sub-keys: `sources`, `sessionLifecycle`, `compactView`. ##### Invocation is a pair of durable events Not a transcript entry. `agent.toolCalled` and `agent.toolReturned` are both in the v2 event codemap, alongside `tool.session.opened` and `tool.session.closed`. ##### Permission is fail-closed Under `toolHooks.perToolAuthorization`, the host checks the run principal's scopes **before** invoking — and if authorization cannot be evaluated, it does not invoke. It emits `agent.toolReturned { status: 'forbidden' }` and a 403. Sibling sub-keys: `prePostEvents`, `perToolRateLimit`. ##### Failure has to be legible on the wire The discriminators on `agent.toolReturned` (`error`, `outcome`, `status`) are optional in the schema, so a bare `{agentId, toolName, callId}` would be schema-valid on failure too. **A host MUST NOT report a failed tool that way:** a bare return means the tool succeeded. #### Idempotency: the part no function-calling spec addresses OpenWOP gives the **effect** — not the call — an identity. This is written out in full normative prose in [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html). | Rule | What the specification requires | | --- | --- | | Keying | Derived from the business operation, stable across every entry point, containing no `runId`, `nodeId`, or ordinal. | | Attempts | The retry counter MUST NOT participate in the identity. Two retries of one logical invocation collide rather than diverge. | | Claim | The persist that guards the effect MUST be an atomic claim — compare-and-set or insert-if-absent — that at most one executor can win. | | Provider key | Where the provider accepts an idempotency key, the host MUST inject the effect identity or a documented deterministic derivative. | | Retention | An effect record MUST be retained for at least 14 days. | It is checkable rather than asserted. A host advertising `idempotency` MUST serve `GET /runs/{runId}/effects`, each record carrying `effectId`, `nodeId`, `attempt`, `keying`, and `state`. ##### Why the atomic claim exists A chat-loop mental model breaks under recovery. An orphan sweeper re-dispatches a run whose previous owner is stalled but still alive. Both executors re-execute the node from the start, so both mint the *same* effect identity. If the invocation log is a read-then-write, both miss and both fire. A host had exactly that shape — `INSERT OR REPLACE`, which always wins and never reports a conflict — and measured **two charges where there should have been one**. The rule is worded to hold *within a single-instance deployment*. This is not a distributed-systems edge case; it is reachable on one machine. #### Composing with MCP and A2A OpenWOP does not compete on transport. REST and SSE are the wire, and a v2 host MUST NOT advertise a transport list — `supportedTransports` does not exist in the v2 capabilities schema, and a discovery document carrying it fails validation. A2A and MCP are **facets, not forks**. A host that speaks either MUST advertise the corresponding facet with every required field, per [`interop.md`](https://openwop.dev/spec/v2/core/interop.html): | Facet field | A2A | MCP | | --- | --- | --- | | Offered | `versions[]` as `major.minor` | `revisions[]` as dates | | Default | `preferredVersion`, served when the peer names none | `preferredVersion` | | Floor | `minimumVersion` | `minimumRevision` | | Freshness | `refreshedAt` | `refreshedAt` | | Profile ids | `a2a-` | `mcp-` | A negotiation that would land below the floor fails closed with `interop_version_unsupported`. There is **no legacy escape hatch**: `a2a-0.3-legacy` and `mcp-2025-06-18-legacy` do not exist in v2, and the `profiles[]` item patterns admit no `-legacy` suffix. Version negotiation is a protocol, not a bag of compatibility shims. Two more details worth keeping straight: - **MCP round ceiling.** `mcp.mrtr.maxRounds` (an integer, 1–16) is an advertised ceiling on multi-round tool-result rounds. Exceeding it fails with `mcp_mrtr_rounds_exceeded`. - **`mcp.serverMount.transports[]` is the MCP server's own transport enum** — explicitly *not* a host transport advertisement. The two get conflated easily. The catalog composes rather than reinvents. `auth.scopes` declares what the per-call authorization enforces; `egress: "safe-fetch"` advertises that a tool routes through the host's SSRF-guarded fetch; `approval` surfaces the existing approval requirement; and `safetyTier: "exec"` is an honest carve-out that MUST declare `source: "host-extension"` and MUST NOT be dressed up as protocol-tier. > The agent world standardized the envelope. OpenWOP specifies who may call, what a failure looks like, and what happens the second time the same call arrives. #### Where the normative text lives - **Effects:** [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html) in the v2 core. - **The catalog:** [`tool-catalog.md`](https://openwop.dev/spec/v2/core/tool-catalog.html) in the v2 core. - **Authorization and invocation hooks:** [RFC 0064](https://openwop.dev/rfcs/0064-tool-invocation-hooks-and-authorization.html). - **Error codes:** the [error vocabulary](https://openwop.dev/errors/). ### Implementing OpenWOP Source: https://openwop.dev/implement/ Four entry points cover every shape of OpenWOP work. Pick the role that matches what you're building — each card links to the role-specific landing with the specs, schemas, and conformance scenarios that role actually touches. #### Roles ##### [Workflow authors →](https://openwop.dev/for/workflow-authors/) You're wiring durable multi-agent runs into a product and calling a compliant host over REST + SSE. Start here if you've already picked (or had picked for you) which host you're talking to and you need to know the exact request shape, event order, and replay contract. ##### [Host implementers →](https://openwop.dev/for/host-implementers/) You're implementing the OpenWOP wire surface against your own runtime (or one your customers picked). Start here for the capability advertisement contract at `GET /.well-known/openwop`, the reference hosts in [`openwop/openwop-examples`](https://github.com/openwop/openwop-examples), and the conformance suite that gates your host's leaderboard row. ##### [Pack authors →](https://openwop.dev/for/pack-authors/) You're publishing reusable workflow components — node packs, workflow-chain packs, or agent definitions — to a registry that hosts can consume. Start here for the pack manifest format, signing requirements, and the registry submission process. ##### [Production evaluators →](https://openwop.dev/for/production-evaluators/) You're evaluating OpenWOP as a substrate for a production workflow. Start here for the production-profile claim, scale tiers, BYOK secret handling, the observability taxonomy, and the conformance evidence that backs each claim. #### Common surface Regardless of role, every implementer touches: - **The wire contract** — [`/spec/v2/`](https://openwop.dev/spec/v2/) prose specs + [REST](https://openwop.dev/api/rest/) reference + [`schemas/`](https://github.com/openwop/openwop/tree/main/schemas) JSON Schema corpus. - **Conformance** — [`@openwop/openwop-conformance`](https://github.com/openwop/openwop/tree/main/conformance) plus the live [leaderboard](https://openwop.dev/conformance/). - **RFCs** — the [in-flight](https://openwop.dev/rfcs/) protocol evolution channel; each major stays additive-only per [`COMPATIBILITY.md`](https://github.com/openwop/openwop/blob/main/COMPATIBILITY.md). - **SDKs** — TypeScript on npm, Python on PyPI, Go module — all in [`openwop/openwop-sdks`](https://github.com/openwop/openwop-sdks). #### Not sure which one you are? If you're calling somebody else's host → **workflow author**. If you're answering somebody else's call → **host implementer**. If you're shipping reusable nodes for either of the above to consume → **pack author**. If you're choosing which of the above to commit to → **production evaluator**. ### Install the white-label demo app Source: https://openwop.dev/install/ Self-host your own branded copy of the OpenWOP reference app — the same workflow-engine deployment that runs at [app.openwop.dev](https://app.openwop.dev/), re-skinnable to your product without forking core logic. This page walks the full install: download, back-end, front-end, seeding, CLI, and re-brand. The app is **two independent deploys** — a back-end (HTTP API, runs + events + host extensions) and a front-end (the React SPA). They are set up separately and the order matters at deploy time. #### Download Grab the source bundle — the full `openwop-demo-app` source (back-end + front-end + `WHITE-LABEL.md` + per-host deploy packs under `deploy/` + provider catalog), tracked files only (no `node_modules`, build output, or secrets): ##### [Download openwop-demo-app.zip →](https://github.com/openwop/openwop-app-install/releases/download/whitelabel/openwop-demo-app.zip) Verify integrity against the published checksum: ```bash ### macOS shasum -a 256 openwop-demo-app.zip ### Linux sha256sum openwop-demo-app.zip ### Compare against: curl -sL https://github.com/openwop/openwop-app-install/releases/download/whitelabel/openwop-demo-app.zip.sha256 ``` The bundle is published as a rolling GitHub release asset, rebuilt from `openwop-app` HEAD on each publish. Prefer git? `git clone https://github.com/openwop/openwop-app-install.git`. #### Choose your host The app is **one portable container** plus a small per-host deploy pack — it is **not** tied to any cloud. Storage, secret-wrapping (KMS), identity (OIDC), and object storage are all selected by environment variables, so the same image runs anywhere. The bundle ships a `deploy/` directory with a ready-made pack for each target; start at `deploy/README.md` (the choose-your-host index + the capability "host contract" every pack satisfies). | Pack | Best for | In the bundle | |---|---|---| | **Docker Compose** | laptop, VPS, on-prem, evaluation — the cloud-free default | `deploy/compose/` | | **Fly.io** | fastest self-serve cloud deploy | `deploy/fly/` | | **Render / Railway** | low-config PaaS | `deploy/render/` | | **AWS** | enterprise — ECS Fargate + RDS + Secrets Manager + KMS | `deploy/aws/` | | **Azure** | enterprise / Microsoft shops — Container Apps + PostgreSQL + Key Vault | `deploy/azure/` | | **Google Cloud** | the steward's reference deploy (`app.openwop.dev`) | `deploy/gcp/` | Not sure? The **Docker Compose** pack runs the whole stack (back-end + Postgres + SPA) with one command and no cloud account: ```bash unzip openwop-demo-app.zip && cd openwop-demo-app/deploy/compose cp .env.example .env # then add OPENWOP_SESSION_SECRET + OPENWOP_BYOK_ENCRYPTION_KEY docker compose up --build ### open http://localhost:8080 ``` The sections below cover the manual back-end/front-end setup that underlies every pack. #### Prerequisites - **Node.js 20+** and npm (or just **Docker** for the Compose pack). - A model-provider key for live AI runs (optional — the demo seeds deterministic mock-AI workflows that run with no key). - For a cloud deploy: an account on your chosen host (see *Choose your host* above). Nothing is GCP-specific — the back-end is a plain Express app and the front-end is static files, so any Node host or container runtime works. The one hard requirement is a **non-buffering edge** for `/api` so run streams (SSE) are delivered live. #### 1. Back-end ```bash unzip openwop-demo-app.zip && cd openwop-demo-app/backend/typescript npm install npm run build ### SQLite-backed local run (no external services): OPENWOP_STORAGE_DSN="sqlite://./data/workflow-engine.db" npm start ``` The API comes up on `:8080`. Confirm it's live and advertising capabilities: ```bash curl http://127.0.0.1:8080/readiness curl http://127.0.0.1:8080/.well-known/openwop ``` Without a model-provider key, `/readiness` answers **503** with `"status": "degraded"`: the managed provider reports `ready: false` and names the key it needs, while `storage` and `config` report `ok`. That is expected for a keyless local run; the discovery document answers `200` either way. Set that key and restart to clear it. Configure it with environment variables (all optional — sensible defaults apply): - `OPENWOP_STORAGE_DSN` — `sqlite://…` (default) or a Postgres DSN for multi-instance durability. - `OPENWOP_SERVICE_NAME` — your service name (surfaces in the OpenAPI discovery doc). - `OPENWOP_SERVICE_DESCRIPTION` — your one-line service description. - `OPENWOP_MANAGED_SYSTEM_PROMPT` — grounding prompt for the managed "try it free" chat tier (the code default is brand-neutral; set this to your own). For a cloud deploy, pick a pack under `deploy/` (see *Choose your host* above): each has a `README.md` with its exact steps. The Google Cloud reference recipe (Cloud Run, secrets, Cloud SQL, KMS) is the bundled `DEPLOY.md` / `deploy/gcp/`. #### 2. Front-end ```bash cd openwop-demo-app/frontend/react npm install VITE_OPENWOP_BASE_URL=/api npm run build ``` `VITE_OPENWOP_BASE_URL` points the SPA at your back-end (`/api` when proxied on the same origin, or an absolute `https://…` URL). The production build aborts if it's unset, so a broken bundle can never ship. The build output in `dist/` is static — serve it from Firebase Hosting, any CDN, or `npm run preview` locally. > **Deploy order:** back-end first, then front-end. A new SPA calls new back-end endpoints; if the front-end lands first those calls 404 until the back-end catches up. #### 3. Seeding On first use the app seeds a demo roster — ten example agents (Ava, Felix, Cleo, Idris, Mira, Ren, Pax, Quill, Iris, Ezra), each with a board, sample cards, schedules, and an org-chart position — so a first-time visitor sees the product without building anything. Seeding is idempotent and never clobbers a tenant's own edits. - **Re-brand the demo content:** edit `backend/typescript/src/host/seed-data/exampleAgents.json` (personas, roles, system prompts, cards). The shape is type-checked at build time. - **Ship a clean tenant (no demo content):** set `OPENWOP_DEMO_SEED_ENABLED=false`. See `backend/typescript/src/host/seed-data/SEEDING.md` in the bundle for the full strategy. #### 4. CLI `@openwop/cli` is the control-plane CLI for any OpenWOP-compatible host — auth, capability discovery, run submission with SSE streaming, agent dispatch, and HITL interrupt resume. Point it at your self-hosted instance: ```bash npm install -g @openwop/cli export OPENWOP_BASE_URL=https://your-host.example/api openwop doctor # verify host + profile + auth openwop runs create # submit a run, stream its events ``` Full command catalog: the bundled CLI quickstart, or `@openwop/cli` on npm. #### 5. Re-brand (white-label) Brand identity is isolated into three additive seams — no core-logic fork required. The default build renders the stock OpenWOP identity; everything below is opt-in. - **Strings + assets** — set `VITE_BRAND_*` env vars at front-end build time: `VITE_BRAND_PRODUCT_NAME`, `VITE_BRAND_MARK_PRE` / `_EMPHASIS` / `_SUB`, `VITE_BRAND_LOGO_SRC`, `VITE_BRAND_DOCUMENT_TITLE`, `VITE_BRAND_PRIMARY_DOMAIN`, and more. No code edit needed. - **Colors + typography** — override design tokens in `frontend/react/src/brand/brand.css` (it loads after the base stylesheet and wins the cascade). In most cases setting `--clay` (accent) and `--paper`/`--ink` is enough. - **Back-end strings** — `OPENWOP_SERVICE_NAME`, `OPENWOP_SERVICE_DESCRIPTION`, `OPENWOP_MANAGED_SYSTEM_PROMPT` (above). The full catalog — every `VITE_BRAND_*` var, a worked example, and the can/can't-change matrix — is in `frontend/react/WHITE-LABEL.md` in the bundle. Vendor sign-in marks (Google, GitHub) are never re-colored. #### 6. Verify ```bash ### Back-end healthy + advertising honestly curl https://your-host.example/api/readiness # → 200 (503 "degraded" without a model key) curl https://your-host.example/api/.well-known/openwop # → your capability set ### Front-end serves your brand curl -s https://your-host.example/ | grep -i "" # → your product name ``` Then open the app: the header wordmark, logo, document title, and footer should all show your brand, and the demo roster (or a clean tenant) should load. #### Next steps - [Run the quickstart](https://openwop.dev/quickstart/) — your first workflow against any host. - [Host implementers](https://openwop.dev/for/host-implementers/) — the capability + conformance contract your deployment now implements. - [Try the live reference](https://app.openwop.dev/) — the unbranded demo this bundle is built from. ### The OpenWOP CLI Source: https://openwop.dev/cli/ `@openwop/cli` is the host-agnostic control plane for any OpenWOP-compatible host — a single `openwop` command that drives the protocol from your terminal. It is a reference *client*, not an engine: every command surfaces a capability the host advertises at `/.well-known/openwop`, sends the wire shape the host expects, and renders the host's resolved view. It bundles to a single file with **zero runtime dependencies** and ships independently of the spec corpus. #### Install ```bash npm install -g @openwop/cli openwop --version ``` One-off, no install (handy in CI or a scratch shell): ```bash npx -y @openwop/cli@latest --help ``` Requires Node 22+ to install (the one-line installer ensures it; the package's `engines` floor is ≥ 20). Channel adapters for Discord and WhatsApp are optional peer dependencies — install them only if you use those channels. #### Point it at a host The CLI reads `OPENWOP_BASE_URL` (or `--base-url`) and `OPENWOP_API_KEY` (or `--api-key`). Discovery needs no credential, so against the reference deployment these work straight away: ```bash export OPENWOP_BASE_URL=https://app.openwop.dev/api openwop capabilities # read /.well-known/openwop openwop doctor # connectivity + advertised-capability check ``` Protocol calls — creating a run, listing runs — need an API key; without one the host answers `401`. To get a key for the reference deployment, sign in at [app.openwop.dev](https://app.openwop.dev/), open [Access → API keys](https://app.openwop.dev/access/api-keys), and create a key with the **CLI — act as me** preset. It covers the protocol surface and workspace commands such as `openwop agents list` and `openwop workflows register`. **Run workflows** is for scripts that only start and read runs: the host answers `403` when such a key reaches a workspace command. **Read only** cannot start a run. The key is shown once, with the two lines the CLI reads: ```bash export OPENWOP_BASE_URL=https://app.openwop.dev/api export OPENWOP_API_KEY=owk_… ``` The key is read from `--api-key`, then `OPENWOP_API_KEY`, then `openwop config set host.apiKey`. The CLI sends it in the `Authorization` header. A key acts as the account that created it, expires after 90 days by default, and can be revoked on the same screen. From CLI 1.4.0 you can skip copying a key. `openwop login` shows a short code; type it on the same Access → API keys page under "Sign in the OpenWOP CLI" and approve it. The host issues the CLI a 30-day key, saved to your config and listed in the app as `CLI: <label>`; `openwop logout` revokes it. Approve a code only if you just ran `openwop login` yourself. ```bash export OPENWOP_BASE_URL=https://app.openwop.dev/api openwop login ``` A new account starts with no workflows, so register one before you create a run: ```bash cat > hello.json <<'JSON' {"workflowId":"walk-hello","name":"Walk hello","nodes":[{"nodeId":"done","typeId":"core.noop","config":{}}],"edges":[]} JSON openwop workflows register hello.json openwop runs create walk-hello --wait ``` To run against a host of your own instead, the [quickstart](https://openwop.dev/quickstart/) starts one locally with a development key, and the [install page](https://openwop.dev/install/) covers a full deployment. `openwop onboard` saves your host URL and stores your own model-provider key (BYOK) on the host. It does not sign you in or issue an OpenWOP API key. The in-app command reference — with one-click copyable snippets, rendered against whatever host you're pointed at — lives at [app.openwop.dev/cli](https://app.openwop.dev/cli). #### How it behaves — the contract Three properties hold for every command, so the CLI is safe to script against a shared host: - **Capability-honest, fail closed.** A command drives a surface only when the host advertises it. When the host doesn't, the command fails closed with a legible message and a non-zero exit — never a stack trace, never a fabricated success. - **The host is the authority.** Policy, RBAC, toggle/variant resolution, consent, approvals, and goal/proposal completion are all decided host-side. The CLI renders the host's decision; it never computes or asserts one locally. - **Secrets are refs, never values.** BYOK keys, OAuth client secrets, SAML certs, and SCIM tokens stay host-side. The CLI handles refs and status only, and runs every response through a redactor before output — nothing secret is printed, logged, or written to local config. Meaningful exit codes make commands scriptable: `0` ok, `3` escalated / pending, `1` failed (with per-group nuances documented in `--help`). #### Command groups Point at a host and run `openwop --help` for the live list. The groups cluster by area: | Area | Groups | |---|---| | Setup & health | `onboard` · `doctor` · `capabilities` · `config` | | Run lifecycle | `runs` · `chat` · `interrupts` · `workflows` · `catalog` · `media` | | Agents & orchestration | `agents` · `roster` · `workforces` (`fleet`) · `a2a` | | Governance & safety | `approvals` · `governance` (`policy`) · `consent` · `toggles` | | Identity & access | `users` · `profiles` · `auth` (`sso`) | | Extensibility & connections | `packs` · `mcp` · `connections` (`conn`) · `providers` · `byok` | | Memory & workspace | `memory` · `workspace` · `prompts` | | Automation & messaging | `cron` · `webhooks` · `triggers` · `messaging` · `relay` · `notifications` | | Observability | `analytics` (`usage`) | #### Agent-platform capabilities Three higher-level capabilities for driving an agent platform — each capability-gated, so the group activates only when the host advertises it. ##### Reviewable learning — `proposals` An agent's learned change is stored as an **inert** proposal. It does nothing until a human reviews it and applies it through the host's activation gate (which may route through an approval). The CLI renders the host's verdict and never activates locally. ```bash openwop proposals list --state pending openwop proposals revise prop_123 --artifact-json '{"systemPrompt":"…"}' openwop proposals apply prop_123 # host materializes + routes through its activation mode ``` ##### Standing goals — `goals` A durable objective the host pursues across runs until a **judge** verdicts it satisfied or a **bound** stops it. Completion is the judge's verdict — there is no `satisfy` verb, and the CLI never declares victory from the client. A bounds-less goal is refused when the host requires bounds, so unattended continuation always terminates. ```bash openwop goals create --objective "Keep the triage backlog under 20" \ --judge verifier --continuation schedule --max-iterations 50 openwop goals pause goal_123 ``` ##### Portability — `export` / `import` Move reusable estate — agents, packs, schedules, rosters, templates — between hosts as a **refs-only** bundle. Secrets never travel as values; the bundle reports `secretsToRebind` references you bind at the destination. Always dry-run first; apply is idempotent and re-owns every entity to you. ```bash openwop export --kinds agent --kinds schedule --out estate.json openwop import estate.json --dry-run # preview creates/updates/skips/conflicts (no writes) openwop import estate.json # apply ``` ##### Trigger bridge — `triggers` Bind an external **webhook / email / form** source to a workflow, then let a verified inbound event start a run with the payload as `ctx.triggerData`. The host verifies the source, drops duplicates, and tracks each binding's state (`active`, `paused`, `failed`, `dead-lettered`). The CLI registers the subscription and shows that state. A webhook binding secret is returned **once** at creation; the CLI prints the fingerprint and never persists the value. ```bash openwop triggers register --source webhook --workflow wf_intake --dedup --verification required openwop triggers list --state active openwop triggers get tgsub_123 # exit code reflects state: 0 active · 3 paused · 1 failed ``` ##### Durable A2A tasks — `a2a` When a host advertises `a2a.durableTasks`, every backing run carries a persisted `A2ATaskState` (`taskId === runId`) that survives caller disconnect, host restart, and HITL pauses. `status` shows what the host advertises; `task` reads one durable task's live state. The record is content-free — no run inputs, outputs, artifacts, or credentials — and the CLI renders the host's projection rather than deriving a state locally. ```bash openwop a2a status openwop a2a task <taskId> # exit: 0 completed · 3 in-progress/needs-input · 1 failed ``` #### Channel relay (optional) The `messaging` and `relay` commands are a non-normative host extension: the CLI can run a local adapter that bridges a chat channel (Signal, iMessage, WhatsApp, Discord) into a workflow or agent on the host. The CLI pairs with the host over an authenticated session via a short code; channel-side auth lives only in the local adapter process and never reaches the host as a primary credential. #### Source `@openwop/cli` is [on npm](https://www.npmjs.com/package/@openwop/cli); source, docs, and the issue tracker are at [github.com/openwop/openwop-cli](https://github.com/openwop/openwop-cli). The CLI evolves additively within the current major. #### Next steps - [Install the demo app](https://openwop.dev/install/) — stand up a host for the CLI to drive. - [Host implementers](https://openwop.dev/for/host-implementers/) — the capability advertisement contract every command checks against. - [Run the quickstart](https://openwop.dev/quickstart/) — your first workflow against any host. ### Spec status policy Source: https://openwop.dev/governance/spec-status/ The four labels below are the only status tiers a spec document under `spec/v2/` may carry. The same vocabulary applies to RFC sections and capability profile claims. They are deliberately ordered from most-stable (top) to most-fluid (bottom). #### The four tiers ##### Stable The wire surface is frozen under its major's compatibility contract. Required fields, endpoint contracts, event payloads, JSON Schema shapes, and SDK package names are locked. A change to a Stable surface is a **breaking change** and SHALL be deferred to the next major. Hosts MAY advertise Stable surfaces via `/.well-known/openwop` without any profile gate. Conformance scenarios MUST be runnable against any Stable surface. ##### Stabilizing The required-field set and endpoint shapes are locked, but the document is still landing additive expansions — new optional fields, additional event-payload variants, broader behavior coverage. Implementations conforming to a Stabilizing surface SHOULD expect their conformance pass-rate to climb over time as new scenarios land, without their code needing to change. A Stabilizing surface MAY graduate to Stable in a minor release once its conformance coverage stops moving. Hosts MAY advertise Stabilizing capabilities via `/.well-known/openwop` and MUST gate them behind a named profile. ##### Draft Section headings, broad surface shape, and the high-level wire contract are stable enough to review and prototype against, but field schemas and event payloads MAY shift on each weekly RFC roll-up. Draft documents typically have an open RFC governing the comment window. Implementations against a Draft surface SHALL expect to make code changes when the surface graduates to Stabilizing or Stable. Hosts SHOULD NOT advertise Draft surfaces via `/.well-known/openwop` outside of explicit experimental opt-in profiles. ##### Experimental The document sketches an in-flight surface — direction set, but neither field shapes nor event payloads are locked. Anything inside an Experimental document MAY break across patch releases. Implementers SHOULD pin only to what is explicitly written and assume gaps everywhere else. Hosts MUST NOT advertise an Experimental capability outside of an explicit opt-in profile, and conformance scenarios for an Experimental surface SHALL run as `it.todo` until the surface graduates. #### Where the label appears Every spec document under `spec/v2/` carries a single status banner at the top of the file. It names the owning RFC(s), and most core docs follow it with the families the doc is the normative home for: ``` > **Status: <Tier> · RFC <NNNN>.** > **Normative home:** `<family>`. ``` The site shows this banner at the top of each document page and on its card in the [`/spec/v2/`](https://openwop.dev/spec/v2/) index. #### Promotion process A spec document graduates between tiers via an RFC and a CHANGELOG entry, never silently: 1. **Experimental → Draft** — landed via an open RFC that opens a comment window. Field shapes are negotiable inside the window. 2. **Draft → Stabilizing** — RFC closes; the comment window expires; required-field set and endpoint shapes lock. Document is editable additively. 3. **Stabilizing → Stable** — every required-field gap closes, conformance coverage stabilizes, and the steward declares the surface frozen for that major. Demotion from Stable inside a major requires a safety-fix RFC under [COMPATIBILITY.md §3](https://github.com/openwop/openwop/blob/main/COMPATIBILITY.md). A document MAY skip tiers (e.g., a fully-specified surface lands directly at Stable inside an existing RFC), but never silently. The CHANGELOG entry SHALL name the new tier. #### Acceptance criteria A spec document SHALL be at the **Stable** tier if and only if: - Required-field set is closed and conformance scenarios cover every required field. - Endpoint contracts are present in `api/v2/openapi.yaml`. - Event payloads are present in `api/v2/asyncapi.yaml` and `schemas/`. - The corresponding capability profile is advertisable in the `/.well-known/openwop` document without an opt-in flag. - No `it.todo` blocks against the document remain in `@openwop/openwop-conformance` outside of an explicit profile gate. A document failing any of these criteria SHALL be labelled **Stabilizing** at most. ### For workflow authors Source: https://openwop.dev/for/workflow-authors/ You're building a product that runs multi-agent workflows. You want the durable, replayable, resumable parts handled by something other than your application code, and you don't want to be locked to one runtime. This page is the shortest path from here to a running workflow on an OpenWOP-compatible host. #### What you actually need to know OpenWOP gives you a wire contract — REST endpoints to start runs, SSE streams to consume events, signed webhooks for durable delivery, and a typed run lifecycle that survives restarts. You write workflows declaratively (nodes + edges + typed channels + reducers) and submit them to any conformance-tested host. The host owns the runtime; your code owns the workflow shape. You don't need to know how the host stores its event log, how it routes provider calls, or how it implements suspend/resume. Those are host concerns gated behind capability flags you can read from `GET /.well-known/openwop`. #### Read these three spec docs first - [Run options & lifecycle](https://openwop.dev/spec/v2/core/runs.html) — what a run actually is, the per-run policy inputs, and the state machine. - [REST endpoints](https://openwop.dev/spec/v2/core/runs.html) — the canonical endpoint catalog (create, get, stream, interrupt, fork, etc.). - [Stream modes](https://openwop.dev/spec/v2/core/events.html) — `values` / `updates` / `messages` / `debug` and how to pick the right one for your UI. That's the minimum surface to author and submit a non-trivial workflow. #### Reach for these when you hit them - [Interrupts](https://openwop.dev/spec/v2/core/interrupt.html) — when your workflow needs to pause for a human (clarification, approval, quorum, external event). - [Webhooks](https://openwop.dev/spec/v2/core/webhooks.html) — when you need durable, signed delivery of run events to your backend. - [Replay](https://openwop.dev/spec/v2/core/replay.html) — when you need to fork a run from a historical checkpoint (debugging, "what if" exploration, retroactive policy change). - [Agent memory](https://openwop.dev/spec/v2/core/capabilities.html#memory) — when your supervisor agent needs to carry context across runs without leaking secrets or crossing tenant boundaries. #### Get to a running workflow The fastest path: start the [v2 reference host](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/v2-reference) (Node, about two minutes), create a run over HTTP, and watch its events stream back over SSE. The [ten-minute quickstart](https://openwop.dev/quickstart-10min/) walks the whole thing. The [quickstart](https://openwop.dev/quickstart/) then covers approvals, forks and webhooks. #### What you'll write yourself - The workflow definition (graph + channels + reducers + nodes — declarative). - The node implementations OR a manifest pointing at pre-published [node packs](https://openwop.dev/spec/v2/core/packs.html). - Your client code — a thin wrapper over the REST endpoints; the TypeScript ([`@openwop/openwop`](https://www.npmjs.com/package/@openwop/openwop)), Python ([`openwop-client`](https://pypi.org/project/openwop-client/)) and Go SDKs in [`openwop/openwop-sdks`](https://github.com/openwop/openwop-sdks) cover the canonical methods. #### What you don't write - A durable event log. - A suspend/resume mechanism. - A replay engine. - An interrupt state machine. - Provider routing and BYOK secret resolution. - HMAC-signed webhook delivery. Those are host responsibilities, defined normatively in the spec corpus and verified by the conformance suite. #### Next steps | Action | Where | |---|---| | Run the quickstart | [OpenWOP in 10 minutes](https://openwop.dev/quickstart-10min/) | | Try the live host | [app.openwop.dev](https://app.openwop.dev/) | | Read the run lifecycle spec | [/spec/v2/core/runs.html](https://openwop.dev/spec/v2/core/runs.html) | | Compare host implementations | [/conformance/](https://openwop.dev/conformance/) | | Ask a question | [GitHub Issues](https://github.com/openwop/openwop/issues) | ### For host implementers Source: https://openwop.dev/for/host-implementers/ You're implementing OpenWOP against a runtime — your own engine, a customer's stack, a managed service you're building. This page tells you what the conformance suite expects, what the advertise-honestly principle costs you, and which reference implementation to mine first. #### The contract in one sentence A host advertises a set of capabilities at `GET /.well-known/openwop`, implements the REST endpoints + SSE streams + signed webhooks for the capabilities it claims, and passes the conformance scenarios gated on those capabilities. Anything you don't claim, you don't have to implement. #### Read these first - [Capabilities](https://openwop.dev/spec/v2/core/capabilities.html) — the advertisement shape. A host that supports a feature advertises a record for it; a host that doesn't, omits it. - [Profiles](https://openwop.dev/profiles/) — the named capability bundles a host can claim. Each profile is a predicate over `/.well-known/openwop` plus a set of conformance scenarios. - [REST endpoints](https://openwop.dev/spec/v2/core/runs.html) — every endpoint with request/response shapes and the canonical error codes. #### The advertise-honestly principle If you advertise a capability, the conformance scenarios for it run against your host, and you must pass them. If you omit it, those scenarios are skipped and you owe nothing. Do **not** advertise a capability your host doesn't handle correctly. Leaving out something you can't do yet is the right posture, not a weakness. The same goes for [`production`](https://openwop.dev/spec/v2/core/capabilities.html#production): claim it only once backpressure, retention, claim acquisition and orphan recovery all hold under test. The leaderboard at [/conformance/](https://openwop.dev/conformance/) is the public record. A host that overclaims will fail in someone else's CI before it fails for an end user. #### Start from a reference host Mine [`examples/hosts/v2-reference/`](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/v2-reference) in `openwop/openwop-examples`. It was written directly from the [v2 spec](https://openwop.dev/spec/v2/) and certifies all three v2 profiles. Its `conformance.md` records which profiles it claims and its latest results. For a step-by-step path, start from [`docs/IMPLEMENT-CORE.md`](https://github.com/openwop/openwop/blob/main/docs/IMPLEMENT-CORE.md). #### Run the conformance suite The conformance suite is published as `@openwop/openwop-conformance` on npm, with its exact-pinned contract peer `@openwop/spec-artifacts` at the same version — install both together. Point it at your host: ```bash npm install --legacy-peer-deps @openwop/openwop-conformance@<version> @openwop/spec-artifacts@<version> npx @openwop/openwop-conformance --base-url https://my-host.example.com --api-key … \ --target-major 2 --require-behavior ``` `--target-major 2` runs the v2 scenarios. `--require-behavior` is strict mode: an advertised behaviour that can't be observed fails instead of being skipped. Scenarios only run for the capabilities your host advertises. The leaderboard wants a signed bundle (`--certify bundle-v3.json`) plus a `conformance.md` recording the run. #### Common implementation surfaces - Event log + suspend/resume: see [Storage adapters](https://openwop.dev/spec/v2/core/persistence.html). - BYOK secret resolution: see [Auth](https://openwop.dev/spec/v2/core/identity.html) and the [secret-leakage threat model](https://github.com/openwop/openwop/blob/main/SECURITY/threat-model-secret-leakage.md). - Signed webhooks: see [Webhooks](https://openwop.dev/spec/v2/core/webhooks.html); the HMAC recipe is `HMAC-SHA256({timestamp}.{rawBody})`. - Interrupts: see [Interrupts](https://openwop.dev/spec/v2/core/interrupt.html) for the nine v2 kinds and the shared resolve contract. - OAuth-granted credentials for connector nodes: see [OAuth](https://openwop.dev/spec/v2/core/oauth.html) — the host as an OAuth client, and the `credentials` family. #### What you owe the matrix When you ship an OpenWOP host (your own or for a customer), open a PR that: 1. Adds a row to [`INTEROP-MATRIX.md`](https://github.com/openwop/openwop/blob/main/INTEROP-MATRIX.md). 2. Adds your signed bundle under `evidence/v2-host-bundles/`, with a `conformance.md` beside it recording the suite version, command line, target URL class, and pass / fail / blocked / inapplicable / skipped counts. 3. Lists the profiles you claim and the ones you explicitly don't (`OPENWOP_OPTED_OUT_PROFILES`, so the bundle records the opt-out). The leaderboard rebuilds automatically. A claim counts as certified only when the bundle shows every required check for that profile passing, and CI re-verifies the bundle. Describing it honestly in your row is still on you. #### Next steps | Action | Where | |---|---| | Read the host capabilities spec | [/spec/v2/core/capabilities.html](https://openwop.dev/spec/v2/core/capabilities.html) | | Browse profile predicates | [/profiles/](https://openwop.dev/profiles/) | | Check the leaderboard | [/conformance/](https://openwop.dev/conformance/) | | Clone the reference host | [examples/hosts/v2-reference](https://github.com/openwop/openwop-examples/tree/main/examples/hosts/v2-reference) | | Run the conformance suite | `npx @openwop/openwop-conformance` | ### For pack authors Source: https://openwop.dev/for/pack-authors/ You're packaging reusable workflow components — node implementations, agent definitions, or chained workflow templates — for other OpenWOP users to consume. This page covers the manifest shape, the signing recipe, the namespace tiers, and how to publish to the canonical registry. #### What a pack is An OpenWOP pack is a signed tarball containing: - A `pack.json` manifest declaring the pack name, version, and the types it exports. - One or more node implementations (`runtime.language` is `javascript`, `python`, `go`, `wasm`, `wasm-component`, or `remote` — an MCP server the host calls; see [Node-pack runtimes](https://openwop.dev/spec/v2/core/node-pack-runtimes.html)). - Optional agent manifests (per [RFC 0003](https://openwop.dev/rfcs/0003-agent-packs.html)). - A detached Ed25519 signature over the RFC 8785 canonical-JSON bytes of the `pack.json` inside the tarball (the v2 `ed25519-canonical-json` scheme). Hosts consume packs at install time, verify the signature against the publisher's public key, and register the declared types in their node catalog. The host's pack-consumer reference implementation is at [`examples/hosts/postgres/src/pack-consumer.ts`](https://github.com/openwop/openwop-examples/blob/main/examples/hosts/postgres/src/pack-consumer.ts) in `openwop/openwop-examples`. It shows the install-time security check: lockfile parse, SRI integrity, version-drift detection and Ed25519 signature verification. #### Read these first - [Node packs](https://openwop.dev/spec/v2/core/packs.html) — pack format, manifest schema, dependency resolution, lockfile semantics, signing recipe. - [Workflow chain packs](https://openwop.dev/spec/v2/core/workflow-chain-packs.html) — composing reusable multi-step workflow templates (RFC 0013). - [Node-pack runtimes](https://openwop.dev/spec/v2/core/node-pack-runtimes.html) — the runtime languages a host may execute, the WASM ABI, and the MCP registry record for `remote` packs. - [Host services](https://openwop.dev/spec/v2/core/host-services.html) — the advertised `host.*` service surfaces a node pack invokes through `ctx`. - Registry operations — submission, deprecation, yank, key rotation. The registry tree and signing rules are in [Packs](https://openwop.dev/spec/v2/core/packs.html). - [`agent-manifest.schema.json`](https://github.com/openwop/openwop/blob/main/schemas/agent-manifest.schema.json) — agent pack manifest shape (RFC 0003). #### Namespace tiers Packs are partitioned into three tiers. The tier is part of the pack identifier; there's no implicit promotion. | Tier | Publishing rule | Signing root | Example | |---|---|---|---| | `core.*` | OpenWOP-maintained; canonical baseline | `openwop-team-1` or `openwop-registry-root` (both permitted for `core.openwop.*`) | `core.openwop.http` | | `vendor.*` | Named vendor; the vendor's own Ed25519 key, listed in the registry's `signingKeys[]` with `permittedNamespaces` scoped to `vendor.<org>.*` | vendor-held keypair | `vendor.myndhyve.canvas` | | `community.*` | Open publishing via PR; the publisher's own Ed25519 key, listed in the registry's `signingKeys[]` with `permittedNamespaces` scoped to the publisher's `community.<publisher>.*` names | per-publisher Ed25519 keypair | `community.openwop-team.demo` | Host-private deployments can additionally use `private.*` — these never enter the public registry. #### Sign your tarball v2 has one signing scheme (per [Packs](https://openwop.dev/spec/v2/core/packs.html) §"Signing"): `signing` on the version manifest is `{ keyId, scheme }`, and `scheme` MUST be `ed25519-canonical-json`. The recipe is: 1. Build a deterministic tarball containing your `pack.json`. 2. Compute the SRI integrity of the tarball. 3. Sign the RFC 8785 (JCS) canonical bytes of that `pack.json` with your Ed25519 private key — a detached 64-byte signature. 4. Submit the `.tgz` + the `.sig` + a version manifest referencing both, with the SRI integrity matching the tarball. A signature over the raw tarball bytes is **not** a v2 signature; a pack signed that way must be re-signed, not relabeled. A verifier checks the signature against the issuing registry's key for `keyId` and the pack name against that key's `permittedNamespaces`. A reference workflow lives at [`scripts/build-pack-tarball.mjs`](https://github.com/openwop/openwop-registry/blob/main/scripts/build-pack-tarball.mjs) in `openwop/openwop-registry` — you can mine it for the canonical command sequence. #### Publish to the canonical registry The reference registry at [`packs.openwop.dev`](https://packs.openwop.dev/) has no write API by design. Publishing is via PR against [`openwop/openwop-registry`](https://github.com/openwop/openwop-registry): 1. Fork the repo. 2. Add your pack tarball + signature + manifest under the appropriate namespace tier. 3. Add a row to the registry index via the canonical generator script. 4. Open a PR with the conventional commit prefix `feat(registry):`. The registry rebuilds on merge. Packs are served under `registry/v2/packs/<name>/-/<version>.{json,sbom.json,sig,tgz}`. Clients resolve every registry path through the `endpoints` in `.well-known/openwop-registry.json` instead of building one. #### Publish to a private registry You can stand up your own OpenWOP registry by serving the discovery doc + the pack URLs over HTTPS. The discovery document is `.well-known/openwop-registry.json`, defined at [Packs](https://openwop.dev/spec/v2/core/packs.html) §"The registry tree". Hosts that point at your registry will consume your packs identically to the canonical one. Use cases: tenant-scoped private packs, air-gapped deployments, enterprise compliance ("our packs never leave our network"). #### Common pitfalls - **Don't reuse a signing key across publishers.** Each `community.<publisher>.*` pack should use a publisher-scoped Ed25519 keypair. - **Pin your dependencies.** Pack-to-pack dependencies are resolved via lockfile; install-time integrity checks fail closed on version drift. - **Honor SR-1.** If your nodes touch secrets, follow the [secret-redaction invariant](https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml) — never put raw secrets in node outputs or event payloads. - **Bump majors honestly.** A pack version bump that changes node input/output shapes is a breaking change; downstream workflows must be able to pin to the prior major. #### Next steps | Action | Where | |---|---| | Read the node-pack spec | [/spec/v2/core/packs.html](https://openwop.dev/spec/v2/core/packs.html) | | Browse the registry | [packs.openwop.dev](https://packs.openwop.dev/) | | See an example pack | [`examples/packs/rust-hello/`](https://github.com/openwop/openwop-examples/tree/main/examples/packs/rust-hello) | | Open a publishing PR | [`openwop/openwop-registry`](https://github.com/openwop/openwop-registry) | ### For production evaluators Source: https://openwop.dev/for/production-evaluators/ You're deciding whether OpenWOP fits your durability, security, interop, and operational requirements before you bet a production system on it. This page is the honest summary of what the protocol guarantees, what the reference hosts demonstrate, and what's explicitly out of scope. #### The shortest honest pitch OpenWOP is an open wire-level protocol for multi-agent workflow orchestration. - **Version:** v2 is the current major, cut 2026-09-05. - **Evolution:** additive-only within a major. - **Evidence:** the public conformance leaderboard lists every host that has published results. All of them are run by the steward or a steward-affiliated organization. - **License:** the specification, schemas and API definitions are CC BY 4.0; the SDKs, conformance harness, CLI and tooling are Apache 2.0, with no field-of-use restrictions. If your evaluation criteria are "is this real, is it stable, can my team take it over if the steward disappears, and is it defensible against an audit" — this page is for you. #### Read these first - [Production profile](https://openwop.dev/spec/v2/core/capabilities.html#production) — the canonical predicate set for hosts that claim production posture. Operational-readiness, NOT throughput. - [Positioning & non-goals](https://openwop.dev/spec/v2/core/overview.html) — what OpenWOP is and explicitly isn't. - [Versioning & compatibility](https://openwop.dev/versioning/) — the versioning axes and the rules they enforce. - [Security posture](https://openwop.dev/security/) — threat model, disclosure policy, public invariants. - [Governance](https://openwop.dev/governance/) — how decisions get made, the sole-steward operating rules, the path to a cross-vendor working group. #### What the protocol guarantees | Guarantee | Source | How it's verified | |---|---|---| | Additive-only evolution within a major | [`COMPATIBILITY.md`](https://openwop.dev/versioning/) §2 | RFC review window; automated checks on every change | | Safety-fix disclosure window | [`COMPATIBILITY.md`](https://openwop.dev/versioning/) §3, [`SECURITY.md`](https://openwop.dev/security/) | 90-day public window OR embargoed advisory per disclosure policy | | Cross-host portability | [Profiles](https://openwop.dev/profiles/) + [Conformance](https://openwop.dev/conformance/) | Black-box scenario suite runs against every claimed host | | BYOK secret non-leakage | [`SECURITY/invariants.yaml`](https://openwop.dev/security/) SR-1 | Public conformance test on every protocol-tier host | | Cross-tenant memory isolation | [`agent-memory.md`](https://openwop.dev/spec/v2/core/capabilities.html#memory) + CTI-1 invariant | Public test; mechanically verified on the Postgres reference host | | Replay determinism | [`replay.md`](https://openwop.dev/spec/v2/core/replay.html) | Fork-from-checkpoint scenarios in the conformance suite | | Signed webhook delivery | [`webhooks.md`](https://openwop.dev/spec/v2/core/webhooks.html) | HMAC-SHA256({ts}.{body}); replay-attack-resistant verification recipe | #### What's explicitly out of scope - **Runtime SLA.** OpenWOP defines the protocol, not the host's uptime. Your host vendor (or your own host) owns that. - **Throughput claims.** The `production` capability is operational-readiness (backpressure, retention, claim acquisition) — NOT a throughput floor. A single-`pg.Client` Postgres host can satisfy it while serializing writes. - **A managed service from the OpenWOP project.** The protocol is open; the reference hosts are reference implementations. There is no hosted, paid offering from the steward at this time. See [Roadmap](https://openwop.dev/roadmap/). - **Lock-in protection across major versions.** Each major is additive within itself; breaking changes land only at a major boundary. Support past a major is a commitment your vendor would need to make. #### The leaderboard [/conformance/](https://openwop.dev/conformance/) is the public record of which hosts implement which profiles, with their results against the conformance suite. Read it cynically. Exact counts change with every run, so they live there, not here. What each host claims: - **v2 reference host** (`openwop-host-v2-reference`, in `openwop-examples`): all three v2 profiles certified. Evidence tier `self` — the steward built it, so it is not independent evidence. - **openwop-app** (`openwop-workflow-engine`, the production host at app.openwop.dev): all three v2 profiles certified. Its latest run also records a few failures on optional checks outside the certified floor; the leaderboard lists them. - **MyndHyve `workflow-runtime`**: `openwop-discovery-core` and `openwop-core-standard` certified. Run by the same organization as the steward, so it is **not** independent evidence. #### Security posture in one paragraph Every protocol-tier MUST-NOT in [`SECURITY/invariants.yaml`](https://openwop.dev/security/) has a public conformance test, and CI refuses a change that leaves one untested. New invariants land with the RFC that introduces them. The threat models (secret leakage, prompt injection, node-pack supply chain, provider policy, auth profiles) are public under [`SECURITY/`](https://github.com/openwop/openwop/tree/main/SECURITY) on GitHub. #### Governance posture OpenWOP is a single-steward project today: one maintainer, one organization. While that lasts, comment windows and the two-approval rule are waived (and each waiver is recorded), but evidence requirements never are. The project moves to a cross-vendor working group once three independent organizations each have a maintainer and a second host implementation exists outside the steward. Neither condition is met yet. If your evaluation needs multi-vendor governance today, raise it now. [Governance](https://openwop.dev/governance/) has the current state. #### Questions worth raising in your eval These are the questions a careful evaluator should ask. They're easier to answer up front than after the contract is signed. 1. Which OpenWOP host will we run? Are we self-hosting, paying a vendor, or both? 2. Which profiles does that host claim, and does the leaderboard back the claim? 3. What's our migration path if the host vendor stops supporting OpenWOP? 4. How do we get notified about safety-fix advisories? (Answer: [GitHub releases on openwop/openwop](https://github.com/openwop/openwop/releases).) 5. What's our policy for adopting a new major version when one ships? #### Next steps | Action | Where | |---|---| | Read the production profile | [/spec/v2/core/capabilities.html#production](https://openwop.dev/spec/v2/core/capabilities.html#production) | | Audit the leaderboard | [/conformance/](https://openwop.dev/conformance/) | | Review the security posture | [/security/](https://openwop.dev/security/) | | Review the versioning policy | [/versioning/](https://openwop.dev/versioning/) | | Open a question | [GitHub Issues](https://github.com/openwop/openwop/issues) | ### OpenWOP for AI agents Source: https://openwop.dev/ai-tools/ Coding agents and AI assistants can read the OpenWOP spec as plain markdown, drive any host through the CLI or plain HTTP, and follow the skills below to start runs, handle approvals and replay runs safely. Approvals stay with people: an agent prepares and reports, and a person decides. #### Give your agent the docs - **[/llms.txt](https://openwop.dev/llms.txt)** is a short summary and map of the site, in the llms.txt format. - **[/llms-full.txt](https://openwop.dev/llms-full.txt)** is the v2 specification, guides and comparisons in one markdown file. Paste it, or point the agent at it. - **Every page has a markdown copy** at its URL with `.md` appended (`index.html.md` for a URL ending in `/`). For example, [runs.html.md](https://openwop.dev/spec/v2/core/runs.html.md). - **Machine-readable contracts:** the [REST API](https://openwop.dev/api/rest/) (OpenAPI) and [event stream](https://openwop.dev/api/events/) (AsyncAPI), and JSON schemas at their canonical `$id`. #### Let your agent act on a host An agent talks to a host over REST and Server-Sent Events, or through the CLI: ```bash npm install -g @openwop/cli export OPENWOP_BASE_URL=https://app.openwop.dev/api openwop capabilities # what the host advertises openwop doctor # connectivity and advertised-capability check ``` Every v2 request sends `OpenWOP-Version: 2`. Calls other than discovery need an API key in the `Authorization` header; see [the CLI](https://openwop.dev/cli/) for keys and scopes. Give an agent the narrowest key that does the job: a read-only key cannot start a run. #### Skills Each skill is written for an agent to follow. Copy the ones you need into your agent's instructions, or point it at this page's [markdown copy](https://openwop.dev/ai-tools/index.html.md). ##### Skill: discover a host before acting **Use when** you are about to call an OpenWOP host you have not inspected in this session. 1. `GET /.well-known/openwop` (no credentials). Read `protocolVersions`, `preferredVersion` and the advertised capabilities. 2. If the host does not list `2.0`, stop and report: v2 calls will fail. 3. Before using an optional feature (replay, approvals routing, triggers, extensions), confirm the host advertises it. If it does not, tell the user instead of trying the call. **Never** guess a capability from a host's name or from another host. Each host advertises its own. ##### Skill: start a run and stream its events **Use when** the user asks to run a registered workflow. 1. Discover the host (skill above). 2. `POST /runs` with `{"workflowId": "…", "inputs": {…}}`, the `OpenWOP-Version: 2` header and an `Idempotency-Key` you generate once. A retry with the same key never starts a second run. 3. Read `runId` from the response, then stream `GET /runs/{runId}/events` (Server-Sent Events) or poll `GET /runs/{runId}/events/poll`. 4. Report progress from the events (`run.started`, `node.started`, `agent.decided`, `interrupt.requested`, `run.completed`, `run.failed`), not from guesses. 5. **Done** when a terminal event arrives. Report the outcome and the `runId`. **Never** put secret values in `inputs`. Hosts reference credentials by name (bring your own key); the values stay on the host. ##### Skill: handle a human approval **Use when** a run emits `interrupt.requested` with an approval kind. 1. Show the person what is being asked: the payload's question, context and the allowed `actions` (a subset of `accept`, `reject`, `refine`, `edit-accept`, `ask`). 2. **Wait for the person's decision.** Do not choose an action yourself unless the person has explicitly told you to, for this run. 3. Resolve with `POST /runs/{runId}/interrupts/{nodeId}` (or `POST /interrupts/{token}` when you were given a token), body `{"resumeValue": {…}}` carrying the chosen `action`, its required field (`refineFeedback` for refine, `editedArtifactData` for edit-accept) and `decidedAt`. Send an `Idempotency-Key`. 4. A `409 interrupt_already_resolved` means someone else decided first, or the run ended. Read the run's events and report; do not retry. 5. **Done** when the stream shows `interrupt.resolved` and then `run.resumed`. ##### Skill: fork or replay a run **Use when** the user wants to see a past run play out again, or try a different path from an earlier step. 1. Confirm the host advertises `replay` and which `modes` it supports (`replay`, `branch`). 2. `POST /runs/{runId}:fork` with `{"mode": "replay" | "branch", "fromSeq": n}`. Events before `fromSeq` are fixed history; events from `fromSeq` on run again. 3. In `replay` mode the host MUST suppress side effects that were already performed, so a replay never pays an invoice twice. In `branch` mode the new run is independent and may act for real: **confirm with the person before starting a branch** that could take external actions. 4. `422 fork_point_invalid` or `400` means `fromSeq` is out of range. Read the source run's events to pick a valid step. 5. **Done** when the fork's own run completes. Report both `runId`s. ##### Skill: check a host with the conformance suite **Use when** the user is building or evaluating a host and asks whether it complies. 1. Install the suite and its contract package at the same version, as [the quickstart](https://openwop.dev/quickstart/#check-a-host-with-the-conformance-suite) shows. 2. Run `npx openwop-conformance --base-url "$OPENWOP_BASE_URL" --target-major 2`. 3. Report the counts as the suite prints them: pass, fail, blocked, inapplicable and skipped. `inapplicable` means the host does not advertise that optional surface; it is not a failure. 4. Never describe a host as conformant to a profile unless the bundle shows every floor requirement of that profile witnessed with nothing blocked. See [conformance](https://openwop.dev/conformance/). #### Rules for any agent working with OpenWOP - Inspect before you change: discovery and reads first, then writes. - Send an `Idempotency-Key` on every mutating call. - Leave approvals to people unless told otherwise, for that run. - Never send, log or echo secret values; refer to credentials by name. - Report what the event log says happened. Route on error codes, never on error messages ([errors](https://openwop.dev/spec/v2/core/errors.html)). - Prefer `replay` to `branch` when the goal is to look, not to act. #### When OpenWOP is not the right tool If you are building a single agent application, a framework such as LangGraph or CrewAI is less overhead. For production durable execution today, Temporal is more mature. For visual automation with many integrations, n8n fits better. OpenWOP is new, has one steward, and no independent organisation has implemented a host yet. See [the comparisons](https://openwop.dev/comparisons/). ## Comparisons ### OpenWOP alternatives and comparisons Source: https://openwop.dev/comparisons/ OpenWOP is an open protocol for multi-agent workflow orchestration, so it sits beside agent frameworks, durable-execution engines and automation platforms rather than replacing them. Pick a framework to write agents, Temporal for production durability, and n8n for visual automation; pick OpenWOP when you need one contract for how runs start, stream, wait for approval and replay across hosts. Checked 4 October 2026. Every comparison links its sources and says where the other tool is the better choice. #### Side by side | Tool | What it is | Licence | Best at | |---|---|---|---| | OpenWOP | Wire protocol plus conformance suite | CC BY 4.0 spec, Apache 2.0 code | A portable run contract: approvals, event log, fork and replay; new, with no independent host yet | | [LangGraph](https://openwop.dev/comparisons/langgraph/) | Agent framework plus hosted runtime | MIT library, Elastic-2.0 server | Building stateful agents in Python or JavaScript | | [CrewAI](https://openwop.dev/comparisons/crewai/) | Multi-agent framework plus platform | MIT | Fast role-based multi-agent apps in Python | | [AutoGen, Agent Framework, AG2](https://openwop.dev/comparisons/autogen/) | Agent frameworks | MIT, MIT, Apache 2.0 | .NET and Azure agents; community multi-agent in Python | | [Temporal](https://openwop.dev/comparisons/temporal/) | Durable-execution engine | MIT | Production durable execution in any domain | | [n8n](https://openwop.dev/comparisons/n8n/) | Low-code automation platform | Sustainable Use (fair-code) | Visual automation with thousands of integrations | #### Protocols it works with - [A2A, MCP and OpenWOP](https://openwop.dev/comparisons/a2a-openwop-mcp/): MCP connects an agent to tools, A2A lets agents delegate to each other, and OpenWOP runs the workflow they take part in. - [OpenExO 3.0 and OpenWOP](https://openwop.dev/comparisons/openexo-3-openwop/): how an organisational framework maps onto the protocol. #### Something out of date? These pages describe other projects as of the date above. If a fact has changed, [open an issue](https://github.com/openwop/openwop/issues) and we will correct it. ### OpenWOP vs LangGraph Source: https://openwop.dev/comparisons/langgraph/ LangGraph is a library and runtime you write stateful agents in; OpenWOP is an open protocol that defines how a host exposes a durable, interruptible, replayable run of whatever agents it hosts. They solve different problems, and for building one agent application in Python or JavaScript today, LangGraph is the more mature choice. Checked 4 October 2026. Figures for LangGraph come from its own docs, repository and package registries. #### At a glance | Property | OpenWOP | LangGraph | |---|---|---| | What it is | A wire protocol (REST and Server-Sent Events) plus a conformance suite | A library plus a server runtime ([docs](https://docs.langchain.com/oss/python/langgraph/overview)) | | Licence | Spec CC BY 4.0, code Apache 2.0 | Library MIT; the Agent Server package is Elastic-2.0, source-available ([PyPI](https://pypi.org/pypi/langgraph-api/json)) | | Languages | Any language that speaks HTTP; SDKs for TypeScript, Python and Go | Python and JavaScript/TypeScript | | Where state lives | An append-only event log per run ([events](https://openwop.dev/spec/v2/core/events.html)) | Checkpoints per thread ([persistence](https://docs.langchain.com/oss/python/langgraph/persistence)) | | Human approval | Interrupts with an approval vocabulary, resumed by token ([interrupt](https://openwop.dev/spec/v2/core/interrupt.html)) | `interrupt()` and `Command(resume=…)` ([docs](https://docs.langchain.com/oss/python/langgraph/interrupts)) | | Replay and fork | Fork any run from any event; a host that advertises replay must not repeat side effects ([replay](https://openwop.dev/spec/v2/core/replay.html)) | Replay and fork from a checkpoint ([time travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel)) | | Spec other runtimes implement | Yes, with a public conformance suite; three v2 hosts so far, all run by the project or a sibling organisation | Agent Protocol (OpenAPI); every listed implementation is LangChain's own ([repo](https://github.com/langchain-ai/agent-protocol)) | | Hosted option | A reference app at [app.openwop.dev](https://app.openwop.dev/) | LangSmith Deployment ([docs](https://docs.langchain.com/langsmith/deployments)) | | Adoption | New: the spec repo started in May 2026 | About 42,700 GitHub stars and 44 million PyPI downloads in the last month ([pypistats](https://pypistats.org/api/packages/langgraph/recent)) | #### When to choose LangGraph - You are building one agent application in Python or JavaScript and want to write the agent logic in a framework. - You want a widely used tool with a large community, a debugger with time travel (LangSmith Studio) and built-in evaluation. - You want to embed orchestration in your own process with no separate server. #### When OpenWOP fits - You need several hosts, or several vendors' tools, to agree on what a run is: how it starts, streams, waits for a person, and replays. - You want approval, fork and replay rules written into a contract that a conformance suite checks, rather than defined by one library's behaviour. - You are building a runtime yourself and want clients to work against it without custom integration. Be clear about OpenWOP's stage: it is months old, has one steward, and no independent organisation has implemented a host yet. #### Using them together An OpenWOP node can run any code, so a LangGraph agent could run inside a workflow step, and an OpenWOP host could call a deployed LangGraph agent over A2A, which both support. Neither integration has been built or tested yet. #### Common questions ##### Is OpenWOP a LangGraph alternative? Only partly. LangGraph is where you write agents; OpenWOP is the contract a host exposes for running them, so you could write a step's agent logic with LangGraph or anything else. ##### Doesn't Agent Protocol already standardise agent runs? It standardises runs, threads and a store over OpenAPI, and LangGraph implements a superset. OpenWOP also specifies interrupts, fork semantics with side-effect suppression and a conformance suite, though it also has no independent host yet. ##### Can LangGraph time travel do what OpenWOP fork does? Both let you go back to an earlier point and run again. OpenWOP's distinguishing rule is that a replay must not perform external side effects a second time; we found no equivalent documented guarantee in LangGraph as of this check. #### Sources - [LangGraph overview](https://docs.langchain.com/oss/python/langgraph/overview), [persistence](https://docs.langchain.com/oss/python/langgraph/persistence), [interrupts](https://docs.langchain.com/oss/python/langgraph/interrupts), [time travel](https://docs.langchain.com/oss/python/langgraph/use-time-travel) - [LangGraph licence](https://github.com/langchain-ai/langgraph/blob/main/LICENSE), [langgraph-api on PyPI](https://pypi.org/pypi/langgraph-api/json) - [Agent Protocol](https://github.com/langchain-ai/agent-protocol), [LangSmith Deployment](https://docs.langchain.com/langsmith/deployments) - OpenWOP: [runs](https://openwop.dev/spec/v2/core/runs.html), [events](https://openwop.dev/spec/v2/core/events.html), [interrupt](https://openwop.dev/spec/v2/core/interrupt.html), [replay](https://openwop.dev/spec/v2/core/replay.html), [conformance](https://openwop.dev/conformance/) ### OpenWOP vs CrewAI Source: https://openwop.dev/comparisons/crewai/ CrewAI is a Python framework for describing teams of role-playing agents; OpenWOP is an open protocol that defines how a host exposes a durable, interruptible, replayable run of whatever agents it hosts. If you want a multi-agent prototype in Python quickly, with a large community and a managed platform, CrewAI is the better fit. Checked 4 October 2026. Figures for CrewAI come from its own docs, repository and pricing page. #### At a glance | Property | OpenWOP | CrewAI | |---|---|---| | What it is | A wire protocol (REST and Server-Sent Events) plus a conformance suite | A Python framework plus a commercial platform, CrewAI AMP ([docs](https://docs.crewai.com/en/introduction)) | | Licence | Spec CC BY 4.0, code Apache 2.0 | MIT ([licence](https://raw.githubusercontent.com/crewAIInc/crewAI/main/LICENSE)) | | Languages | Any language that speaks HTTP; SDKs for TypeScript, Python and Go | Python ([PyPI](https://pypi.org/pypi/crewai/json)) | | Where state lives | An append-only event log per run ([events](https://openwop.dev/spec/v2/core/events.html)) | Flow state, persisted to SQLite with `@persist` ([flows](https://docs.crewai.com/en/concepts/flows)) | | Human approval | Interrupts resumed by token ([interrupt](https://openwop.dev/spec/v2/core/interrupt.html)) | `human_input`, `@human_feedback`, and approvals by email or webhook on AMP ([docs](https://docs-platform.crewai.com/platform/en/guides/human-in-the-loop)) | | Replay | Fork any run from any event ([replay](https://openwop.dev/spec/v2/core/replay.html)) | Replay from a chosen task of the latest kickoff ([docs](https://docs.crewai.com/en/learn/replay-tasks-from-latest-crew-kickoff)) | | A2A and MCP | Both, negotiated per host ([interop](https://openwop.dev/spec/v2/core/interop.html)) | A2A client and server, MCP client ([A2A](https://docs.crewai.com/en/learn/a2a-agent-delegation), [MCP](https://docs.crewai.com/en/mcp/overview)) | | Spec other runtimes implement | Yes, with a public conformance suite | We found none | | Adoption | New: the spec repo started in May 2026 | About 59,300 GitHub stars | #### When to choose CrewAI - You want to describe agents by role, goal and task in Python and get something running fast. - You want a managed platform with a visual editor, tracing and approvals out of the box. - You value a large community and plenty of examples. #### When OpenWOP fits - You need runs to behave the same across hosts or vendors, with approval, replay and fork defined by a contract. - You are building a runtime and want any compatible client to work with it. - You want a conformance suite to check that a host does what it claims. OpenWOP is months old, has one steward, and no independent organisation has implemented a host yet. #### Using them together A workflow step can run Python code, so a crew could run inside an OpenWOP node, and an OpenWOP host could call a CrewAI agent over A2A, which both support. No packaged integration exists today. #### Common questions ##### Is OpenWOP a CrewAI alternative? Not directly. CrewAI is how you describe a team of agents in Python; OpenWOP is how a server exposes a run of whatever agents it hosts. ##### Does CrewAI have replay? Yes, from a chosen task of the latest kickoff, and Flows can resume or fork from persisted state. OpenWOP specifies fork from any event in a run's log, and a host that advertises replay must not repeat side effects. #### Sources - [CrewAI introduction](https://docs.crewai.com/en/introduction), [flows](https://docs.crewai.com/en/concepts/flows), [replay](https://docs.crewai.com/en/learn/replay-tasks-from-latest-crew-kickoff), [A2A](https://docs.crewai.com/en/learn/a2a-agent-delegation), [MCP](https://docs.crewai.com/en/mcp/overview) - [AMP human-in-the-loop](https://docs-platform.crewai.com/platform/en/guides/human-in-the-loop), [pricing](https://www.crewai.com/pricing), [licence](https://raw.githubusercontent.com/crewAIInc/crewAI/main/LICENSE) - OpenWOP: [events](https://openwop.dev/spec/v2/core/events.html), [interrupt](https://openwop.dev/spec/v2/core/interrupt.html), [replay](https://openwop.dev/spec/v2/core/replay.html), [interop](https://openwop.dev/spec/v2/core/interop.html), [conformance](https://openwop.dev/conformance/) ### OpenWOP vs AutoGen, Microsoft Agent Framework and AG2 Source: https://openwop.dev/comparisons/autogen/ AutoGen, its successor Microsoft Agent Framework, and the community fork AG2 are frameworks you write agent conversations in; OpenWOP is an open protocol that defines how a host exposes a durable run of whatever agents it hosts. If you build on .NET or Azure, Microsoft Agent Framework is the better choice for writing agents. Checked 4 October 2026. Figures come from each project's own README, docs and package registries. #### Which project is which - **Microsoft AutoGen** is in maintenance mode. Its README says it "will not receive new features or enhancements" and points new users to Microsoft Agent Framework ([README](https://github.com/microsoft/autogen/blob/main/README.md)). - **Microsoft Agent Framework** is the successor to both AutoGen and Semantic Kernel, for .NET and Python; 1.0 shipped in April 2026 ([overview](https://learn.microsoft.com/en-us/agent-framework/overview/)). - **AG2** is a community fork of AutoGen 0.2, Apache 2.0; its v1.0 (July 2026) was a rewrite ([repository](https://github.com/ag2ai/ag2)). #### At a glance | Property | OpenWOP | Microsoft Agent Framework | |---|---|---| | What it is | A wire protocol plus a conformance suite | An SDK with an in-process workflow runtime ([overview](https://learn.microsoft.com/en-us/agent-framework/overview/)) | | Licence | Spec CC BY 4.0, code Apache 2.0 | MIT (AG2: Apache 2.0) | | Languages | Any language that speaks HTTP; SDKs for TypeScript, Python and Go | .NET and Python; Go in preview (AG2: Python) | | Where state lives | An append-only event log per run ([events](https://openwop.dev/spec/v2/core/events.html)) | Checkpoints after each step; restoring needs the same workflow shape ([checkpoints](https://learn.microsoft.com/en-us/agent-framework/workflows/checkpoints)) | | Human approval | Interrupts resumed by token ([interrupt](https://openwop.dev/spec/v2/core/interrupt.html)) | Request ports and tool-approval requests ([docs](https://learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop)) | | Replay and fork | Fork any run from any event; replay must not repeat side effects on hosts that advertise it ([replay](https://openwop.dev/spec/v2/core/replay.html)) | Resume from a checkpoint | | Interop | A public wire spec and conformance suite | A2A, MCP and AG-UI; no wire spec of its own ([integrations](https://learn.microsoft.com/en-us/agent-framework/integrations/)) | | Hosting | Reference app at [app.openwop.dev](https://app.openwop.dev/) | Microsoft Foundry hosted agents | | Maturity | New: the spec repo started in May 2026 | Generally available since April 2026, about 13,900 GitHub stars | #### When to choose Agent Framework or AG2 - You want Microsoft backing, .NET as a first-class language, and Azure hosting (Agent Framework). - You want an actively developed community project with broad protocol support in Python (AG2). - You are writing the agents themselves; OpenWOP gives you nothing to write agents in. #### When OpenWOP fits - You need a run's lifecycle, approvals and replay to be the same across independent hosts, checked by a conformance suite. - You are building a host or platform and want clients to work with it without custom integration. OpenWOP is months old, has one steward, and no independent organisation has implemented a host yet. #### Using them together An OpenWOP host could call an Agent Framework or AG2 agent over A2A, which all three support, or run one inside a workflow step. Neither has been built or tested yet. #### Common questions ##### Is AutoGen still maintained? Microsoft's AutoGen is in maintenance mode and Microsoft points new users to Agent Framework. AG2 is a separate, community-run fork that is still actively developed. ##### Why not just use Agent Framework checkpoints? If you run one Agent Framework application, they are enough. They are tied to its internal format and workflow shape, whereas OpenWOP's run surface is meant to be the same across independent hosts. #### Sources - [AutoGen README](https://github.com/microsoft/autogen/blob/main/README.md), [Agent Framework overview](https://learn.microsoft.com/en-us/agent-framework/overview/), [checkpoints](https://learn.microsoft.com/en-us/agent-framework/workflows/checkpoints), [human-in-the-loop](https://learn.microsoft.com/en-us/agent-framework/workflows/human-in-the-loop), [integrations](https://learn.microsoft.com/en-us/agent-framework/integrations/), [1.0 announcement](https://devblogs.microsoft.com/agent-framework/microsoft-agent-framework-version-1-0/) - [AG2 repository](https://github.com/ag2ai/ag2), [AG2 v1.0 release](https://github.com/ag2ai/ag2/releases/tag/v1.0.0) - OpenWOP: [events](https://openwop.dev/spec/v2/core/events.html), [interrupt](https://openwop.dev/spec/v2/core/interrupt.html), [replay](https://openwop.dev/spec/v2/core/replay.html), [conformance](https://openwop.dev/conformance/) ### OpenWOP vs Temporal Source: https://openwop.dev/comparisons/temporal/ Temporal is a durable-execution engine that runs your workflow code reliably; OpenWOP is an open protocol for AI-workflow runs that a host exposes, and a host could be built on Temporal. For production durable execution today, choose Temporal: it has about a decade of hardening, and OpenWOP's own positioning says so. Checked 4 October 2026. Figures for Temporal come from its own docs, repository and company pages. #### At a glance | Property | OpenWOP | Temporal | |---|---|---| | What it is | A protocol for AI-workflow runs plus a conformance suite | A durable-execution runtime ([docs](https://docs.temporal.io/temporal)) | | Licence | Spec CC BY 4.0, code Apache 2.0 | MIT ([licence](https://github.com/temporalio/temporal/blob/main/LICENSE)) | | Languages | Any language that speaks HTTP; SDKs for TypeScript, Python and Go | Eight official SDKs ([SDKs](https://docs.temporal.io/encyclopedia/temporal-sdks)) | | History | An append-only event log in a shared, closed vocabulary ([events](https://openwop.dev/spec/v2/core/events.html)) | An append-only Event History of Temporal-specific events ([docs](https://docs.temporal.io/workflow-execution/event)) | | Human approval | A first-class approval interrupt ([interrupt](https://openwop.dev/spec/v2/core/interrupt.html)) | Built with Signals or Updates ([docs](https://docs.temporal.io/encyclopedia/workflow-message-passing)) | | Replay and fork | Fork as a new run, in replay or branch mode ([replay](https://openwop.dev/spec/v2/core/replay.html)) | Deterministic replay for recovery; Reset ends the old run and starts a new one | | Wire spec | REST and Server-Sent Events, OpenAPI and AsyncAPI, conformance suite | gRPC protos ([temporalio/api](https://github.com/temporalio/api)); we found no third-party server | | Maturity | New: the spec repo started in May 2026; no independent host | More than 2,900 customers ([about](https://temporal.io/about)) | | Managed option | A reference app | Temporal Cloud ([pricing](https://temporal.io/pricing)) | #### When to choose Temporal - You need durable execution in production now, in any domain, with timers, retries and task queues at scale. - You want a mature managed service and SDKs in eight languages. - You are comfortable writing approval and agent patterns yourself on top of Signals and Activities, or using Temporal's agent SDK integrations. #### When OpenWOP fits - You want AI-workflow concepts (approval interrupts, agent events, fork without repeating side effects) as a portable wire contract rather than code patterns. - You want clients and tools that work against any compatible host, checked by a conformance suite. OpenWOP borrowed per-run version pinning from Temporal. It is months old, has one steward, and no independent organisation has implemented a host yet. #### Using them together The spec describes how an OpenWOP host could be built on Temporal: a run maps to a workflow, an interrupt's resolution to a signal, and a step to an activity. Nobody has built such a host yet. #### Common questions ##### Is OpenWOP a Temporal alternative? No. Temporal is an engine that runs your code durably; OpenWOP is a contract a host exposes, which could itself run on Temporal. ##### Doesn't Temporal already do replay? Yes. Temporal's replay rebuilds workflow state for recovery and testing. OpenWOP's fork creates a new run from any event, and a host that advertises replay must not perform side effects a second time. #### Sources - [What is Temporal](https://docs.temporal.io/temporal), [Event History](https://docs.temporal.io/workflow-execution/event), [message passing](https://docs.temporal.io/encyclopedia/workflow-message-passing), [SDKs](https://docs.temporal.io/encyclopedia/temporal-sdks) - [Temporal licence](https://github.com/temporalio/temporal/blob/main/LICENSE), [API protos](https://github.com/temporalio/api), [about](https://temporal.io/about), [pricing](https://temporal.io/pricing) - OpenWOP: [events](https://openwop.dev/spec/v2/core/events.html), [interrupt](https://openwop.dev/spec/v2/core/interrupt.html), [replay](https://openwop.dev/spec/v2/core/replay.html), [conformance](https://openwop.dev/conformance/) ### OpenWOP vs n8n Source: https://openwop.dev/comparisons/n8n/ n8n is a visual, low-code automation platform with thousands of integrations; OpenWOP is an open protocol for developers who build or call workflow hosts. For most automation needs, and for anyone who wants to build workflows on a canvas, n8n is the better choice. Checked 4 October 2026. Figures for n8n come from its own docs, licence, repository and blog. #### At a glance | Property | OpenWOP | n8n | |---|---|---| | What it is | A wire protocol plus a conformance suite | A low-code workflow automation platform with AI features ([docs](https://docs.n8n.io/welcome.md)) | | Licence | Spec CC BY 4.0, code Apache 2.0 (OSI-approved) | Sustainable Use License, "fair-code"; n8n says "we do not call ourselves open source" ([FAQ](https://docs.n8n.io/n8n-community-license/community-license/license-faq.md)) | | Who it's for | Developers building hosts and clients | Builders on a visual canvas, plus code nodes | | Integrations | Signed packs; 44 in the reference app | 2,285 listed on its [integrations page](https://n8n.io/integrations/) | | Where state lives | An append-only event log per run ([events](https://openwop.dev/spec/v2/core/events.html)) | Execution data in SQLite or Postgres | | Human approval | Approval interrupts ([interrupt](https://openwop.dev/spec/v2/core/interrupt.html)) | Send-and-wait approvals and human review of AI tool calls ([docs](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/approvals.md)) | | Replay | Fork without repeating side effects on hosts that advertise replay ([replay](https://openwop.dev/spec/v2/core/replay.html)) | Retry executions and debug past runs in the editor ([docs](https://docs.n8n.io/build/understand-workflows/understand-executions/debug-executions.md)) | | Portability | One run API across compatible hosts | Workflow JSON that runs on n8n | | Adoption | New: the spec repo started in May 2026 | About 206,000 GitHub stars | #### When to choose n8n - You want to automate business processes visually, connecting SaaS tools without writing a service. - You need a large integration catalogue and templates now. - Non-developers on your team will build and maintain the workflows. #### When OpenWOP fits - You are a developer building an agent platform or runtime and want a defined run contract: approvals, event log, replay and conformance. - You need the same run behaviour across hosts, not inside one product. OpenWOP is months old, has one steward, and no independent organisation has implemented a host yet. #### Using them together An OpenWOP step could call an n8n workflow through its webhook trigger, and an n8n HTTP node could start an OpenWOP run; both also support MCP. Nothing has been built yet. #### Common questions ##### Is OpenWOP an n8n alternative? Not for most n8n users. n8n is a product for building automations visually; OpenWOP is a protocol for developers who build or call workflow hosts. ##### Is n8n open source? n8n describes itself as fair-code under the Sustainable Use License, not open source. OpenWOP's code is Apache 2.0 and its spec is CC BY 4.0. #### Sources - [n8n overview](https://docs.n8n.io/welcome.md), [licence FAQ](https://docs.n8n.io/n8n-community-license/community-license/license-faq.md), [licence](https://github.com/n8n-io/n8n/blob/master/LICENSE.md), [integrations](https://n8n.io/integrations/) - [Approvals](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.slack/approvals.md), [debugging executions](https://docs.n8n.io/build/understand-workflows/understand-executions/debug-executions.md) - OpenWOP: [events](https://openwop.dev/spec/v2/core/events.html), [interrupt](https://openwop.dev/spec/v2/core/interrupt.html), [replay](https://openwop.dev/spec/v2/core/replay.html), [packs](https://openwop.dev/spec/v2/core/packs.html) ### A2A vs MCP vs OpenWOP Source: https://openwop.dev/comparisons/a2a-openwop-mcp/ A specification-level comparison of three complementary agentic AI protocols: inter-agent collaboration, durable workflow orchestration, and tool/context integration. > Prepared 2026-06-05. Last reviewed 2026-09-28 against A2A `1.0.1` (the latest release; a2a-protocol.org still labels its specification `1.0.0`), MCP revision `2026-07-28` with its Tasks extension, and the OpenWOP v2 corpus at `v2.43.0` (whose A2A operation map pins upstream `1.0.1`, negotiated as `1.0`). #### Executive Summary **A2A**, or Agent2Agent, is an open standard for communication and interoperability between independent, potentially opaque AI agent systems. The official A2A specification says its primary goals include capability discovery, modality negotiation, collaborative task management, and secure information exchange without requiring agents to expose internal state, memory, or tools. **MCP**, or Model Context Protocol, standardizes how AI applications connect to external tools, resources, prompts, and contextual systems. Its official architecture documentation defines an MCP Host, MCP Client, and MCP Server model, with a JSON-RPC data layer and transports such as stdio and Streamable HTTP. Since revision `2026-07-28` the protocol is stateless: there is no `initialize` handshake and no session. **OpenWOP**, or Open Workflow Orchestration Protocol, is an open, vendor-neutral wire protocol for durable multi-agent workflow orchestration. Its documentation describes a system where clients start workflow runs, stream run events, pause for human checkpoints, replay history, use signed webhooks, and preserve portability across compliant hosts. > **Most important architectural takeaway:** these are not direct substitutes. A2A is the horizontal agent-to-agent layer. MCP is the vertical agent-to-tool/context layer. OpenWOP is the durable workflow-control layer that can orchestrate agents, tools, humans, events, and replayable state. | Question | A2A | MCP | OpenWOP | | --- | --- | --- | --- | | Primary purpose | Agent-to-agent collaboration | Connect AI apps to tools, data, prompts, and context | Durable multi-agent workflow orchestration | | Core unit | Task | Tool/resource/prompt interaction over an MCP session | Run | | Main discovery object | Agent Card | `server/discover` plus primitive lists such as `tools/list`, `resources/list`, and `prompts/list` | Host capability document and workflow catalog | | Best mental model | “Ask another agent to do something.” | “Give the model/app safe, structured access to external capabilities.” | “Run and observe a durable workflow.” | | Primary boundary | Between independent agents | Between AI host application and context/tool servers | Between workflow client and workflow host | | Internal execution visibility | Intentionally opaque | Visible at tool/resource/prompt-call level, not full workflow orchestration | First-class: events, traces, replay, artifacts, checkpoints | #### Protocol Purpose and Scope ##### A2A: inter-agent collaboration A2A solves the problem of independent agents discovering each other, exchanging messages, delegating work, and reporting task progress across framework, vendor, and language boundaries. Its target users are agent platforms, agent marketplaces, enterprise automation systems, and specialist agents that need to collaborate without exposing internals. ##### MCP: tool and context integration MCP solves the problem of connecting AI applications to external context providers and action surfaces through a shared protocol for tools, resources, prompts, notifications, and elicitation. Revision `2026-07-28` deprecates sampling, roots, and logging; they keep working through a deprecation window of at least twelve months. Its target users are AI app developers, tool providers, IDEs, data platforms, SaaS integrations, local automations, and agent frameworks that need a standardized integration surface. ##### OpenWOP: durable workflow control OpenWOP solves the problem of portable AI workflow execution: how to start, stream, interrupt, resume, replay, observe, and validate long-running multi-agent workflows. Its target users are workflow authors, host implementers, pack authors, debuggers, evaluators, and enterprise platforms that need durable orchestration semantics. ##### Assumptions about the surrounding ecosystem | Protocol | Assumptions | | --- | --- | | A2A | Multiple autonomous or semi-autonomous agents exist behind endpoints; callers should not need to know the remote agent's memory, plans, tools, or implementation. | | MCP | AI applications need access to changing external context and actions; tool/resource providers should expose capabilities through a common client-server protocol rather than bespoke integrations. | | OpenWOP | AI applications increasingly need durable, replayable, observable, multi-step workflows; workflow hosts should expose a common wire contract even if runtimes differ. | #### Architectural Models ##### A2A architecture A2A uses a remote-agent model. An A2A client discovers an A2A server through an Agent Card, sends a message, receives either a direct message or a task, and then follows task status, artifacts, streaming events, or push notifications. ``` A2A Client / Caller | | Send Message v A2A Server / Remote Agent | | Creates or updates Task v Task lifecycle -> status updates -> artifacts / messages ``` ##### MCP architecture MCP uses a host-client-server model. The host is the AI application. The MCP client is the connection manager inside that host. The MCP server exposes context or capabilities such as tools, resources, and prompts. The MCP data layer uses JSON-RPC, while the transport layer supports stdio for local process communication and Streamable HTTP for remote server communication. Revision `2026-07-28` removed the `initialize` handshake and protocol-level sessions. Every request now carries its protocol version and client capabilities in `_meta`, and every server MUST implement `server/discover` to advertise its versions, capabilities, and identity. A server that needs state across calls mints an explicit handle and passes it as an ordinary tool argument. ``` MCP Host: AI application / IDE / agent runtime | | one or more MCP Client connections v MCP Server(s) | | expose tools, resources, prompts, notifications v External APIs, databases, filesystems, SaaS apps, services ``` ##### OpenWOP architecture OpenWOP uses a workflow-host model. A client starts a run against a compliant host. The host executes a declarative workflow, emits events, may suspend for human input or approval, streams state over SSE, sends signed webhooks, and can support replay or fork behavior. ``` Client / SDK / Agent | | POST /runs v OpenWOP Host | | Executes WorkflowDefinition v Run -> Event Log -> SSE / Webhooks / OpenTelemetry | | Interrupt / pause / resume / replay v Humans, tools, agents, workers, subflows, artifacts ``` | Architecture concern | A2A | MCP | OpenWOP | | --- | --- | --- | --- | | Core entities | Client, server/remote agent, Agent Card, Message, Task, Artifact, Part | Host, client, server, tools, resources, prompts, elicitation, notifications; tasks via the Tasks extension (sampling, roots, and logging are deprecated as of revision `2026-07-28`) | Client, host, workflow, run, event log, node, artifact, interrupt, pack | | Communication pattern | Message/task exchange with streaming and push options | JSON-RPC over stdio or Streamable HTTP | REST run control, SSE event streams, signed webhooks; A2A and MCP are compositions over that wire, each advertised by its own facet (gRPC is an unadvertisable v2 extension, not part of the core wire) | | Discovery | Well-known Agent Card | `server/discover` plus primitive list calls | Well-known host capability document plus workflow catalog | | Lifecycle | Task state machine | Stateless requests, each carrying its protocol version and capabilities; primitive discovery, calls, notifications; with the Tasks extension, a per-request task lifecycle (`working`, `input_required`, `completed`, `failed`, `cancelled`) | Run lifecycle, event log, pause/resume, replay/fork | | State management | Remote agent owns task state; execution internals are opaque | No protocol-level session since `2026-07-28`; cross-call state is an explicit server-minted handle passed as a tool argument; list results carry cache hints (`ttlMs`, `cacheScope`) | Host owns durable run state and exposes events/snapshots | | Trust boundary | Between independent agents | Between host application and MCP servers, including local or remote tool providers | Between workflow client and execution host; also between workflow nodes/tools/humans | #### Specification Comparison | Dimension | A2A | MCP | OpenWOP | Architectural implication | | --- | --- | --- | --- | --- | | Agent/service discovery | Agent Card advertises identity, skills, endpoints, security, capabilities, input/output modes. | `server/discover` advertises supported versions, capabilities, and server identity; each request declares its own client capabilities; primitive discovery through list methods such as `tools/list`. | Capability document advertises protocol version, transports, limits, profiles, envelopes, node support, and host behavior. | A2A discovers agents; MCP discovers capabilities exposed by context/tool servers; OpenWOP discovers workflow hosts. | | Identity and authentication | Agent identity and security schemes appear in the Agent Card; supports standard web-security patterns. | Transport-specific authentication, especially for Streamable HTTP; stdio often relies on local process trust. | Host/caller auth with operation scopes and production profiles; workflow execution identity may travel through run metadata and agent surfaces. | A2A has the most explicit remote-agent identity layer; MCP auth depends heavily on deployment mode; OpenWOP focuses on run-operation authorization. | | Authorization and permissions | Agent-defined authorization; callers must respect security schemes and scopes exposed by the agent. | Server/tool permissions generally mediated by the host/client and transport authentication; tool authorization is often provider-specific. | Standardized operation scope vocabulary such as run creation, read, approval response, artifact read. | OpenWOP is strongest where workflow operation-level authorization matters. | | Message format | Canonical protocol model with JSON representations and bindings including JSON-RPC, gRPC, HTTP+JSON. | JSON-RPC 2.0 request/response/notification messages. | HTTP JSON schemas, events, envelopes, workflow definitions, artifacts, and run snapshots. | MCP is the simplest RPC substrate; A2A and OpenWOP carry richer task/workflow semantics. | | Transport layer | JSON-RPC, gRPC, HTTP+JSON, streaming, push notifications. | Stdio and Streamable HTTP, which may answer a request with an SSE response stream. The older HTTP+SSE transport is deprecated, and `2026-07-28` removed stream resumability. | REST, SSE, signed webhooks; v2 keeps gRPC out of the core wire. | MCP covers local and remote tool servers; A2A and OpenWOP are more naturally network service protocols. | | Task delegation | Core purpose: delegate to another agent as a task. | Not task delegation; it is tool/context invocation by a host application. | Delegation happens inside workflow runs through nodes, workers, agents, subflows, or tools. | Use A2A for peer delegation; use MCP for capability invocation; use OpenWOP for orchestrated delegation. | | Tool invocation | Possible inside the remote agent but not standardized as the primary abstraction. | Central abstraction through `tools/list` and `tools/call`. | Can model tool calls as workflow nodes and can compose with MCP tool calls. | MCP is the most direct tool integration protocol. | | Human-in-the-loop | Task states such as input-required and auth-required can request user participation. | Elicitation lets servers request additional user information through the client/host. In `2026-07-28` it rides the Multi Round-Trip Requests pattern: the server returns an `input_required` result and the client retries with the answers. | First-class interrupts, approvals, clarifications, external events, resume schemas, and run suspension. | OpenWOP is strongest for auditable approval gates; MCP elicitation is narrower and tool-session-oriented. | | Error handling | Binding-native errors plus protocol-specific task states and error semantics. | JSON-RPC error responses plus tool-call result semantics. | Run errors, lifecycle errors, capability-gated refusals, terminal events, and debug artifacts. | OpenWOP is strongest for workflow-runtime failures; MCP is strongest for RPC/tool-call errors. | | Observability | Task status, streaming, push notifications, enterprise tracing guidance. | A logging primitive (deprecated in `2026-07-28` in favor of stderr or OpenTelemetry) and documented `_meta` keys for W3C trace context, but not full workflow tracing by itself. | OpenTelemetry naming, W3C trace context, run events, cost attribution, event logs, debug bundles. | OpenWOP has the most explicit production observability model. | | Auditability | Caller can audit task-level outputs and status, not necessarily internal steps. | Tool calls can be logged, but audit semantics are largely host/platform responsibilities. | Event log, replay, artifacts, trace IDs, and signed events support audit-heavy systems. A host advertising `auditLogIntegrity` keeps a chained, checkpointed audit log that a privileged insider cannot silently rewrite, and serves `GET /audit/verify` ([`security-defaults.md`](https://openwop.dev/spec/v2/core/security-defaults.html)). | OpenWOP is best for regulated workflow audit trails. | | Extensibility | Agent Card extensions and protocol extension mechanisms. | Primitive capabilities, an `extensions` capability field for official and custom extensions (Tasks is one), and opt-in list-changed notifications over `subscriptions/listen`. | Host extensions, profiles, node packs, workflow-chain packs, pack registry. | All three are extensible, but in different layers. | | Versioning | A2A-Version header/parameter and semantic compatibility expectations. | Dated specification revisions; every request carries its revision in `_meta`, and `server/discover` lists the revisions a server supports. | Versioned endpoint surface and compatibility/conformance profiles; its A2A and MCP interfaces negotiate the upstream version under authentication, fail closed below an advertised floor, and record every outcome as a `negotiation.decided` event. | A2A and OpenWOP are more explicit at the public wire-contract level; MCP now declares its version and capabilities per request. | | Deployment model | Remote agent service. | Local process or remote server connected to an AI host. | Durable workflow host. | MCP can be embedded/local; OpenWOP usually requires a durable host; A2A requires addressable agents. | #### Similarities All three protocols attempt to reduce bespoke integration work in agentic AI systems. They each provide a standardized contract around a boundary that would otherwise become custom glue code. | Shared pattern | A2A | MCP | OpenWOP | | --- | --- | --- | --- | | Capability declaration | Agent Card | `server/discover` and primitive lists | Host capabilities and workflow metadata | | Asynchronous or long-running work support | Tasks, streaming, push notifications | Progress notifications, plus durable task handles when both sides support the Tasks extension | Runs, event logs, SSE, webhooks, pause/resume | | Structured artifacts/results | Artifacts and Parts | Tool/resource content arrays and structured JSON-RPC responses | Artifacts, events, envelopes | | Human involvement | Input-required/auth-required task states | Elicitation from server to user via host | Interrupts and approval/clarification gates | | Security boundary awareness | Agent trust boundary | Host/server/tool trust boundary | Run/host/tool/human trust boundaries | | Extensibility | Extensions and metadata | Capabilities, primitives, notifications, server-specific tools/resources/prompts | Profiles, packs, extensions | #### Differences ##### Horizontal vs vertical vs orchestration layer | Layer | Protocol | What it standardizes | | --- | --- | --- | | Horizontal agent layer | A2A | How independent agents discover, message, delegate, and return results to each other. | | Vertical tool/context layer | MCP | How an AI host connects to external tools, data sources, prompts, and contextual capabilities. | | Workflow-control layer | OpenWOP | How durable multi-step workflows are started, streamed, interrupted, resumed, replayed, observed, and validated. | ##### Different answers to “what is opaque?” A2A intentionally treats the remote agent as an opaque collaborator. The caller can see declared skills, task states, and artifacts, but not the remote agent’s private planning, memory, tool chain, or execution graph. MCP is neither an agent transparency protocol nor a workflow protocol. It exposes capabilities from servers to hosts. A host can see and invoke tools/resources/prompts — and, with the Tasks extension, follow one long-running call through its own state machine — but MCP does not define an end-to-end workflow graph or independent agent collaboration model. OpenWOP removes that opacity inside a workflow host. It standardizes run state, event streams, artifacts, interrupts, replay, node packs, and observability so the workflow can be inspected and validated. ##### MCP Tasks and OpenWOP runs are not the same durability MCP revision `2026-07-28` added a [Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview): when a server decides a request will be long-running, it returns a durable `taskId` instead of blocking. The client polls `tasks/get`, answers mid-flight `input_required` prompts via `tasks/update`, and can `tasks/cancel`. The task is durably created before the response is sent, and the handle survives a client restart. That is a real durability guarantee, and it closes the gap this page previously described as MCP having no execution lifecycle at all. It is worth being precise about what it does and does not cover, because the two models are easy to conflate: | Dimension | MCP task | OpenWOP run | | --- | --- | --- | | Scope | One request's asynchronous execution | A multi-step graph of nodes, edges, triggers, and variables | | Who decides | Server-directed, per request | Client starts a named workflow | | Retention | TTL-bounded (`ttlMs`) | Retention floors the host advertises | | Cancellation | Cooperative — the server may still reach a non-`cancelled` terminal state | A `cancelling` → `cancelled` transition in the run state machine | | History | Current status, then a terminal result | An append-only event log | | Re-execution | Not defined | Replay and fork | | Repeat-call safety | Not defined | Effect identity, atomic claim, and a `GET /runs/{runId}/effects` witness | The practical read: MCP Tasks makes *one call* survivable. It does not describe the sequence of calls, what to do when the second attempt of one of them arrives, or how to reconstruct what happened. Those are the questions a workflow layer answers, and a harness that needs them still has to supply them — which is the same additive relationship the rest of this page describes, just at a closer distance than it was in June. Since September the two models also meet on the wire. The v2 corpus specifies an MCP **server mount** on an OpenWOP host: `tools/list` lists the workflows the caller may run, and `tools/call` starts a run whose trust boundary is `untrusted` ([RFC 0208](https://openwop.dev/rfcs/0208-v2-a2a-mcp-operation-mappings.html)). Without the Tasks extension, when that run stops for a human, the mount answers the call with an MCP `input_required` result carrying one `elicitation/create` request per open interrupt. A host caps those rounds at its advertised `mcp.mrtr.maxRounds` (at most 16). A host MAY also serve the Tasks extension there ([RFC 0198](https://openwop.dev/rfcs/0198-mcp-server-mount-tasks.html)). If it does, it answers a long `tools/call` with an MCP task whose `taskId` is the run's projected id, `tasks/get` reads the run without appending to its log, and `tasks/update` resolves the run's open interrupts. The MCP task is then a view onto one OpenWOP run. The event log, replay, fork and effect witness in the table above stay on the OpenWOP side of that view. The mount is optional. RFC 0198, like every RFC of the corpus's MCP/A2A program (0197–0216), is `Accepted` only provisionally, pending its cross-organization retrospective review. Its evidence is the steward's v2 reference host, a reference example rather than a production host. Every host the [conformance matrix](https://openwop.dev/conformance/) has measured is run by the steward or a steward-affiliated organization; no independent-organization host exists yet. ##### Different units of composition | Protocol | Composition unit | Typical example | | --- | --- | --- | | A2A | Skill advertised by an agent and invoked as a task | “Ask the procurement agent to compare vendors.” | | MCP | Tool, resource, prompt, elicitation, logging; optionally a task | “Query CRM, read the contract, call the pricing API, use a prompt template.” | | OpenWOP | Workflow definition, node, pack, run, interrupt, artifact | “Run a vendor-evaluation workflow, pause for approval, replay if policy changes.” | #### Production Failure Modes — which layer fences them An agent is a loop: a model reasons, calls a tool, observes the result, and repeats. That loop is what makes agents capable, and it is also where they fail in production — not in the model, but in the cycle wrapped around it. The well-documented failure modes are properties of the loop itself: small per-step error rates compound across steps, every lap costs tokens, an open-ended loop may never decide it is done, and a model that can call a tool can be tricked into calling the wrong one by poisoned input. This is the dimension where the three protocols diverge most sharply, because **OpenWOP is the only one of the three that owns the loop.** A2A treats the remote agent's loop as opaque; MCP's Tasks extension gives a single call a durable, resumable lifecycle, but the loop *around* those calls is still the harness's. So the controls that fence a production agent — cap the steps, cap the time, cap the spend, gate the irreversible action, narrow the toolbox, mark untrusted input — land naturally in the workflow-control layer. | Production failure mode | A2A | MCP | OpenWOP | | --- | --- | --- | --- | | **Runaway loop** — the agent never converges | Coarse task states; no iteration bound across the remote agent's internal loop | No loop construct to bound | `maxLoopIterations` run-execution bound; breach emits a terminal `loop_limit_exceeded` plus a `cap.breached` event, with the per-turn iteration counter observable on the run's event stream | | **Hang / stall** — a step blocks indefinitely | Push-notification timeouts are caller-side | Per-call only; no workflow-level wall clock | `runTimeoutMs` wall-clock bound on the whole run, clamped to the host's `limits.maxRunDurationMs`; a breach emits `cap.breached` and fails the run with `run_timeout` | | **Cost blowout** — every lap drags the full context | Not modeled; spend is internal to the remote agent | Per-tool-call only | First-class budget / quota / cost policy: reserve / consume / threshold / exhaust events plus `cap.breached`, with per-call accounting attributed on usage events. The events may carry the aggregate cost the caller capped, never the host's rate card or per-unit prices | | **Compounding errors** — 90%-per-step ≈ 35% over ten steps | Caller sees the final task result, not intermediate self-correction | No self-correction model | Self-correction is first-class: an agent decision below the run's `escalationThreshold` MUST suspend the node on a `low-confidence` interrupt, and a verifier turn with explicit convergence criteria lets a run check its own work, so it escalates instead of confidently emitting a wrong result | | **Irreversible autonomous action** — money out, data deleted | Coarse `input-required` / `auth-required` task states | Elicitation requests user input, but cannot enforce an approval gate | The model proposes; the host performs. High-blast-radius steps route through a first-class `interrupt` (`kind: "approval"`) that suspends **before** the action. The host MUST refuse a resolver outside the listed approvers (and, where advertised, the named groups or roles), can require a quorum, and resumes deterministically. A `reject` fails closed: the raising node fails with `approval_rejected` (not retryable), and a rejected gate cannot satisfy a success edge, so the run continues only over an edge that admits a failed source ([`interrupt.md`](https://openwop.dev/spec/v2/core/interrupt.html)) | | **Over-broad toolbox** — every tool is a way to go wrong | Tools hidden behind the agent; not the caller's to scope | `tools/list` exposes the catalog, but scoping is host/server policy | Portable tool catalog listing only the tools the caller may invoke. The host assigns each tool's safety tier, replay policy, and egress itself, never copying them from an MCP server's `annotations`, which are untrusted; an MCP tool it has not classified counts as `write` ([`tool-catalog.md`](https://openwop.dev/spec/v2/core/tool-catalog.html)). Per-tool authorization hooks sit on top, and a credential received inbound is never forwarded to an A2A, MCP, or webhook hop | | **Prompt injection** — poisoned input redirects the agent | Trust boundary between agents; in-agent injection is opaque to the caller | Tool/server output flows to the host; trust handling is the host's responsibility | A documented threat model with conformance-tested invariants: external content (user input, retrieved knowledge, prior artifacts, **and MCP/tool output**) carries an untrusted trust marker, and — critically — untrusted tool/MCP output cannot advance an approval gate. A run started through the MCP mount starts `untrusted` | The takeaway reinforces the layered reading of the rest of this comparison. None of these controls make A2A or MCP worse at what they do — A2A still owns agent collaboration, MCP still owns tool/context integration. But the moment an agent runs unattended against real systems, the production question becomes "what bounds the loop, and what stops it before the irreversible step?" — and that is the workflow-control layer's job. OpenWOP's contribution here is not that it makes the model more reliable (no protocol does); it is that when a run goes long, costs too much, loops forever, or reaches for an irreversible action, that fact is observable on the event stream and, for the irreversible case, stopped at a fail-closed gate. #### Strengths and Weaknesses | Protocol | Strengths | Weaknesses / open questions | | --- | --- | --- | | A2A | Strong abstraction for independent agent collaboration. Agent Card creates a clear discovery and capability surface. Supports long-running and human-in-the-loop task interaction without exposing internals. Good fit for cross-vendor agent ecosystems and skill marketplaces. | Not designed to standardize internal workflow graphs, replay, or node-level traces. Tool invocation is hidden behind agent behavior unless paired with MCP or another tool protocol. Task states are intentionally coarse compared with a workflow engine. | | MCP | Best protocol for connecting models/apps to tools, data, prompts, and APIs. Simple JSON-RPC interaction model with dynamic capability discovery. Works locally via stdio and remotely via Streamable HTTP. Widely adopted mental model: “USB-C for AI tool/context integration.” | Does not define agent-to-agent task delegation. Does not define durable workflow orchestration, replay, or full run audit semantics. Security posture depends heavily on host trust, server trust, local process isolation, tool metadata validation, and transport authorization. | | OpenWOP | Strongest durable execution, replay, event-log, and observability model. Rich human checkpoint and interrupt semantics. Conformance-oriented approach for workflow-host portability. Composes naturally with both A2A and MCP. | Heavier implementation burden than A2A or MCP. Not a replacement for an agent-to-agent messaging protocol. Not a replacement for MCP’s direct tool/context integration surface. Requires durable host infrastructure to realize its full value. Every host measured so far is run by the steward or a steward-affiliated organization; and its current-A2A and current-MCP compatibility claims both still wait on a result against a real upstream peer. | #### Use Case Fit | Scenario | Best fit | Why | | --- | --- | --- | | Agent-to-agent collaboration | A2A | Designed specifically for independent agents exchanging messages and tasks. | | Tool/API/data-source integration | MCP | Tools, resources, and prompts are MCP’s core primitives. | | Durable multi-step AI workflow | OpenWOP | Run lifecycle, event log, interrupts, replay, and observability are central. | | Human approval flows | OpenWOP | First-class interrupts and resume semantics beat coarse task states or tool elicitation. | | Simple remote specialist agent invocation | A2A | Lower-level workflow details do not need to be exposed. | | Local filesystem/database/SaaS tool access from an AI app | MCP | MCP servers expose these as tools/resources over stdio or HTTP. | | Workflow replay, fork, time-travel debug | OpenWOP | A2A and MCP do not define this as a core lifecycle. | | Agent marketplace or skill catalog | A2A | Agent Cards and skills are the natural marketplace metadata unit. | | Enterprise agent platform | All three | A2A for external agents, MCP for tools/context, OpenWOP for durable orchestration. | | Secure tool execution inside audited workflow | MCP + OpenWOP | MCP invokes the tool; OpenWOP governs run-level policy, event log, human approval, and traceability. | | Cross-vendor interoperability | Layer-specific | A2A for agents, MCP for tools/context servers, OpenWOP for workflow hosts. | #### Integration Possibilities The strongest architecture treats the three protocols as composable layers rather than alternatives. ``` External agent ecosystem | | A2A: discover agents, delegate tasks, exchange artifacts v A2A-facing agent or gateway | | OpenWOP: start durable run, stream events, pause/resume, replay v Workflow host / orchestration plane | | MCP: call tools, read resources, use prompts, elicit input v External APIs, databases, files, SaaS apps, local services ``` ##### Pattern 1: OpenWOP host exposed as an A2A agent An OpenWOP host can publish an A2A Agent Card. Each workflow can be advertised as an A2A skill. An incoming A2A message creates an OpenWOP run. Run events are projected into A2A task status updates. Run artifacts become A2A artifacts. In v2 this projection is normative, not just a design option. [`spec/v2/core/interop.md`](https://openwop.dev/spec/v2/core/interop.html) and its operation map ([RFC 0208](https://openwop.dev/rfcs/0208-v2-a2a-mcp-operation-mappings.html)) pin it to A2A `1.0.1`: - the card lists one skill per invocable workflow, with `skills[].id` set to the routed `workflowId`; - `SendMessage` maps to `createRun`, or resolves an open interrupt when it names a waiting task; - `GetTask`, `ListTasks` and `CancelTask` map to `getRun`, `listRuns` and `cancelRun`; - a task the caller could not read through `getRun` is answered exactly as a nonexistent one; - `SubscribeToTask` re-attaches to a run's event stream, which needs the host's `a2a.durableTasks` and `a2a.streaming` facets; - a run suspended on a `credential` interrupt projects to the A2A `auth-required` state ([RFC 0199](https://openwop.dev/rfcs/0199-outbound-oauth-client-and-credential-interrupt.html)); - an A2A error carries a `google.rpc.ErrorInfo` detail, and an A2A interface never answers in the OpenWOP error envelope ([RFC 0211](https://openwop.dev/rfcs/0211-a2a-error-details-are-errorinfo.html)). A host MAY also publish each inventoried agent as its own Agent Card ([RFC 0202](https://openwop.dev/rfcs/0202-per-agent-a2a-agent-cards.html)). It MAY serve a run artifact in the A2A `Artifact`/`Part` shape when the client negotiates `application/a2a+json` ([RFC 0205](https://openwop.dev/rfcs/0205-run-artifacts-and-turns-speak-a2a-parts.html)). A2A push notifications are handled as webhook egress ([RFC 0214](https://openwop.dev/rfcs/0214-a2a-push-credential-is-a-destination-credential.html)). All of this is advertised through the optional `a2a` facet. The corpus makes no current-A2A compatibility claim yet: the v2 A2A `1.0` legs run against the conformance suite's own fake peer, the claim requires a result against a real upstream peer, and that result is still externally gated. | A2A concept | OpenWOP projection | | --- | --- | | Agent Card | Host metadata plus workflow catalog | | AgentSkill | WorkflowDefinition | | Message | Run input | | Task | Run | | Task status update | Run event or run snapshot | | Artifact | Workflow artifact | | Input required | OpenWOP interrupt, approval, or clarification | | Auth required | OpenWOP `credential` interrupt | ##### Pattern 2: OpenWOP workflow calls MCP tools Inside a workflow run, worker nodes can call MCP servers for tool execution or resource retrieval. This keeps MCP at the tool/context boundary while OpenWOP governs run-level durability, state, observability, retry behavior, and approvals. v2 gives this direction a normative home as well. A host advertising `mcp.client` exposes `ctx.mcp.callTool`, `listTools`, `readResource` and `serverHealth` to pack code ([`host-services.md`](https://openwop.dev/spec/v2/core/host-services.html)). Each call is made against a host-configured server at the negotiated revision, and returns the server's MCP result unaltered, including an `isError` result ([RFC 0204](https://openwop.dev/rfcs/0204-host-mcp-client-returns-mcp-results.html)). A `remote` node-pack runtime can also name its MCP server by an inline subset of its MCP Registry record. That subset carries no install instructions and no credential values ([RFC 0203](https://openwop.dev/rfcs/0203-remote-runtime-mcp-registry-record.html)). When an MCP server needs an OAuth grant, the host is the OAuth client: it runs PKCE, holds the credential itself, and can suspend the run on a `credential` interrupt until a user connects the account ([RFC 0199](https://openwop.dev/rfcs/0199-outbound-oauth-client-and-credential-interrupt.html), now `Accepted` provisionally). As with A2A, the corpus makes no current-MCP compatibility claim yet: that claim requires a result against a pinned real peer, and that result is still externally gated. | OpenWOP workflow need | MCP role | | --- | --- | | Read external system state | Expose resource or tool call | | Execute a business operation | Expose action as an MCP tool | | Use a reusable instruction template | Expose prompt primitive | | Ask user for missing tool input | Use MCP elicitation, optionally wrapped in OpenWOP interrupt | ##### Pattern 3: A2A agent backed by MCP and OpenWOP A specialist agent can expose a simple A2A interface while internally using MCP to call actual systems and OpenWOP to coordinate the work. This is likely the cleanest enterprise pattern because it separates the public contract from internal execution and integration details. ``` A2A caller -> A2A specialist agent -> OpenWOP workflow run -> MCP CRM server -> MCP database server -> MCP filesystem or document server -> OpenWOP approval interrupt -> A2A artifact result ``` ##### Adapter responsibilities | Adapter concern | Mapping / design choice | | --- | --- | | Discovery | Expose OpenWOP workflows as A2A skills; expose MCP server primitives to workflow node catalogs. | | Invocation | Map A2A `SendMessage` to OpenWOP `POST /runs`; map OpenWOP tool nodes to MCP `tools/call`. | | Identity | Propagate caller identity from A2A through OpenWOP run metadata and into MCP tool authorization where appropriate. | | State | Project OpenWOP run states into A2A task states; preserve extra detail in metadata. | | Human-in-the-loop | Map OpenWOP interrupts to A2A input-required states and/or MCP elicitation depending on where the user interaction belongs. | | Observability | Preserve trace IDs across A2A, MCP, and OpenWOP calls. v2 specifies the carrier: `params._meta` on an MCP request, `Message.metadata.openwop.traceparent` on an A2A message, or the HTTP `traceparent` header ([RFC 0207](https://openwop.dev/rfcs/0207-trace-context-across-mcp-and-a2a.html)). | | Security | Do not let MCP tool metadata implicitly escalate privileges; enforce host/workflow policy before tool invocation. | #### Final Recommendations ##### Choose A2A when... - You need independent agents to discover and call each other. - You want an agent or skill marketplace. - The caller should not inspect internal tools, workflows, memory, or reasoning. - The core problem is remote task delegation and artifact return. ##### Choose MCP when... - You need to connect an AI app or agent runtime to external tools, data, prompts, APIs, files, or SaaS systems. - You want dynamic discovery of tools/resources/prompts. - You need local process integrations via stdio or remote integrations via Streamable HTTP. - Your main challenge is context/tool integration rather than agent collaboration or durable workflow control. ##### Choose OpenWOP when... - You need durable, replayable, observable AI workflows. - You need human approvals or clarifications as first-class workflow checkpoints. - You need event logs, run snapshots, fork/replay, traces, and production debugging. - You are implementing or evaluating a workflow host, not merely an agent endpoint or tool server. ##### Use all three when... Use all three for an enterprise-grade agentic platform: ``` A2A = public agent collaboration boundary MCP = tool, data, prompt, and context integration boundary OpenWOP = internal durable workflow and observability boundary ``` ##### Decision checklist | Question | Likely answer | | --- | --- | | Do we need one agent to call another independent agent? | A2A | | Do we need the agent to call external tools or read external data? | MCP | | Do we need durable, replayable, observable workflow execution? | OpenWOP | | Do we need human approval gates with deterministic resume? | OpenWOP | | Do we need a simple integration between an AI app and a database/API/filesystem? | MCP | | Do we want remote agents to remain implementation-opaque? | A2A | | Do we want to expose workflows as agent skills? | A2A + OpenWOP | | Do we want audited tool execution inside durable workflows? | MCP + OpenWOP | | Do we want a complete enterprise agent platform? | A2A + MCP + OpenWOP | > **Bottom line:** A2A, MCP, and OpenWOP are best viewed as a layered protocol stack. A2A lets agents collaborate. MCP lets agents and AI applications use tools and context. OpenWOP makes multi-step agentic work durable, observable, interruptible, replayable, and portable. #### Sources The analysis distinguishes specification-grounded claims from architectural interpretation. These are the primary source documents consulted. - [Agent2Agent (A2A) Protocol Specification](https://a2a-protocol.org/latest/specification/) — official A2A specification (the page is labelled `1.0.0`). - [A2A release v1.0.1](https://github.com/a2aproject/A2A/releases/tag/v1.0.1) — the latest A2A release: three specification fixes, and the version the OpenWOP operation map pins. - [Model Context Protocol Architecture Overview](https://modelcontextprotocol.io/docs/learn/architecture) — official MCP architecture, primitives, transports, and lifecycle overview. - [Model Context Protocol GitHub repository](https://github.com/modelcontextprotocol/modelcontextprotocol) — official MCP specification and schema repository. - [MCP specification, revision `2026-07-28`](https://modelcontextprotocol.io/specification/2026-07-28) — the current revision, checked for this review. - [MCP `2026-07-28` changelog](https://modelcontextprotocol.io/specification/2026-07-28/changelog) — stateless protocol and `server/discover` (SEP-2575), sessions removed (SEP-2567), Multi Round-Trip Requests (SEP-2322), Tasks moved to an extension (SEP-2663), and sampling, roots, and logging deprecated (SEP-2577). - [MCP Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) — durable handles, polling, mid-flight input, and the task lifecycle compared above. - [OpenWOP — Workflow Orchestration Protocol](https://openwop.dev/) — OpenWOP overview and specification landing page. - [OpenWOP Spec v2 — Interop](https://openwop.dev/spec/v2/core/interop.html) — the normative v2 home for A2A and MCP composition: facets, authenticated version negotiation, the operation map, the MCP Tasks projection, and per-agent cards. ### OpenExO 3.0 and OpenWOP Source: https://openwop.dev/comparisons/openexo-3-openwop/ OpenExO 3.0 describes the AI-native organization. OpenWOP can provide the durable workflow protocol that makes that organization executable, observable, governable, and portable across hosts. #### Why this matters now OpenExO 3.0 argues that organizations must be redesigned around intelligence rather than merely upgraded with intelligent agents. Salim Ismail’s public ExO 3.0 framing describes a shift from hierarchical structures into AI-native systems capable of sensing, deciding, and executing at unprecedented speed and scale.[1](#fn-salim) The OpenExO Organizational Singularity outline frames the transformation as three coupled moves: **ExO 3.0** as the destination architecture, the **Intelligence Stack** as the new operating system, and **REWRITE** as the migration playbook.[2](#fn-os-outline) That creates a protocol gap. A firm cannot operate at machine tempo on strategy language alone. It needs an execution layer that can express workflows, run agent teams, pause for humans, preserve auditability, learn from replay, and connect to tools and other agents. OpenWOP addresses that gap by defining an open protocol for durable multi-agent workflow orchestration: supervisor agents decide, workers call tools, humans participate in conversation loops, and every event flows through an event log, streams, webhooks, and telemetry.[3](#fn-openwop-home) > **Positioning statement:** OpenWOP is the open execution protocol for the Intelligence Stack — not a replacement for ExO 3.0, not a generic agent framework, and not another consulting methodology, but the technical layer that makes agentic organizational work durable, inspectable, and governable. #### The conceptual fit: organization design meets protocol design ExO 3.0 is concerned with the shape of the AI-native firm: purpose, autonomy, decision architecture, recursive learning, elastic agency, human architecture, governance, and ecosystem trust. OpenWOP is concerned with the execution contract for AI-native work: workflow definitions, run lifecycle, event streams, interrupts, artifacts, capabilities, scopes, conformance, and observability. | OpenExO 3.0 need | OpenWOP capability | Why it matters | | --- | --- | --- | | Move from human-centric workflows to agentic workflows | Workflow definitions, runs, node packs, worker nodes, and human conversation nodes | Makes the redesigned workflow executable rather than conceptual. | | Make the Intelligence Stack operational | REST, SSE, signed webhooks, event logs, OpenTelemetry, per-run options | Provides the runtime contract for sensing, interpreting, deciding, orchestrating, acting, and learning. | | Build Edge Twins without forking the enterprise data estate | Workflow-scoped runs, metadata, scopes, correlation IDs, tool/resource access patterns | Supports governed access by workflow rather than uncontrolled data replication. | | Continuously govern agent actions | Interrupts, approval queues, logs, replay, redaction, scopes, and policy-aware hosts | Turns GOVERN / ASSURE into runtime controls instead of an after-the-fact audit process. | | Avoid proprietary lock-in while scaling agent workforces | Vendor-neutral wire protocol, conformance suite, additive within-major compatibility | Lets firms adopt AI-native workflows without making one vendor the permanent nervous system. | #### Mapping OpenWOP to the Intelligence Stack The Organizational Singularity describes the Intelligence Stack as the operating core of ExO 3.0, and crosswalks it to industry vocabulary such as intelligence, action, governance, orchestration, and economics.[4](#fn-stack-crosswalk) OpenWOP maps naturally to the execution and orchestration layers, while also producing the telemetry and control evidence needed by governance and economics. ``` Purpose / MTP as protocol ↓ SENSE ── gather signals, events, data, user input ↓ INTERPRET ── build context, assemble evidence, reason over state ↓ DECIDE ── supervisor agent selects next-worker, ask-user, or terminate ↓ ORCHESTRATE ── OpenWOP run lifecycle, event log, state channels, interrupts ↓ ACT ── workers call tools, APIs, MCP servers, external systems ↓ LEARN ── replay, fork, evals, human-correction capture, telemetry ``` | Intelligence Stack layer | What OpenWOP contributes | OpenExO implication | | --- | --- | --- | | **Purpose / MTP** | Per-run metadata, policy tags, refusal boundaries, governance hooks | The organization’s purpose can become machine-readable constraints attached to workflow execution. | | **SENSE** | Run inputs, webhooks, event streams, external triggers, artifact ingestion | The firm can ingest signals into durable workflows rather than ad hoc prompts. | | **INTERPRET** | Worker nodes, evidence assembly, typed state channels, artifacts | Reasoning steps become inspectable, replayable, and improvable. | | **DECIDE** | Supervisor/orchestrator decisions, human interrupts, approval gates | Decision architecture can encode which decisions are automated, escalated, or reserved for humans. | | **ORCHESTRATE** | Run lifecycle, suspend/resume, replay/fork, streaming, cancellation, conformance | This is where OpenWOP is most foundational: it standardizes the execution control plane. | | **ACT** | Tool calls, node packs, MCP integration, external system interactions | Agents can act against real systems while remaining bounded by workflow context and policy. | | **LEARN** | Event logs, replay, time-travel debugging, eval hooks, run metrics | Recursive learning becomes operational: improvements can be derived from completed runs, failures, overrides, and cost traces. | #### REWRITE as an OpenWOP workflow migration process OpenExO describes REWRITE as a six-step migration playbook and distinguishes Direct Mode for smaller organizations from Edge Mode for larger ones. The public outline emphasizes that the GOVERN / ASSURE control plane operates from Day 1, every agent action is logged with correlation IDs, and parallel runs require success criteria and rollback protocols.[5](#fn-rewrite) OpenWOP can make REWRITE concrete by turning each migration candidate into a versioned workflow definition, running it in shadow mode, collecting human corrections, and graduating autonomy only when telemetry proves readiness. ##### BACKCAST → define the target workflow architecture Translate the future-state workflow into an OpenWOP workflow definition: nodes, edges, human checkpoints, tools, data dependencies, artifacts, success metrics, and failure boundaries. ##### ASSESS → determine migration readiness Use OpenWOP capability discovery and conformance checks to verify that the chosen host supports the required node types, event modes, security scopes, secrets, webhooks, and observability surfaces. ##### EXTRACT → create the Workflow Data Manifest OpenExO’s Workflow Data Manifest asks which data sources each workflow touches, why, with what sensitivity and approval model.[6](#fn-data-manifest) OpenWOP can carry those requirements into per-run options, metadata, access scopes, tool bindings, and audit trails. ##### DIAGNOSE → locate decision and control boundaries Identify which workflow steps are safe to automate, which require human review, which require policy agent checks, and which must remain human-only. Map those boundaries into interrupts, scopes, and approval queues. ##### BUILD & PROVE → run in shadow mode Execute OpenWOP runs alongside the legacy workflow. Compare outputs, measure override rates, replay failures, and fork alternate designs before expanding autonomy. ##### REWIRE → migrate production responsibility Once the agentic workflow outperforms the old process, migrate responsibility incrementally. Keep rollback paths, run histories, audit artifacts, and operational-system source-of-truth rules intact. #### Edge Twin architecture powered by OpenWOP The OpenExO outline defines the Edge Twin as a structurally separate, AI-first replica of a core business function or unit: a small human team plus an agent cluster that rebuilds specific mothership workflows using the Intelligence Stack, proves superior performance, then replaces those workflows one at a time.[7](#fn-edge-twin) OpenWOP is especially strong here because an Edge Twin is fundamentally a workflow migration engine. It needs governed access to existing systems, parallel execution, rollback, audit trails, and evidence that agentic workflows are outperforming legacy ones. ``` Enterprise mothership systems ERP · CRM · billing · support · policy · knowledge bases │ │ workflow-scoped governed API access │ read/write separated · short-lived credentials · correlation IDs ▼ OpenWOP-powered Edge Twin ├─ workflow catalog ├─ run lifecycle and event log ├─ supervisor agents ├─ worker agents and MCP tools ├─ human approval queues ├─ governance and eval agents └─ telemetry, replay, cost, artifacts │ │ prove → migrate → deprecate legacy workflow ▼ AI-native operating unit ``` | Edge Twin requirement | OpenWOP design pattern | | --- | --- | | Do not fork the data estate | Grant workflow-scoped tool/API access; preserve source-of-truth boundaries in metadata and policy. | | Run beside the mothership before replacing it | Use shadow runs, historical replay, parallel comparison, and explicit graduation criteria. | | Recover from agent failure | Pause, cancel, replay, fork, retry, and roll back at the workflow level. | | Show CIO/CISO governance evidence | Expose logs, traces, artifacts, scopes, approvals, redactions, and conformance results. | | Migrate one workflow at a time | Represent each migration candidate as a workflow definition with its own run history and manifest. | #### The agent workforce as an operating contract An agent workforce cannot be governed as a list of bots. It needs a contract that says what each agent team can do, how it receives work, what data it may touch, when it must escalate, and how performance is measured. OpenWOP provides the workflow-level contract for that workforce. ##### Supervisor agents Own the workflow loop: decide next worker, ask a human, terminate, retry, or escalate. In OpenWOP terms, the supervisor governs the run path. ##### Worker agents Execute bounded tasks: assemble evidence, classify exceptions, draft recommendations, call MCP tools, or produce artifacts. ##### Governance agents Monitor policy, drift, spend, quality, risk, and human override patterns. They may begin alert-only, then gain escalation or kill-switch authority. ##### Minimum viable agent spec | Field | Purpose | OpenWOP implementation hook | | --- | --- | --- | | Role | What the agent is accountable for | Node type, workflow role, metadata | | Autonomy tier | What it can decide without a human | Interrupt rules, approval profiles, policy scopes | | Data boundary | Which systems and objects it may access | Tool permissions, per-run overlays, secrets, manifest references | | Decision boundary | What must be escalated | Human review queue, typed interrupts, policy gates | | Memory boundary | What may persist beyond the run | State channels, artifact retention, redaction policy | | Performance target | How improvement is measured | Run metrics, evals, outcome tags, cost traces | | Recovery behavior | How failures are handled | Retry, replay, fork, cancel, rollback, compensation workflow | #### GOVERN / ASSURE becomes executable OpenExO describes GOVERN / ASSURE as four operational primitives: trusted evals, searchable logs, granular rollback, and human review queues.[8](#fn-govern) OpenWOP is unusually well aligned with this because it standardizes the runtime surfaces that produce those controls. ##### Trusted evals Attach eval runs, historical replay, and shadow-mode comparisons to workflow versions. ##### Searchable logs Persist event logs, artifacts, state transitions, correlation IDs, and trace context. ##### Granular rollback Replay, fork, cancel, pause, resume, or compensate at the workflow-run level. ##### Human review queue Use typed interrupts for approvals, clarifications, exceptions, and high-risk decisions. OpenWOP’s spec corpus emphasizes run lifecycle, SSE stream modes, HMAC-signed webhooks, replay and time-travel debugging, idempotency, typed state channels, version negotiation, and human-in-the-loop interrupts.[9](#fn-spec-corpus) Its governance page states the protocol mission as declaring, running, streaming, interrupting, replaying, and validating durable AI workflows across hosts while remaining vendor-neutral and implementable by unaffiliated hosts.[10](#fn-governance) #### How OpenWOP composes with A2A and MCP OpenWOP should not be positioned as the only protocol in the ExO 3.0 stack. The better architecture assigns each protocol a clear layer: | Protocol | Best role in an ExO 3.0 architecture | Boundary | | --- | --- | --- | | **OpenWOP** | Durable workflow execution protocol | Inside the Intelligence Stack: runs, event logs, interrupts, workflow lifecycle, observability, conformance. | | **A2A** | Agent-to-agent collaboration protocol | Between organizations, agent networks, vendors, or specialist agents. | | **MCP** | Tool, context, and data access protocol | Between an agent/workflow node and external systems such as databases, search, files, SaaS tools, and APIs. MCP is officially described as an open standard for connecting AI applications to external systems.[11](#fn-mcp) | ``` ExO 3.0 organization └─ Intelligence Stack ├─ OpenWOP: workflow run lifecycle, event log, governance, replay │ ├─ MCP: tools, data sources, resources, prompts, APIs │ └─ A2A: external specialist agents and partner organizations └─ GOVERN / ASSURE: evals, logs, rollback, review queues ``` This composition lets OpenExO describe a practical open architecture: **A2A for inter-agent collaboration, MCP for tool and data connectivity, and OpenWOP for durable organizational workflow execution.** #### Implementation blueprint: from concept to pilot The best proof of the OpenWOP/OpenExO fit is a concrete pilot. A strong candidate is an exception-heavy workflow such as invoice exception handling, customer escalation triage, claims review, order exception resolution, compliance intake, procurement approval, or sales operations follow-up. ##### Phase 1: Publish the OpenExO workflow profile - Define a minimal `openexo.workflow.profile` extension for OpenWOP run metadata. - Include MTP alignment, autonomy tier, workflow data manifest ID, human review policy, cost budget, risk tier, and success metrics. - Map GOVERN / ASSURE evidence requirements to run outputs. ##### Phase 2: Build one reference workflow - Select one real-world workflow with high coordination load and measurable output quality. - Model it as an OpenWOP workflow: supervisor, evidence worker, policy worker, action worker, human reviewer, and learning loop. - Run in shadow mode beside the current process. ##### Phase 3: Instrument for proof - Track cycle time, cost per completed outcome, escalation rate, human override rate, false-positive rate, customer impact, policy violations, and recovery time. - Use OpenWOP event logs, artifacts, traces, and run metadata as the proof record. ##### Phase 4: Graduate autonomy in waves - Begin with alert-only governance. - Move to human approval for low-risk recommended actions. - Allow bounded autonomous action only after repeated replay and shadow-mode evidence. - Keep rollback, pause, and kill-switch controls available throughout. ##### Phase 5: Share what works - Co-author an ExO 3.0 + OpenWOP reference guide from the pilot's evidence. - Publish workflow templates for the first five migration patterns, openly. - Define a conformance checklist for Edge Twin pilots. - Make REWRITE something a technical team can execute, not just read. #### Honest boundaries A protocol earns trust by what it refuses to claim. Five boundaries worth stating plainly: | Fair challenge | Where OpenWOP stands | | --- | --- | | “This sounds like another workflow engine.” | It is a wire-level protocol and conformance target, not a proprietary runtime. Any host that passes the public suite runs the same workflows — portability is the point, not a product. | | “OpenExO is a management model, not a software spec.” | Exactly. The two meet at a boundary: ExO 3.0 defines what the AI-native firm should become; OpenWOP defines the contract its workflows run on. Neither replaces the other. | | “Enterprises will not let an Edge Twin access core systems.” | They should not — not without least-privilege scopes, short-lived credentials, source-of-truth rules, and correlation IDs. The Edge Twin pattern above assumes governed, workflow-scoped access, never a forked data estate — the same rule the OpenExO outline itself sets.[7](#fn-edge-twin) | | “Agent autonomy is risky.” | It is. That is why autonomy graduates by wave — alert-only, then approval-gated, then bounded — with interrupts, replay, approval queues, and rollback available at every stage, never granted wholesale. | | “The protocol is young.” | True. Start with pilot workflows, shadow runs, and conformance checks rather than mission-critical replacement on Day 1. The escape hatches are documented, and the suite is public. | #### Final thesis > **OpenExO 3.0 tells leaders what the AI-native organization should become. OpenWOP can tell systems how that organization actually runs.** OpenWOP is compelling for OpenExO 3.0 because it sits exactly where the organizational thesis needs an implementation substrate: the boundary between strategic redesign and operational execution. It can turn REWRITE from a playbook into a set of durable, governed workflow migrations. It can turn the Intelligence Stack from a conceptual operating model into a runtime architecture. It can turn the agent workforce from a collection of bots into a measurable, auditable, portable execution system. Said plainly: > **OpenWOP is the open workflow protocol for the ExO 3.0 Intelligence Stack: a durable, observable, human-governed execution layer for AI-native organizations.** #### An open door Everything above is testable today, with nothing proprietary at stake. The [v2 spec corpus](https://openwop.dev/spec/v2/), the [conformance suite](https://openwop.dev/conformance/), and the reference hosts are public; the protocol is [openly governed](https://openwop.dev/governance/) and implementable by unaffiliated hosts; and a [downloadable white-label app](https://openwop.dev/install/) ships the full agent-workforce surface — named agents, autonomy tiers, approval queues, durable boards — ready to stand up against one real workflow. If the Organizational Singularity describes the firm that must exist, this is one concrete, open way to make it run. #### Sources and references - Salim Ismail, [official site](https://salimismail.com/), public description of ExO 3.0 as a framework for redesigning organizations around intelligence, autonomy, and purpose. - OpenExO, [The Organizational Singularity v20 Bookapp](https://openexo.com/organizational-singularity), outline describing ExO 3.0, the Intelligence Stack, and REWRITE. - OpenWOP, [homepage](https://openwop.dev/), description of OpenWOP as a protocol for multi-agent workflow orchestration with REST, SSE, signed webhooks, event logs, and OpenTelemetry. - OpenExO, [The Organizational Singularity](https://openexo.com/organizational-singularity), Intelligence Stack sections and industry stack crosswalk. - OpenExO, [REWRITE Playbook](https://openexo.com/organizational-singularity), Direct Mode / Edge Mode and governance-from-Day-1 language. - OpenExO, [Workflow Data Manifest](https://openexo.com/organizational-singularity), Step 3 EXTRACT language on workflow data sources, sensitivity, retention, and approvals. - OpenExO, [Edge Deployment Model](https://openexo.com/organizational-singularity), Edge Twin definition and governed API access/no-data-fork guidance. - OpenExO, [GOVERN / ASSURE](https://openexo.com/organizational-singularity), four primitives: trusted evals, searchable logs, granular rollback, human review queue. - OpenWOP, [v2 spec corpus](https://openwop.dev/spec/v2/), run lifecycle, capability declaration, SSE stream modes, webhooks, replay, idempotency, typed state channels, and HITL interrupt primitive. - OpenWOP, [governance page](https://openwop.dev/governance/), vendor-neutral mission and implementability by unaffiliated hosts. - Model Context Protocol, [official introduction](https://modelcontextprotocol.io/docs/getting-started/intro), MCP as an open-source standard for connecting AI applications to external systems, tools, data, and workflows. ## Core specification ### Artifact-Type Packs Source: https://openwop.dev/spec/v2/core/artifact-type-packs.html > **Status: Stable.** > **Normative home:** `artifactTypes`. #### Why this exists An artifact-type pack binds an `artifactTypeId` to a JSON Schema, an advisory rendering hint and export formats, so two hosts mean the same thing by one type. The manifest is `schemas/v2/artifact-type-pack-manifest.schema.json`, whose descriptions carry the per-field rules; installation and signing follow [packs.md](https://openwop.dev/spec/v2/core/packs.html). A registry MUST refuse a manifest that mixes `kind: "artifact-type"` with `nodes[]`, `chains[]` or `prompts[]`, with `pack_kind_invalid`. #### Schema distribution The schema at `schemaRef`, inside the signed tarball, is the source of truth. Its canonical URL and `$id` is `{HostBase}/schemas/artifacts/{artifactTypeId}.schema.json`, served as `application/schema+json`. - A host advertising `artifactTypes` SHOULD serve each installed type's schema there, and MUST for a host-registered type whose `schemaVersion` it advertises. - A tarball copy and a served copy of one `(artifactTypeId, schemaVersion)` MUST be byte-identical. At registry publish and at install, a host (invariant `artifact-schema-compile-bounded`): - MUST reject, with `pack_validation_failed`, an artifact schema exceeding its bounds on serialized size, `$ref` depth or keyword/subschema count; - MUST compile it under a wall-clock timeout; - SHOULD reject a `pattern` it cannot evaluate in linear time. #### Registration A host advertising `artifactTypes` places each `WorkflowNode.artifactType`, `nodes[].artifact.typeId` and `artifact.created.artifactType` value in one tier: **pack-registered** (an installed pack declares it; `registrationSource: "pack"`), **host-registered** (a host-native type with a host-known schema; `"host"`), or **unregistered**. - Before emitting `artifact.created` for a registered type, the host MUST validate the payload against its schema as written, per its `validation`. On failure it MUST NOT emit and MUST surface the error; on success it sets `registered: true` and the matching `registrationSource`. - A host MAY emit `registered: true` for a host-registered type only if it serves that type's schema. - A host MUST NOT reject or schema-validate an unregistered value. It SHOULD emit `registered: false` and SHOULD log an unresolved-type warning. - A host not advertising `artifactTypes` treats every value as an opaque string. #### The capability `artifactTypes` advertises `store`, `render` and `export` independently; `types` overrides them per `artifactTypeId`, falling back to the global values (`schemas/v2/capabilities.schema.json`). - **`store`** — every path by which a host persists an artifact of a registered type MUST emit `artifact.created`; a host with a silent path MUST NOT advertise `store: true` for that type. - **`render`**, **`export`** — advisory. A host MUST NOT refuse to store an artifact, or fail a run, because it cannot render it. *Sources: RFCs 0071, 0075, 0141, 0145, 0205.* ### Discovery and Capabilities Source: https://openwop.dev/spec/v2/core/capabilities.html > **Status: Stable.** #### Why this exists A host advertises what it supports in one discovery document. Every capability family in it is one record type on a closed root, generated from one declaration file. That file also mints the pack peer-dependency identifiers and the `§` anchors below. Profiles are derived predicates, never a wire field. #### 1. One well-known resource `/.well-known/openwop` is one resource. Its representation MUST be selected by `OpenWOP-Version`, per [`versioning.md`](https://openwop.dev/spec/v2/core/versioning.html) §1.3: - No header ⇒ the v1 document, with `protocolVersions[]` and `preferredVersion` added, through the overlap. - `OpenWOP-Version: 2` ⇒ the closed v2 root. A single fetch answers the major the client speaks and names the other. A v2 root MUST NOT contain a v1 sub-object; one document never carries per-major sub-objects. ##### 1.1 Cache validators A host MUST emit a standard `ETag` on the discovery document and MUST honor `If-None-Match` with `304`. The v2 representation has no `Capabilities-Etag`: the document's bytes are its negotiation identity. A host that changes semantics without changing bytes is non-conformant. ##### 1.2 Removal triggers Each `deprecations.json` row carries a `removalTrigger` (`v2.0-cut | v1-end-of-support`). The following MUST be absent from the v2 representation and MUST be removed from the v1 representation at v1 end-of-support ([`overview.md`](https://openwop.dev/spec/v2/core/overview.html)): - the wrapper (`capabilities-wrapper`); - the dotted mirror (`host-dotted-mirror`); - `Capabilities-Etag` (`capabilities-etag-header`). The `/.well-known/wop` alias (`well-known-wop-alias`) carries the same trigger. #### 2. The capability record Every family at the v2 root is one object: ```json { "status": "stable" | "experimental" | "deprecated", "since": "<major>.<minor>", "until": "<major>.<minor>" | "<YYYY-MM-DD>", "witness": "witnessable-unaided" | "witnessable-gated" | "seam-gated" | "claims-check" | "negative-existence", ...facets } ``` | Field | Rule | | --- | --- | | `status`, `since`, `witness` | MUST be present | | `until` | REQUIRED when `status` is `experimental` or `deprecated`; MUST NOT be present when `stable` | | `until` in the past | Non-conformant; a validator MUST answer `400` `until_in_past` | | `witness` | MUST be one of the five wire-legal classes | | `supported` | Not a field: the record's presence is the claim, and a host that does not support a family MUST omit it | | facets | A facet is advertised by the presence of its key; a host MUST omit the key for a facet it does not offer, except where the facet's own schema states a meaning for its absence | `unwitnessable` MUST NOT appear on a wire record; such a family lives in `spec/v2/ext/` and is not advertised. A family's facets are hand-decided where `spec/v2/facets/<key>.schema.json` exists, and otherwise generated from the declaration row. #### 3. The closed root The root of `schemas/v2/capabilities.schema.json` is `additionalProperties: false`, and `protocolVersions` and `preferredVersion` are REQUIRED. Every root key is one of: - a metadata key (§3.1); - a core family (§5); - an `ext/` family (§6); - `extensions` (§3.2). Any other key MUST fail validation: no dotted key, no wrapper, no mirror, no root `profiles[]`. ##### 3.1 Metadata keys (18) `protocolVersion`, `protocolVersions`, `preferredVersion`, `extensions`, `implementation`, `engineVersion`, `eventLogSchemaVersion`, `configurable`, `observability`, `minClientVersion`, `runtimeCapabilities`, `testing`, `conformance`, `fixtures`, `compliance`, `signingKeys`, `discovery`, and `supportedTransports`. - Each is declared as metadata, with its own schema, in `spec/v2/declaration.json`. A metadata key is not a record and carries no `status` or `witness`. - `supportedTransports` is declared only to record its deletion (§4). - The version-axis keys are specified in [`versioning.md`](https://openwop.dev/spec/v2/core/versioning.html); `configurable` in [`runs.md`](https://openwop.dev/spec/v2/core/runs.html); `signingKeys` in [`conformance.md`](https://openwop.dev/spec/v2/core/conformance.html). - `implementation` (`name`, `version`, `vendor`, `url`) is self-reported: a client SHOULD NOT change behaviour because of it or authorize from it. ##### 3.2 `extensions.<org>.<name>` Vendor and host extensions live under one key, `extensions`. Its members MUST match `^[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*$` (short form `<org>.<name>`). - The orgs in `spec/v2/declaration.json` `reservedOrgs` — `openwop`, `vendor`, `effect-seams` and `events` — are reserved: a host MUST NOT use any of them. - An extension record's shape is the org's own (`additionalProperties: true` inside the record). - An extension family of §6 is advertised as `extensions["<org>.<extensionName>"]`, where `extensionName` is the kebab-case name its declaration row carries (for example `rest-transport`). The family key itself MUST NOT appear at the root. - A change to an extension that would break an existing reader MUST ship under a new key. - A host MUST NOT read one key's record as another's (A2A §4.6.3). #### 4. Deleted keys These keys are not part of the v2 root: | Key | Why | | --- | --- | | `contractProvenance` | An advisory self-declaration the wire cannot falsify | | `supportedTransports` | REST is the wire ([`interop.md`](https://openwop.dev/spec/v2/core/interop.html)) | | `grpc` | Unwitnessable, so not advertisable ([`interop.md`](https://openwop.dev/spec/v2/core/interop.html) §gRPC) | | `Capabilities-Etag`, `auth.subjectLinking`, `replay.fork`, bare `a2a.supported` / `mcp.supported`, the `openwop-core` alias | Rows `C2.1`–`C2.10` in `spec/v1/migrations.json` give each its codemod | | `host.media`, `host.collaboration` | A closed root cannot represent reserved slots | `host.workspace` is the declared family `workspace`. `minimumSuiteVersion` lives in the declaration file ([`versioning.md`](https://openwop.dev/spec/v2/core/versioning.html) axis 9). #### 4.1 The declaration file `spec/v2/declaration.json` (schema `spec/v2/declaration.schema.json`) is the single source for: - the generated `schemas/v2/capabilities.schema.json`; - each family's `witness` class and maturity; - the pack peer-dependency identifier, identical to the root key; - the spec anchor (`core/capabilities.md#<key>` or `ext/<key>/`); - the floor scenarios and requirement ids that define `openwop-core-standard`; - the profile predicates (§7). Operation paths live in `spec/v2/path-manifest.json` ([`versioning.md`](https://openwop.dev/spec/v2/core/versioning.html)). The declaration is generated from nothing and checked against everything (`scripts/check-declaration.mjs`). #### 5. Core families (73) Each heading below is a `spec/v2/declaration.json` row with `anchor: core`. `scripts/check-declaration.mjs` MUST fail when a heading here, a root key in the generated schema, or a pack peer-dependency identifier names a family the declaration does not. The peer-dependency identifier is identical to the key ([`packs.md`](https://openwop.dev/spec/v2/core/packs.html)). Under each heading: the family's witness class. Its facets are named in its normative home (§2 says where they are decided). Maturity axes are §8; the owning RFC is the row's `owningRfc`. ##### § supportedEnvelopes Witness `witnessable-gated`. ##### § schemaVersions Witness `witnessable-gated`. ##### § limits Witness `witnessable-gated`. ##### § envelopeStrictness Witness `claims-check`. ##### § envelopeContracts Witness `claims-check`. ##### § envelopes Witness `claims-check`. ##### § prompts Witness `witnessable-gated`. ##### § nodePackRuntimes Witness `claims-check`. ##### § secrets Witness `witnessable-gated`. ##### § connections Witness `witnessable-gated`. ##### § selfHostedRunner Witness `witnessable-gated`. ##### § purposePropagation Witness `witnessable-gated`. ##### § dataResidency Witness `witnessable-gated`. ##### § anonymousActor Witness `seam-gated`. ##### § credentials Witness `witnessable-gated`. ##### § feedback Witness `witnessable-gated`. ##### § replay Witness `witnessable-gated`. ##### § oauth Witness `witnessable-gated`. ##### § authorization Witness `witnessable-gated`. ##### § multiPartyConversation Witness `witnessable-gated`. ##### § conversationTurnModelProvenance Witness `witnessable-gated`. ##### § channelPresence Witness `witnessable-gated`. ##### § multiAgent Witness `claims-check`. ##### § modelCapabilities Witness `witnessable-gated`. ##### § providerUsage Witness `witnessable-gated`. ##### § aiProviders Witness `witnessable-gated`. ##### § agents Witness `witnessable-gated`. ##### § memory Witness `witnessable-gated`. ##### § conversationPrimitive Witness `claims-check`. ##### § subWorkflow Witness `claims-check`. ##### § fs Witness `witnessable-gated`. ##### § kvStorage Witness `witnessable-gated`. ##### § tableStorage Witness `witnessable-gated`. ##### § queueBus Witness `witnessable-gated`. ##### § scheduling Witness `witnessable-gated`. ##### § heartbeat Witness `witnessable-gated`. ##### § toolHooks Witness `witnessable-gated`. ##### § toolCatalog Witness `witnessable-gated`. ##### § httpClient Witness `witnessable-gated`. ##### § artifactTypes Witness `witnessable-gated`. ##### § forms Witness `claims-check`. ##### § aiEnvelope Witness `witnessable-gated`. ##### § promptLibrary Witness `claims-check`. ##### § agentRuntime Witness `claims-check`. ##### § deadLetter Witness `witnessable-gated`. ##### § webhooks Witness `witnessable-gated`. ##### § triggerBridge Witness `witnessable-gated`. ##### § a2a Witness `seam-gated`. A facet MAY name a URL on another origin; that is a claim about the facet, not the origin. `agentCardUrl` (and `mcp.serverUrls[]`) are `format: uri` with no origin constraint. - The advertiser knows where the card is, not that the named origin serves `/.well-known/openwop` or speaks any major. - The suite exercises the advertiser only, and certification is per origin. Open gap: whether a facet naming an origin that does not answer SHOULD be withdrawn, and how a client learns that origin's major. ##### § budget Witness `witnessable-gated`. ##### § nondeterminismPolicy Witness `claims-check`. ##### § workspace Witness `witnessable-gated`. ##### § uiPlugins Witness `witnessable-gated`. ##### § sql Witness `witnessable-gated`. ##### § nosql Witness `claims-check`. ##### § vectorStore Witness `witnessable-gated`. ##### § searchIndex Witness `witnessable-gated`. ##### § blobStorage Witness `witnessable-gated`. ##### § cache Witness `witnessable-gated`. ##### § workflowChainPacks Witness `witnessable-gated`. ##### § packs Witness `claims-check`. ##### § mcp Witness `seam-gated`. `serverUrls[]` MAY name other origins; the off-origin rule under § a2a applies. ##### § sandbox Witness `witnessable-gated`. ##### § compensation Witness `seam-gated`. ##### § idempotency Witness `witnessable-gated`. ##### § eventLog Witness `claims-check`. ##### § production Witness `witnessable-gated`. ##### § auth Witness `seam-gated`. ##### § auditLogIntegrity Witness `witnessable-gated`; see [security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html) §Audit-log integrity. ##### § i18n Witness `witnessable-gated`. ##### § content Witness `witnessable-gated`. ##### § portability Witness `witnessable-gated`. ##### § interrupt Witness `witnessable-gated`. ##### § runList Witness `witnessable-gated`; see [runs.md](https://openwop.dev/spec/v2/core/runs.html) §List. #### 6. Extension families (13) Rows with `anchor: ext` are documented under `spec/v2/ext/<key>/`. Each document MUST declare `witness` and both maturity axes in its header ([`overview.md`](https://openwop.dev/spec/v2/core/overview.html)). The families are `restTransport`, `a2uiSurface`, `brand`, `canvas`, `chat`, `coordination`, `dataIntegration`, `entities`, `kanban`, `knowledge`, `launchStudio`, `messaging`, `webResearch`. - Each is advertised under `extensions` (§3.2), never at the root. `a2uiSurface` is the exception: its contract is admitted through `schemaVersions.kinds`, and only its deprecated facet is an `extensions` record. - `restTransport` (`witnessable-gated`) and `a2uiSurface` (`seam-gated`) have behavioral witnesses. The other 11 are discovery-only reservations checked by `claims-check`. - An ext document's `Draft`/`Stable` label is a predicate over certified evidence ([`../ext/README.md`](https://openwop.dev/spec/v2/ext/README.html)). Becoming core takes its own RFC. #### 7. Profiles A profile is a predicate over the declaration file, published in `spec/v2/profiles.json` (generated): every listed family is present as a record, and every listed metadata key is present. | Profile | Predicate | | --- | --- | | `openwop-discovery-core` | Metadata `protocolVersions`, `preferredVersion` | | `openwop-core-standard` | Families `interrupt`, `replay`, `webhooks`, `idempotency`, `eventLog`, plus the 2.0.0 floor scenarios and requirement ids the declaration names | | `openwop-conformance-seams-v2` | The seams profile ([`conformance.md`](https://openwop.dev/spec/v2/core/conformance.html)); forbidden from the capability namespace | - The v2 root has no `profiles[]`; a host that emits one MUST fail schema validation (§3). - The facet `auth.lanes[]` ([`identity.md`](https://openwop.dev/spec/v2/core/identity.html)) replaces `auth.profiles`. `a2a.profiles[]` and `mcp.profiles[]` are facets too ([`interop.md`](https://openwop.dev/spec/v2/core/interop.html)). - The claim vocabulary is in [`overview.md`](https://openwop.dev/spec/v2/core/overview.html) (invariant `profile-claim-floor-not-overstated`). #### 8. Maturity axes | Axis | Values | Source | | --- | --- | --- | | `technical` | `experimental \| stable \| deprecated` | The record's `status`, which MUST NOT exceed the declaration row | | `adoption` | `none \| single-witness \| multi-witness \| independent` | Derived from INTEROP-MATRIX bundle evidence | - `stable` does not require a tier-3 host; `independent` records whether one exists. - There is no cap on the number of families: a family MAY exist at any count if it declares its witness class. - `memory.injectionBudget`, `toolCatalog.compactView`, and `aiProviders.promptPrefixCache` keep their families with `adoption: single-witness`. #### 9. Externally-gated `externally-gated` is the disposition for a surface held on grounds that are neither technical nor adoption, such as a legal citation or a non-steward host tripwire. - An externally-gated family MAY be declared with `status: experimental` and an `until` equal to the tripwire review date, or omitted. - It MUST NOT be `stable` (`externally-gated-never-stable`). `sandbox` is externally gated. #### 10. Migration rows Rows `C2.1`–`C2.10` are `spec/v1/migrations.json` entries. `openwop.codemod.discovery-document-v2` transforms `C2.2`–`C2.8`: it drops a family with `supported: false` and promotes a dotted-only declared family to its plain key. *Sources: RFCs 0144, 0169, 0175, 0176, 0179, 0197. Each family's owning RFC is its `owningRfc` in [`declaration.json`](../declaration.json).* ### Conformance Source: https://openwop.dev/spec/v2/core/conformance.html > **Status: Stable.** > **Normative home:** `production`. #### Why this exists The v2 evidence contract: how a requirement is asserted and witnessed, how the seams are mounted, what the suite ships, and what a bundle proves. Profiles are in [overview.md](https://openwop.dev/spec/v2/core/overview.html); the capability vocabulary the suite gates on is in [capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html). #### Requirement ids `expect(x, req('openwop.<area>.<slug>', '<doc> §<section>', '<requirement>'))` is the only assertion form. Ids are minted in `conformance/requirements.json`, and every test declares its id explicitly. - A scenario assertion without a requirement id MUST fail the suite's lint. - A title reword without a corresponding `requirement-aliases.json` row MUST fail CI. The ledger records per `it`; a bundle's `results.requirements[]` is the per-assertion list. A post-assertion soft-skip MUST record `skipped` for every id not reached and MUST NOT record `pass`. ##### Whose fact is the reason? A soft-skip carries a disposition and a reason. The reason MUST identify a fact about the host under test. - Use `inapplicable` only when the requirement does not bind that host. - A missing fixture, unreadable corpus file, or other suite-side failure MUST be `blocked`, never `inapplicable`. - A suite with a blocked row MUST NOT issue a certification. When more than one gate can skip a test, evaluate host predicates before suite predicates. #### Witness class Every family in `spec/v2/declaration.json`, every requirement in `conformance/requirements.json`, and every row of `SECURITY/invariants.yaml` MUST carry `witness` from the closed set: | Class | Meaning | | --- | --- | | `witnessable-unaided` | the suite observes it on any host with no advertisement | | `witnessable-gated` | observed when the host advertises the gating capability | | `seam-gated` | observed only through the seams profile | | `claims-check` | the host's own claim is checked for shape, not behavior | | `negative-existence` | the suite asserts a thing is absent | | `unwitnessable` | no observation path exists; `rationale` REQUIRED | - A protocol-tier invariant marked `unwitnessable` MUST fail the corpus gate. - `tests: []` is expressible only as `unwitnessable`. - `blocked` is a bundle disposition, not a witness class. - A MUST whose only witness is `seam-gated` MUST either mint a normative observation path before the cut or be demoted to SHOULD. - The seam count in `docs/witness-baseline.json` is a ratchet and MUST NOT rise. #### The seams profile Test seams are the profile `openwop-conformance-seams-v2` (`spec/v2/profiles.json`), described by `api/seams-v2.yaml` with schemas under `schemas/v2/seams/`, at `/conformance/seams/…`. The seam schemas `$ref` the canonical error and event schemas with no tolerance path. The profile is versioned with the suite (`seams-v2` for 2.x). - A host that mounts the seams MUST advertise the profile as `conformance.seamsProfile: "openwop-conformance-seams-v2"` at the discovery root (the closed root has no `profiles[]`; [capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html) §3). - A host MUST NOT advertise a `testSeams` capability flag. - `api/v2/openapi.yaml` and `spec/v2/path-manifest.json` MUST contain no seam or sample-host operation; an SDK generated from the canonical document has no seam method. #### Two products, two ledgers Corpus-coherence checks run in the spec repo's CI (`scripts/check-spec-coherence.mjs`) and MUST NOT appear in a host bundle; the bundle schema forbids their ids. `--offline` is a declared property of a scenario, not a runtime discovery. - `@openwop/openwop-conformance@2.0.0` ships `dist`, `fixtures` and `vectors` only. - The corpus — `api/`, `schemas/`, the `spec/v2/*.json` registries, `CORPUS-STAMP.json` — is `@openwop/spec-artifacts@2.0.0`, an exact-pinned peer dependency. The suite MUST digest-check it at start and MUST refuse to run against it on a mismatch. - The suite is one package: `--target-major 1|2` selects the target (default: the host's `preferredVersion`), and scenario ids share one namespace across majors. The 1.x target is removed at v1 end-of-support. #### Bundle v3 A certification bundle validates against `schemas/v2/certification-bundle.schema.json`: closed root, `bundleVersion: "3"`. | Field | Rule | | --- | --- | | `suite` | `name`, `version`, `targetMajor`, `specArtifactsVersion` REQUIRED | | `host` | `name`, `version`, `build.{kind, id}` REQUIRED; `kind` is `image-digest`, `commit` or `artifact-sha256` | | `host.deployment` | OPTIONAL; `colocated-companion` only (below). Absent: the bundle measures the served host | | `discovery` | `url`, `sha256`, `protocolVersions`, `preferredVersion` REQUIRED | | `claimedProfiles[]` | `id`, `evidenceTier` (`self` \| `steward` \| `independent`), `witnessCount`, `certified` REQUIRED | | `results` | `totals` and the per-requirement list REQUIRED | | `witnessSha256` | REQUIRED; SHA-256 of the preimage in §"Canonical JSON" | | `assertionCount` | REQUIRED, ≥ 1 | | `detail.nonPass[]` | REQUIRED when any total other than `executedPass` is non-zero | | `results.requirements[].evidence` | OPTIONAL, closed; structured evidence on an `executed-pass` row (below) | | `durability.rung` | OPTIONAL; a claim the verifier re-derives (below) | | `signature` | REQUIRED | ##### Signature and attribution `signature` is an Ed25519 attestation over the JCS bytes (§"Canonical JSON") of `{ witnessSha256, host.build, suite.version, discovery.sha256 }`; `over` MUST list exactly those four members. - A host that signs bundles MUST publish the corresponding public keys as `signingKeys[]` in its discovery document, and `signature.keyId` MUST name one of them. - A verifier MUST resolve `keyId` there — in the discovery document of the host the bundle is *about* — and MUST verify the attestation under the published key. - A retired key MUST stay listed. A signature that cannot be resolved to a published key attests **integrity only**, and the bundle MUST NOT be read as attributable evidence. A gate MUST distinguish three outcomes: *no discovery document was read*, *read and the key is not published*, and *the attestation does not verify*. `evidenceTier: independent` MUST carry a `verifierKeyId` distinct from the host's signing key. The verifier MUST refuse, not warn, on a missing or self-signed independent claim. ##### Certification - A bundle with `totals.blocked > 0` does not certify. - At major 2 a requirement a test did not observe records `blocked`, even when the test asserted setup facts first. - An `executed-pass` carrying a `partial-witness:` detail is reserved for a leg that observed its requirement and skipped an optional extra. - A verifier MUST derive the operator's opt-outs from the signed `skipped` rows and MUST reject a bundle whose captured discovery document advertises one of them (`opted-out-but-advertised`). - v1 and v2 bundles are never upgraded to v3; a bundle is evidence at its own version. ##### Colocated companion A *colocated companion* is the served host's image run beside the suite, trusting a suite-held trust anchor. - A bundle cut from one MUST carry `host.deployment: "colocated-companion"`, which the preimage covers (§"Canonical JSON"). - A host serving production traffic MUST NOT list a suite-held trust anchor among the trust roots it advertises. - A companion is evidence only for the requirements in `spec/v2/harness-trust-anchors.json`, and only when it pairs with a certified served-host bundle of the same `image-digest` build, signed under a key that bundle's discovery publishes, whose discovery document is equal once each document's origin and the `oidc` lane's `issuers` are set aside. ##### Canonical JSON Every signature and digest in this corpus is over the RFC 8785 (JCS) serialization, UTF-8 encoded. `conformance/vectors/jcs-v1.json` is normative. The value MUST be I-JSON (RFC 7493): - A signer or hasher MUST refuse, not coerce, a value with duplicate member names, a lone surrogate, a non-finite number, an integer literal whose magnitude exceeds 2^53 − 1, or a non-JSON value. - A verifier that meets one in a document it must re-canonicalize MUST fail verification. `witnessSha256` is SHA-256 over the JCS bytes of the rows: - One object per `results.requirements[]` entry, with exactly `id`, `scenario`, `result` and, each only when present, `assertions`, `detail`, `evidence`. - Rows sorted by `id` in UTF-16 code-unit order. A locale-sensitive comparator MUST NOT be used. - Only when `host.relaxations[]` is non-empty or `host.deployment` is present, the preimage is instead `{ "rows": …, "relaxations": …, "deployment": … }`, carrying each of the last two only when so, with relaxations in the order carried. `discovery.sha256` is SHA-256 over the JCS bytes of the captured document. ##### Recovery evidence A row's `evidence` enters `witnessSha256` only when present. | Row | `evidence` member | | --- | --- | | `0158.bound-is-derived` | `recoveryBounds[]` of `{ class, bound, terms[] }`, each term `{ name, ms }` | | `0158.kill-after-accept`, `0158.kill-during-execution` | `recovery: { class, boundMs, observedMs }` | - In `recoveryBounds[]`, `bound` MUST equal the sum of its terms. There is one entry per recovery class and no aggregate bound. - In `recovery`, `class` is the class exercised, `boundMs` the bound applied, `observedMs` the kill-to-resumption interval. - `class` and `name` are opaque host-chosen identifiers, never a closed vocabulary. `durability.rung` is outside the attestation, so a verifier MUST re-derive it and MUST reject (`rung-not-derivable`) a claim it cannot derive. Derivation requires every row of the rung to be `executed-pass`, and each kill row's `class` to name a declared entry whose `bound` equals its `boundMs` and is not exceeded by its `observedMs`. Only `durable-single-instance` is derivable in this revision; a higher claim is refused. The evidence does not prove the kill landed in the class it names; killing only once the execution claim is held is the host's obligation. #### Production profile `production` is the operational bar for a public host. A host claiming it: - MUST pass `openwop-core-standard`, publish the suite version and command used, and document every optional profile it claims; - MUST serve the events channel or poll, and SHOULD serve both; - MUST persist run state and event logs outside process memory, replayable after restart, including stale-claim recovery; - MUST tolerate at least five retries of one `Idempotency-Key` within its retention; - MUST log run id, tenant or project id, terminal status, error code and correlation id, and SHOULD export the `openwop.*` OTel spans and metrics. Its facets: - **`backpressure`.** At capacity it MUST answer `503 service_unavailable` with `Retry-After`. A hold beyond 24 hours SHOULD omit the header. - **`retention`.** It MUST document event-log retention, at least 7 days for snapshots and events unless labelled development-only. An expired run MUST answer `404 not_found` or `410 run_expired`, and SHOULD answer `410` when expiry is known. - **`debugBundle`.** A debug bundle (`schemas/v2/debug-bundle.schema.json`) MUST redact secrets and tokens and MUST mark truncation explicitly; the host MUST document its truncation limits. #### Corpus-gate evidence An RFC whose acceptance criteria are corpus gates rather than host scenarios records the evidence label **corpus gate — no host tier** in its `Updated` line. For such an RFC the accepted-predicate check reads `(corpus)` rows from `evidence/corpus-ledger.json` and MUST NOT require a host bundle. *Sources: RFCs 0009, 0158, 0168, 0212, 0216, 0228.* ### Connection Packs Source: https://openwop.dev/spec/v2/core/connection-packs.html > **Status: Stable.** > **Normative home:** `connections`. #### Why this exists A connection pack is a signed provider definition — the endpoints, scope catalog, and reach a connector's `auth.provider` string resolves against. The manifest is `schemas/v2/connection-pack-manifest.schema.json`; installation and signing follow [packs.md](https://openwop.dev/spec/v2/core/packs.html). #### Provider identity A `provider.id` MUST be unique per host. Built-in provider definitions are the host's own pack for every rule in this document. | Situation | Host behavior | | --- | --- | | Exactly one definition of bare id `P` | `P` resolves to it | | An installed pack and a built-in both define `P` | the later registration MUST be refused with `connection_provider_conflict` | | Two installed packs both define `P` | the later registration MUST be refused with `connection_provider_conflict` | | No definition of `P` | the dependent connector or pack MUST be refused with `connection_provider_unresolved` | A host MUST NOT choose between two claimants by comparing versions. #### The qualified form A connector MAY name a provider by its qualified form `<packName>#<id>`. A qualified reference resolves only to the named pack's definition. A bare id resolves only when exactly one definition exists on the host. #### Resolution A host advertising `connections.packsSupported` MUST resolve a connector's `auth: { type: "oauth2", provider: P }` (and any `host.oauth` invocation for `P`) against the definition selected above, to obtain its endpoints and scope catalog. - An unresolvable provider MUST be refused when the dependent connector or pack is registered. - On a publish-path host, resolution MUST run after the idempotency short-circuit: a byte-identical re-publish of an installed pack MUST succeed even when resolution inputs have since changed. #### Observation A host advertising `connections.providerRead` MUST serve two reads under `manifest:read` (`schemas/v2/connection-provider-registry.schema.json`). The registry is host-global and carries no tenant data. - `GET /connection-providers` lists every definition, built-ins included, each bare id once, and the pack registrations refused under §Provider identity, with the code and, for a conflict, the holder. - `GET /connection-providers/{providerId}` resolves one reference; `?pack=<packName>` is the qualified form. An unresolvable reference answers `404` `connection_provider_unresolved`. - A row carries ids, pack names and codes only, never an endpoint, a scope catalog or credential material. - Refusals MAY be recomputed at each start. A host advertising `packsSupported` SHOULD advertise `providerRead`. #### Errors Both codes are in `spec/v2/errors.json`: `connection_provider_conflict` (two claimants for one bare id) and `connection_provider_unresolved` (no definition for the referenced id). *Sources: RFCs 0095, 0177, 0233.* ### Conversation surfaces Source: https://openwop.dev/spec/v2/core/conversation.html > **Status: Stable.** > **Normative home:** `multiPartyConversation`, `channelPresence`, `conversationTurnModelProvenance`. #### Why this exists No client route opens a conversation. This document states the obligations a host takes on by advertising a multi-party conversation family. #### `multiPartyConversation` A host advertising `multiPartyConversation`: - MUST accept an optional `participants` array of agent references on conversation creation; - MUST refuse a turn from a principal absent from that roster, rather than silently accepting it; - MUST refuse, at creation, a roster that exceeds `multiPartyConversation.maxParticipants`, rather than truncating it. #### `channelPresence` A host advertising `channelPresence` MUST report present members as a subset of the channel's roster, and MUST NOT include a subject that is not a member. The payload is closed: it carries opaque, non-PII subject references and nothing else. #### `conversationTurnModelProvenance` A host that stamps model provenance on an agent turn MUST advertise `conversationTurnModelProvenance`. The stamp is non-secret and non-PII (provider and model identifiers only), and a host MUST NOT place prompt or completion content in it. *Sources: RFCs 0101, 0109, 0110.* ### Errors Source: https://openwop.dev/spec/v2/core/errors.html > **Status: Stable.** #### Why this exists Every error a v2 host returns is a row in one registry. A client routes on `error`, never on `message`. A code that is not registered is not a protocol error. #### The registry `spec/v2/errors.json` holds one row per code, defined by `spec/v2/errors.schema.json`. It registers **120** codes. `schemas/v2/error-envelope.schema.json` is GENERATED from it and MUST NOT be edited by hand. - A host MUST emit a registered code, or a vendor code, wherever it emits an error code: the `error` of every error response, and `error.code` on `run.failed`, `node.failed` and the snapshot's `error` (overview.md §0). A recorded event re-emitted by replay or `:fork` is carried as recorded ([replay.md](https://openwop.dev/spec/v2/core/replay.html)). - 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 [overview.md](https://openwop.dev/spec/v2/core/overview.html) §0. #### 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 shaped by the row's `details` schema. A row whose `details` is `null` accepts any object. When a row registers a schema, the generated envelope applies it to `details` only when `error` names that code. - Contextual data (conflict refs, trace ids, validation paths) MUST live under `details`, never at a new top level. - When present, `details.correlationId` MUST be a non-empty string. The same envelope is the per-id `error` of `bulkCancelRuns` ([runs.md](https://openwop.dev/spec/v2/core/runs.html)). `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. - A host MUST NOT emit `details.retryAfter`, `details.retryAfterMs` or `details.retryAfterSeconds`. - A `429 rate_limited` response MUST set `Retry-After`. The retriable rows are `residency_unavailable`, `rate_limited`, `internal_error`, `pack_registry_unreachable`, `runner_unavailable`, `service_unavailable`, `upstream_unavailable`. #### One code per state An interrupt has one code per state ([interrupt.md](https://openwop.dev/spec/v2/core/interrupt.html), [identity.md](https://openwop.dev/spec/v2/core/identity.html)): - 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`. - `interrupt_cancelled` is registered but names no state of the core resolve surfaces. A host MUST NOT emit it from `resolveInterruptByRun`, `inspectInterruptByToken` or `resolveInterruptByToken`. The idempotency mismatch code is `idempotency_key_mismatch` only ([idempotency.md](https://openwop.dev/spec/v2/core/idempotency.html)). #### Host-service refusals A `ctx.*` call that rejects MUST use a registered or vendor code. A host MAY carry an uncaught rejection unchanged as the `node.failed` code. - A generic code (`not_found`, `forbidden`, `validation_error`, `rate_limited`, `credential_not_found`, `credential_forbidden`) MUST carry `details.service`, the family key. - `details.reason` MAY name a finer cause in lower-kebab. A client MUST NOT route on it. - A vendor code MUST NOT stand for a state a registered code names. #### Unadvertised operations An operation gated on a family or facet the host does not advertise MUST answer `404 not_found`. #### Codes by HTTP status Every registered code, by HTTP status, is listed in [error-codes.md](https://openwop.dev/spec/v2/generated/error-codes.html), generated from `spec/v2/errors.json` (120 codes). *Sources: RFCs 0171, 0213, 0227, 0228.* ### Events Source: https://openwop.dev/spec/v2/core/events.html > **Status: Stable.** > **Normative home:** `heartbeat`, `envelopeContracts`, `envelopes`, `feedback`, `providerUsage`, `supportedEnvelopes`, `schemaVersions`, `envelopeStrictness`. #### Why this exists A run is its append-only event log. Every snapshot, stream, poll, fork and diff is a projection of it. There is one closed envelope, type registry, payload registry, ordering field, events channel and poll cursor. #### The envelope `schemas/v2/run-event.schema.json` (`RunEventDoc`) is closed. `eventId`, `runId`, `type`, `payload`, `timestamp`, `sequence` and `schemaVersion` are REQUIRED; `nodeId`, `engineVersion` and `causationId` are OPTIONAL. Every id field `$ref`s its grammar in `schemas/v2/ids.schema.json` ([identity.md](https://openwop.dev/spec/v2/core/identity.html)). | Field | Meaning | | --- | --- | | `sequence` | The one ordering field: integer ≥ 0, first event `0`, strictly increasing per run | | `schemaVersion` | Per-event schema version, integer ≥ 1, first-class | | `engineVersion` | Integer ≥ 0 everywhere | | `eventId` | Host-minted, opaque | | `causationId` | The `eventId`, or AI-envelope `correlationId`, that caused this event | | `timestamp` | ISO 8601 | - Persisted logs are never renumbered. - A consumer MUST treat `eventId` as a string. - A consumer MUST NOT throw on an event whose `type` it does not know; it folds what it understands and ignores the rest. #### Types `type` is `oneOf` a closed enum of registered protocol types and a vendor pattern. The enum is generated from `spec/v2/event-codemap.json` and MUST NOT be edited by hand. The vendor branch is exactly: ```text ^(?!openwop\.)[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)?$ ``` - **Naming.** A protocol type is `domain.verb-ed`: kebab-case, exactly two segments, past tense for a transition (`run.started`, `node.suspend-failed`, `run.resume-started`). `domain.noun` is permitted only for an emitted artifact (`output.chunk`, `provider.usage`, `channel.presence`, `agent.handoff`, `envelope.refusal`, `agent.reasoning-delta`, `voice.synthesis-chunk`, `voice.endpoint-candidate`); each exception is recorded in the codemap. - **Reserved prefix.** `openwop.` is the only reserved prefix. `core.`, `community.`, `vendor.`, `private.` and `local.` are pack namespaces, not event namespaces; a type under them is invalid. - **Vendor events.** A vendor type's first segment MUST be an org registered in the `extensions` object of `spec/v2/declaration.json` (the org registry, not the `extensions` metadata key a host publishes in discovery). An unregistered org fails validation. An org in `reservedOrgs` is forbidden and never registered. `extensionsKeyPattern` is the shape a vendor type must have, not a permission to use it. `example` is held by the protocol for documentation and conformance and is never assignable to a vendor. - **Growth.** The registry grows by the closed-enum rule in [overview.md](https://openwop.dev/spec/v2/core/overview.html) §0. A producer MUST NOT emit an unregistered protocol type. A consumer MUST accept an unknown registered member and MUST NOT act on it. #### Payloads `schemas/v2/run-event-payloads.schema.json` holds one `$defs` entry per payload, each `additionalProperties: false`, plus `_typeIndex`: the normative map from v2 type to `$defs` key, generated from `spec/v2/event-codemap.json`. - A host MUST emit a payload that validates against the entry `_typeIndex` names for its `type`. - Sub-typing is `$ref` composition, never duplication. `approval.*` and `clarification.*` resolve to `interruptRequested` / `interruptResolved` ([interrupt.md](https://openwop.dev/spec/v2/core/interrupt.html)); `lease.acquired`, `lease.renewed` and `lease.lost` share `leaseLifecycle`. - The CloudEvents mapping and the webhook delivery envelope are generated from the same definition. An event's `type`, `eventId`, `sequence` and `payload` are byte-identical across the run stream, a CloudEvents rendering and a webhook delivery. Specific payloads: - `run.started` carries the `owner` block ([identity.md](https://openwop.dev/spec/v2/core/identity.html) §1.1). - `run.cancelled` carries `reason`, `cancelledBy`, `durationMs`, and `parentRunId`. - `run.completed` MUST carry `outputs` as an object. An empty object is valid; an absent key is not. This separates "no outputs" from "outputs not rendered". #### AI envelopes: E1–E5 `schemas/v2/ai-envelope.schema.json` is the shape an LLM emits; the engine records its acceptance as one or more `RunEventDoc`s. - `correlationId` and `meta.source` are REQUIRED on every envelope. An engine MUST reject an envelope that omits either; nothing is synthesized. - An envelope kind MUST be namespaced under the same `<org>.` rule as events, universal kinds and the core content-primitive families `ui.*` and `media.*` excepted. The five contracts: - **E1 partial reassembly.** Every chunk of one partial emission carries the same `correlationId`, and the events that record them are ordered by `sequence`. The emission is complete at the first recorded chunk with `partial: false`. A consumer MAY render progressively but MUST NOT enable any action before that event. - **E2 multi-turn correlation.** Each turn is an envelope with its own `correlationId`; every event it produces carries `causationId = correlationId`. A re-emission with a `correlationId` already recorded in the run MUST return the cached outcome and MUST NOT emit new events. - **E3 vendor kinds.** The registry of vendor kinds is `spec/v2/declaration.json`. A kind whose org is not registered is invalid. - **E4 sub-typing.** `$ref` composition, as in the payload registry above. - **E5 refusal × retry.** `configurable.ai.maxRefusals` ([runs.md](https://openwop.dev/spec/v2/core/runs.html)) is the ceiling on `envelope.refusal` events a run records. A host MUST NOT retry the emission that produced a refusal. #### The events channel `api/v2/asyncapi.yaml` declares one channel, `runEvents`, at `/runs/{runId}/events`, and `api/v2/openapi.yaml` declares the same path (`streamRunEvents`). The two MUST resolve to the same absolute path. ##### Stream modes The `streamMode` query parameter is one pattern: ```text ^(values|(updates|messages|debug)(,(updates|messages|debug))*)$ ``` | Mode | Emits | Combines | | --- | --- | --- | | `updates` (default) | Deltas for run transitions, terminal node transitions, suspensions, `node.dispatched`, interrupt events, `artifact.created`, `eval.*`, `deployment.*`, `workspace.updated` | yes | | `values` | One synthesized `state.snapshot` (`schemas/v2/run-snapshot.schema.json`) after each `updates`-tier transition | never | | `messages` | `ai.message.chunk` (`outputChunk` payload) from streaming AI nodes only | yes | | `debug` | Every event in the log, including `log.appended`, `variable.changed`, `version.pinned`, `lease.*`, `node.retried` and every vendor event | yes | - A host MUST implement `updates` and SHOULD implement all four. - A value outside the pattern, or a mode the host does not implement, MUST return `400 unsupported_stream_mode` with `details.supported` listing each individual mode the host serves; combinations are not listed. - Validation MUST run before any content negotiation. - In `messages`, a host MUST populate a Tier 1 `meta` slot whenever it has the data. - Vendor events appear in `debug` only. - In a mixed mode the host emits the union of the filters in log order and MUST NOT reorder. Each frame SHOULD carry `event:` naming the mode that admitted it. A consumer MUST tolerate an event admitted by more than one mode. ##### SSE frames Each frame carries `id:` (the `sequence`), `event:` (the v2 `type`, in a single mode) and `data:` (the `RunEventDoc`). Three frame names are not types and are absent from the `type` enum: - `state.snapshot` — `values` mode; `data:` is a `RunSnapshot`. - `batch` — `bufferMs`; `data:` is an array of `RunEventDoc`. - `ai.message.chunk` — `messages` mode; `data:` is the `outputChunk` payload, persisted as type `output.chunk`. A host MUST set `Content-Type: text/event-stream`, MUST emit a keep-alive comment at least every 30 seconds, and MUST close the connection after the run's terminal event (`run.completed`, `run.failed`, `run.cancelled`). ###### Resuming with `Last-Event-ID` `Last-Event-ID: N` resumes every mode as an exclusive cursor, with the semantics of poll's `afterSequence`. - The host MUST stream the events with `sequence > N` in log order and MUST NOT re-emit `N`. - When `N` is at or beyond the last persisted sequence there is no backlog. On a live run the host MUST hold the stream open for later events; on a terminal run it MUST close the stream without a frame. - A host MUST NOT refuse a well-formed non-negative integer `Last-Event-ID` because no event carries that sequence. Any other value SHOULD be refused with `400 validation_error`. - The header is evaluated only after the caller is authorized to read the run. For a run the caller cannot read, the response MUST be the one the host gives without the header. - In `values` mode, resumption MUST emit a `state.snapshot` first. ###### Batching and subscribers With `bufferMs` (0..5000) the host accumulates events into one `event: batch` frame whose `data:` is an array of `RunEventDoc`. - It MUST flush on a terminal event, on `node.suspended`, and on close. - The batch's `id:` SHOULD be its highest `sequence`, and `Last-Event-ID` MUST honor that id. - A consumer MUST tolerate both a one-element batch and an unbatched frame. - A host MUST NOT limit subscribers per run except for resource protection, and then MUST answer `429 rate_limited` with `Retry-After` rather than drop silently. ##### Host events `hostEvents` carries the heartbeat messages (`schemas/v2/heartbeat-evaluated.schema.json`, `schemas/v2/heartbeat-state-changed.schema.json`) at the default address `/host/events` (`streamHostEvents`). A host MAY declare another address under `heartbeat.deliveryChannel` ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)); every channel has an address. The channel carries no run data. ###### `heartbeat` A heartbeat evaluates a predicate on an interval (at least `minIntervalSec`) and acts only on a state change. A host advertising `heartbeat` SHOULD also advertise `scheduling`, and otherwise MUST document its own interval substrate. On each tick it MUST: - skip, not queue, a tick while the prior evaluation still runs; - bound the evaluation by `maxRuntimeMs`, itself capped by `limits.maxRunDurationMs`, terminating an overrun with `status: timeout`; - pass the predicate the prior tick's state, and perform no side effect itself. The predicate MUST be a pure function of observed and prior state; - emit `heartbeat.evaluated`; - on a transition only, emit `heartbeat.stateChanged` and, if the predicate asks, call `createRun`; never on an unchanged tick. The first tick of a `heartbeatId` MUST be treated as a transition from `{}`. A durable host SHOULD persist prior state so a restart does not re-notify. #### Poll `GET /runs/{runId}/events/poll` (`pollRunEvents`) is the long-poll fallback. | Parameter | Meaning | | --- | --- | | `afterSequence` | Integer ≥ 0; return events with `sequence > afterSequence`. Omitted means from sequence 0 | | `timeout` | Seconds to wait for new events, 1..60, default 30 | `lastSequence` and `since` are not parameters. The response is `{ runId, events, lastSequence, status, isTerminal }` (closed): - `lastSequence` is the highest sequence in the log at the time of the response, `-1` when the log is empty. - `status` is the snapshot status; `isTerminal` is whether the run is terminal. - A cursor past the end of the log MUST return `200` with an empty `events` array. #### The terminal event A run's log MUST contain exactly one terminal run event: `run.completed`, `run.failed` or `run.cancelled`. - After it, the log MUST NOT contain another terminal run event, `run.started`, `run.resumed`, `run.resume-started`, `run.paused`, `run.restored-from-snapshot`, any `node.*` event or any `interrupt.*` event. - `compensation.*` events and `run.dead-lettered` MAY follow it (a compensating host unwinds after a cancelled parent). Vendor-prefixed types are unconstrained. - A host that receives work for a run whose terminal event is recorded (a duplicate delivery, a late worker) MUST NOT append forward-execution events for it, SHOULD record the refusal in its operational log, and MUST NOT surface the refusal as a run event. #### Era-2 logs An `eventLogSchemaVersion` of `2` means v1-written. Every reader, diff included, translates it per [persistence.md](https://openwop.dev/spec/v2/core/persistence.html) §"The reader rule". - A projection MUST NOT silently drop a property: it carries it or fails with `500 payload_unprojectable` (hatch `^(openwop-|x-|vendor\.)`). - Fork and replay over an era-2 parent are in [replay.md](https://openwop.dev/spec/v2/core/replay.html). #### The envelope-kind catalog `supportedEnvelopes`, `schemaVersions` and `envelopeStrictness` are one flow, read in that order on every inbound envelope. ##### `supportedEnvelopes` `supportedEnvelopes.kinds` is the catalog. A host advertising it MUST refuse an emitted `type` that is neither universal nor a member, with `unknown_envelope_kind`. An absent `kinds` is not an empty catalog and is not an unrestricted one: a host advertising `supportedEnvelopes` without it has made no catalog claim, and an engine MUST refuse every non-universal kind rather than admit it unchecked. ##### `schemaVersions` `schemaVersions.kinds` maps a kind to its advertised floor; a kind absent from the map has a floor of `0`. An emitted `schemaVersion` above the floor MUST be refused with `unknown_schema_version`, whatever the strictness. `ui.a2ui-surface` at schema version 2 is specified by `ext/a2uiSurface/README.md`. ##### `envelopeStrictness` `envelopeStrictness.mode` governs drift below the floor only. A host MUST NOT read an absent `mode` as "no checking". - Under `warn` (the value when the seat is absent), an engine MUST validate against the advertised version and log `envelope_schema_version_drift`. - Under `strict`, the same condition MUST refuse with `unknown_schema_version`. #### Envelope contracts `envelopeContracts` advertises that a host enforces per-node envelope permission sets. A host advertising `envelopeContracts.advertised` MUST refuse a node whose emitted envelope `type` is neither universal nor listed in that node's accepted set. It MUST refuse it distinctly from the capability-gated `typeId` refusal: the two stack rather than substitute. #### Envelope, feedback and usage facets ##### `feedback` `feedback.signals` lists the signal kinds a host accepts; absent means all four. `feedback.targets` names the resources an annotation may be attached to. A host advertising `feedback` MUST: - accept an annotation on a terminal run; - refuse an annotation whose target is outside the advertised `targets`, and MUST NOT write it to the replayable run event log; - keep annotations visible only within the run's tenant (invariant `annotation-cross-tenant-isolation`); - redact secret-shaped material in `signal.correction` and `note` before persistence, listing and export (invariant `annotation-content-redaction`); - audit-log each recording with the acting principal. ##### `providerUsage` A host advertising `providerUsage` MUST emit exactly one `provider.usage` per LLM provider invocation, before that node's `node.completed`. A host that does not advertise it omits the event. - `inputTokens` and `outputTokens` MUST replay identically; `costEstimateUsd` MAY be omitted on replay. - **`providerUsage.costEstimates`** advertises that the host stamps a derived cost on the event. That figure is an estimate from the host's own rate table; a consumer MUST NOT treat it as a billed amount. - The payload MUST NOT carry credential refs, hashed credential identifiers, or prompt or response text (invariant `provider-usage-no-credential-leak`). - `providerUsage.currency` is the ISO 4217 currency of `costEstimateUsd`; absent means USD. ##### `envelopes` **`envelopes.tierOneSubsetCompliance`** advertises that the host accepts the Tier 1 structured-output subset shared across major providers. A host advertising it MUST accept an envelope restricted to that subset from any provider it advertises, rather than refusing on provider-specific grounds. `envelopes.reasoning` advertises the host's prompt posture for the optional payload field `reasoning`. - `reasoning` SHOULD be the first property where the schema dialect orders properties. A vendor kind whose payload needs multi-step reasoning SHOULD declare it as optional. - Where a payload schema permits it, a host SHOULD instruct the model to populate it. A host MUST NOT reject an envelope without it, whatever `promptDirective` says. - A host MUST NOT route on `reasoning`. A known secret in the input MUST NOT appear verbatim in it, in derived events, span attributes or the debug bundle (invariant `envelope-reasoning-secret-redaction`). A host advertising `envelopes.reliability`: - MUST emit `envelope.retry-exhausted` and `envelope.refusal`, and list in `reliability.events[]` both of them and only the other reliability events it emits; - SHOULD emit `envelope.retry-attempted` before each retry past the first; - MUST pass `previousError`, `finalError` and `refusalText` through the secret redaction applied to envelope payloads (invariants `envelope-refusal-no-prompt-leak`, `envelope-recovery-no-content-leak`); - MUST replay `totalAttempts`, `outputTokenCount` and the recovery `path` identically. `refusalText` MAY differ, and a consumer MUST tolerate `null`. Under `reliability.completion.distinguishesTruncation`, an envelope is complete only on a clean provider stop with a payload that validates against its kind's schema. - **Truncation** (no clean stop, whatever the payload): the host MUST emit `envelope.truncated`. A retry SHOULD raise the output budget, by `truncationBudgetMultiplier` (default 2), and MUST NOT carry a schema-correcting fragment. - **Schema violation** (clean stop, invalid payload): a retry's corrective fragment SHOULD reproduce the validator's failure and MUST NOT contain text from the model's output. The retry MUST NOT raise the output budget. - **Refusal** is terminal: the node MUST fail with `envelope_refusal`. - Each retry past the first MUST emit `envelope.retry-attempted` with that `reason`. Both paths count against `limits.schemaRounds`. - On exhaustion the host MUST emit `envelope.retry-exhausted` with that `finalReason` and `cap.breached` `kind: "schema"`, and fail the node with `envelope_truncation_unrecoverable` or `envelope_invalid`. - Lenient-parse recovery consumes no retry. It emits `envelope.recovery-applied` once per recovery, carrying only the path and an optional byte offset. *Sources: RFCs 0026, 0030, 0032, 0033, 0056, 0060, 0171, 0172, 0176, 0185, 0194, 0213.* ### Execution Source: https://openwop.dev/spec/v2/core/execution.html > **Status: Stable.** > **Normative home:** `selfHostedRunner`, `subWorkflow`, `multiAgent`, `agents`. #### Why this exists A step can run on a user's machine, in a child run, or under a supervisor loop; the host stays the orchestration, persistence and replay authority. #### `selfHostedRunner` A runner is a user-operated process that dials out, holds credentials the host cannot reach, and executes single model or tool steps. On host-defined paths, it receives dispatch frames over SSE and POSTs result frames. A host MUST NOT advertise `selfHostedRunner` unless it accepts registrations, routes matching dispatch and delivers results. - **Records.** A registration (`schemas/v2/self-hosted-runner-registration.schema.json`) MUST NOT appear in discovery. `dispatchKinds` lists `model`, `tool` or both. - **Subject isolation.** A host MUST NOT route a step to a runner its run's subject does not own, and MUST match on subject before capability. - **Credentials.** A runner credential MUST NOT transit the host or appear in any frame, event, result, debug bundle or log. The runner bearer MUST NOT be a provider credential. - **Untrusted output.** Runner output re-entering an agent loop MUST be fenced as untrusted. - **At most once.** A frame's `seq` is a per-runner cursor, resumed by `Last-Event-ID`, and MUST NOT be conflated with event `sequence`. The host MUST drop, not re-dispatch, a `{runId, stepId}` already persisted; the runner MUST answer under the pair it received. - **Liveness.** Dispatch to a departed runner MUST fail with retriable `runner_unavailable`, never hang. - **Replay and fork.** Replay MUST NOT re-dispatch. A fork MUST NOT pin later dispatch to the original `runnerId`. #### `subWorkflow` `core.subWorkflow` starts a child run of another workflow and waits for its terminal status. - **`workflowId`.** A host MUST refuse the parent run with `node_config_invalid` when that workflow is not loaded. - **`waitForCompletion`** defaults to `true`; a host MAY refuse `false` with `validation_error`. - **`onChildFailure`.** `fail-parent` (default) fails the node and the run; `absorb` records the failure and continues. - **Output.** `node.completed` MUST carry `outputs.childRunId` and `outputs.childStatus` (`completed | failed | cancelled`), and MAY add fields. - **`inputMapping`** (`childVar → parentVar`) seeds the child once, at creation, after and over its `variables[].defaultValue`, which MUST seed first. An unset parent variable MUST arrive undefined, never an error or `null`. A host not advertising `subWorkflow.inputMapping` MUST refuse a non-empty `inputMapping` at registration with `validation_error`, naming it in `details.requiredCapability`. - **`outputMapping`** (`parentVar → childVar`). After the child completes, the host MUST copy each mapped variable into the parent, without throwing on or copying an undefined one. - **`propagateCancellation`** (default `true`) cancels the child with its parent. - **Parent link.** Where `getRunAncestry` is served, the child's `parent` MUST be the parent run, with `cause: "core.subWorkflow"`. It does not name the dispatching node; that node's `node.completed` carries `outputs.childRunId`. `parentRunId` is fork lineage, not this link. #### `multiAgent` `multiAgent.executionModel.version` is cumulative: a host advertising `N` MUST implement levels 1 through `N`. Each level adds: 1. the supervisor loop and worker handoff; 2. confidence escalation, and memory across sub-runs; 3. cross-host causation; 4. replay determinism under nondeterministic models; 5. stateful loop lifecycle and context budget; 6. verifier turn and convergence. At level 1, a workflow whose `core.orchestrator.supervisor` feeds `core.dispatch` runs this loop: - Each turn records one decision as `runOrchestrator.decided`. `terminate` completes the run; `clarify` and `escalate` suspend on a `clarification` or `approval` interrupt; `next-worker` dispatches each of `nextWorkerIds[]` as a child run, and the next turn waits for all of them. - The loop MUST be re-entrant: replay from any iteration MUST reproduce its state. - A worker moves `pending → dispatching → running → harvested`, or ends `failed` or `cancelled`. Each transition that occurs MUST emit `core.workflowChain.event` whose `causationId` names the prior transition, first `runOrchestrator.decided`. A host MUST NOT synthesize a phase it never produces. - `output.harvested` fires exactly when a child completes with a non-empty `outputMapping`. - A host not advertising `executionModel` MUST NOT emit `core.workflowChain.event`. At level 2: - A `next-worker` or `terminate` decision whose `confidence` is below the floor (`confidenceEscalationFloor`, else `0.5`) MUST NOT execute silently. The host MUST record `core.workflowChain.confidence-escalated`, then fire a `clarification` interrupt (preferred) or an `approval` interrupt, both before any `dispatch.began` for that decision. An absent `confidence` MUST NOT trigger escalation. - With `memory` also advertised, a child of `core.dispatch` or `core.subWorkflow` MUST keep memory scoped per `(tenantId, scopeId)` ([host-services.md](https://openwop.dev/spec/v2/core/host-services.html) §`memory`). When it shares its parent's scope, its writes are visible to the parent from its completion and on later supervisor turns, and an entry's `ttl` MUST run from the child's write time. - The host MUST serialize sibling children's writes to the shared scope per parent run. A host advertising `crossChildMemoryConcurrency: "advisory"` is exempt, and SHOULD document last-write-wins. - The host MUST persist memory snapshots by log index: before `fromSeq`, a fork's memory reads MUST return the source run's memory as of `fromSeq`, or the fork is refused ([replay.md](https://openwop.dev/spec/v2/core/replay.html)). At level 3, with `crossHostCausation` advertised: - An event whose `causationId` names an event on another host MUST carry `causationHostId`, equal to that host's `crossHostCausation.hostId`; a same-host one MUST NOT. - The host MUST propagate the run's trace context into every outbound MCP request and A2A message, and adopt an inbound one, per [interop.md](https://openwop.dev/spec/v2/core/interop.html). - Under `ancestryEndpointSupported` the host MUST serve `getRunAncestry` ([runs.md](https://openwop.dev/spec/v2/core/runs.html)); otherwise it returns `404` and clients walk `causationHostId`. At level 4, every LLM-calling node MUST meet [replay.md](https://openwop.dev/spec/v2/core/replay.html)'s invocation key, refusal-divergence and observable-result rules. At level 5: - Every `runOrchestrator.decided` MUST carry `iteration`, 1-based and incremented by exactly 1 per turn; `maxLoopIterations` bounds it ([runs.md](https://openwop.dev/spec/v2/core/runs.html)). - Turn *i*'s inputs MUST be reproducible on replay: memory as of its log index, the workspace snapshot when `workspace` is advertised, and the log tail bounded by `transcriptWindow`. A turn's writes MUST become visible to turn *i+1*, never to turn *i*. - Under `statefulResume`, a resumed loop MUST continue at the same `iteration` with the same snapshot lineage. A heartbeat MAY enqueue a fresh loop run and MUST NOT advance a suspended one. - `contextBudget` bounds that transcript in tokens, per its `schemas/v2/capabilities.schema.json` descriptions and `context.summarized`. A replay MUST reuse a recorded summary, never re-summarize. At level 6, with `verifier` advertised: - A critic MUST emit content-free `agent.verified` over a prior result: an `agent.decided` event, a child run or a tool call. - Under `verifier.gating`, a `fail` MUST NOT be merged or `terminate` as success, and a `revise` SHOULD route back to an actor turn within `maxLoopIterations`. A missing verdict is not a failure. Without `gating`, verdicts are observational. - A consumer MUST NOT treat a `terminate` whose `successCriteria` has a `met: false` entry as goal-satisfied. #### `agents` `agents` and each of its facets are optional; a client MUST tolerate their absence. A host advertising `agents` serves `RunSnapshot.agent` and `runOrchestrator`, emits the `agent.*` family, and suspends a low-confidence `agent.decided`. Each facet's rules are its description in `spec/v2/facets/agents.schema.json`, with the records it names under `schemas/v2/`: | Facets | Records | | --- | --- | | `profile`, `modelClasses`, `orchestratorPattern`, `orchestrator`, `dispatch`, `dispatchMapping`, `reasoning`, `subRunAttestation` | `dispatch-config` | | `manifestRuntime`, `liveRuntime` | `agent-manifest`, `agent-inventory-response` | | `memoryBackends`, `memoryConsolidation`, `commitments` | `memory-entry` ([host-services.md](https://openwop.dev/spec/v2/core/host-services.html)) | | `evalSuite` | `agent-eval-suite`, `eval-summary` | | `deployment` | `agent-deployment`, `agent-deployment-transition`, `agent-ref` | | `roster`, `orgChart` | `agent-roster-entry`, `agent-org-chart` | | `proposals`, `goals` | `proposal`, `goal` | A host MUST NOT advertise `liveRuntime` or `roster` without `manifestRuntime`, `orgChart` without `roster`, or `memoryConsolidation` without `long-term` in `memoryBackends`. `roster.installScope` MUST equal `manifestRuntime.installScope`; `orgChart.installScope` SHOULD equal it. *Sources: RFCs 0002, 0007, 0022, 0037, 0039, 0040, 0041, 0061, 0068, 0077, 0086, 0087, 0090, 0111, 0122, 0228.* ### Form Content Packs Source: https://openwop.dev/spec/v2/core/form-content-packs.html > **Status: Stable.** > **Normative home:** `forms`. #### Why this exists A form-content pack ships declarative form templates a host renders in its own chrome. The manifest is `schemas/v2/form-content-pack-manifest.schema.json`; installation and signing follow [packs.md](https://openwop.dev/spec/v2/core/packs.html). #### Conditional visibility A field MAY carry `when: <EdgeCondition>`. The grammar is the `WorkflowEdge.condition` object `{ type, left, right }` of `schemas/v2/workflow-definition.schema.json`, with the operator set of [workflow-chain-packs.md §"Edge conditions"](https://openwop.dev/spec/v2/core/workflow-chain-packs.html). A host MUST evaluate `when` with its edge-condition semantics and MUST NOT accept any other expression language for visibility. ```jsonc { "id": "region", "type": "select", "label": "Region", "when": { "type": "equals", "left": "fields.shipping", "right": "international" } } ``` #### Instantiation A form instantiated from a pack advertised through `forms.contentPacks` MUST be created through the same path a hand-authored form uses, and MUST remain editable afterwards: the pack is a starting point, not a managed object. - A host MUST degrade an unrecognized field type to plain text rather than failing the instantiation. - A host MUST NOT execute anything carried by the pack. - A pack-authored string, and any value collected through an instantiated template, is untrusted input. When one is interpolated into a prompt, the composed envelope MUST carry `meta.contentTrust: "untrusted"`, so a downstream reader can tell authored text from pack-supplied text. #### Localized strings `label`, `title`, and `description` are localized strings. A host MUST select the rendered language by the locale-selection and fallback rules of [i18n.md](https://openwop.dev/spec/v2/core/i18n.html), and MUST treat every rendered string as untrusted: escaped for the target surface, never interpreted as markup, script, or a template directive. #### Validation | Constraint | Applies to | Rule | | --- | --- | --- | | `required` | any type | the value MUST be present | | `format` | `text`, `longtext` | one of `email`, `uri`, `date`, `date-time`, `time`, or `x-<format>` | | `minLength` | `text`, `longtext` | a host MUST reject a shorter value | | `min`, `max` | `number` | a host MUST reject a value outside the closed range | | `pattern` | `text`, `longtext` | a host MUST reject a non-matching value | The five spec-reserved `format` values are the core set. A host that recognizes a format SHOULD apply it; one that does not MUST ignore it and accept plain text. A host MUST ignore `format` on any type other than `text` or `longtext`. *Sources: RFCs 0137, 0177.* ### Headers Source: https://openwop.dev/spec/v2/core/headers.html > **Status: Stable.** Generated from `api/v2/openapi.yaml` by `scripts/derive-v2-api.py`; do not edit. #### Why this exists Every non-standard header is named `OpenWOP-<Name>`, and every header is declared in OpenAPI. These tables are generated from that declaration, so they list all of them: a header not listed here is not part of the protocol. Standard headers keep their standard names: `Idempotency-Key`, `ETag`, `If-None-Match`, `Last-Event-ID`, `Retry-After`, `Authorization`. #### Request headers | Header | Operations | Meaning | | --- | --- | --- | | `Accept-Language` | 1 | BCP-47 preference list; authoritative for locale selection (i18n.md). A malformed value MUST NOT produce a 400. | | `Idempotency-Key` | 15 | Per-mutation idempotency token (`idempotency.md` Layer 1). The server caches `(tenantId, endpoint, key)` → response for ≥24h. A duplicate request returns the cached response with `OpenWOP-Idempotent-Replay: true`. | | `If-None-Match` | 2 | Conditional request on the discovery document (capabilities.md §1) and the run snapshot (runs.md §Snapshot). A value matching the `ETag` the host sent MUST yield `304 Not Modified` with no body. The 304 carries `OpenWOP-Version` like every response (versioning.md §1.4). | | `Last-Event-ID` | 1 | Resume from sequence after this ID. | | `OpenWOP-Client-Version` | 58 | The protocol version the client implements (versioning.md §1.5). Compared with `minClientVersion` on major.minor. A malformed value is treated as absent and MUST NOT produce a 400. Never selects a contract. | | `OpenWOP-Dedup` | 1 | When set, the host's cross-host claim system rejects a duplicate `(tenantId, scopeId)` pair with `409 Conflict`. | | `OpenWOP-Force-Engine-Version` | 1 | Test keys only. The server emits this run's events as if it ran the given engine version, which must be within `Capabilities.testing.forceEngineVersionRange`. Servers MUST reject it on production API keys with `403 force_engine_version_forbidden`. | | `OpenWOP-Version` | 58 | Selects one of the host's listed major.minor versions. Absent, `/.well-known/openwop` uses `preferredVersion` and every other unversioned path is v2; an unlisted value is 406 protocol_version_unsupported. | #### Cross-origin preflight A host that answers a CORS preflight for an operation by granting the requesting origin (`Access-Control-Allow-Origin`) MUST admit the operation's method in `Access-Control-Allow-Methods` and, in `Access-Control-Allow-Headers`, every request header `api/v2/openapi.yaml` declares for it, `Authorization` when it is authenticated and `Content-Type` when it takes a body, by listing them or by reflecting `Access-Control-Request-Headers`. A `*` does not admit `Authorization`. Which origins a host grants, and whether with credentials, is host policy. Admitting this table's union on every operation conforms. #### Response headers | Header | Operations | Meaning | | --- | --- | --- | | `Cache-Control` | 2 | Standard HTTP caching directive (RFC 9111), set per operation: public content-page delivery, and prompt templates (immutable semantics when the version was pinned); see each operation. | | `Content-Encoding` | 1 | Present only when the host negotiated compression from `Accept-Encoding`; pairs with `Vary: Accept-Encoding`. The decoded body is byte-identical to the identity body. | | `Content-Language` | 1 | The BCP-47 locale actually used (equals the response `locale`). | | `ETag` | 3 | Standard HTTP validator (RFC 9110 §8.8.3). The obligation is per operation: MUST on the discovery document (capabilities.md §1), SHOULD on the run snapshot (runs.md §Snapshot), a content hash on a prompt template (getPromptTemplate); see each operation. | | `Location` | 1 | Canonical URI of the new template. | | `OpenWOP-Idempotent-Replay` | 1 | Set when the response was served from the idempotency cache. | | `OpenWOP-Version` | 58 | The contract that produced this response. It MUST equal the one used. | | `Retry-After` | 1 | Seconds until the active claim is stale-eligible. | | `WWW-Authenticate` | 51 | A `Bearer` challenge. On a host with an oauth2 or oidc lane it carries `resource_metadata` and `error="invalid_token"`. Never sent on a non-disclosure 404. | #### Webhook delivery headers Declared in `webhooks.md`, not in OpenAPI, because the host is the client: - `OpenWOP-Webhook-Id`, `OpenWOP-Event-Type`, `OpenWOP-Timestamp`, `OpenWOP-Signature`, `OpenWOP-Signature-Algorithm`. - On a subscription that opted into Standard Webhooks (webhooks.md): that standard's `webhook-id`, `webhook-timestamp` and `webhook-signature`, under their standard names. - During the v1 overlap only: the `X-openwop-*` family, emitted beside them and removed at v1 end-of-support. #### Removed in v2 These header names are not part of v2: `Capabilities-Etag` (use the standard `ETag`/`If-None-Match` pair), `X-Dedup`, `X-Force-Engine-Version`, `X-Pack-Sha256`, `X-Pack-Signing-Method`, `X-openwop-*` (webhooks), `openwop-Webhook-Signature`. *Sources: RFC 0165, RFC 0171, RFC 0172, RFC 0201.* ### Host services Source: https://openwop.dev/spec/v2/core/host-services.html > **Status: Stable.** > **Normative home:** `aiEnvelope`, `promptLibrary`, `prompts`, `aiProviders`, `agentRuntime`, `mcp`, `workspace`, `secrets`, `modelCapabilities`, `scheduling`, `queueBus`, `toolHooks`, `httpClient`, `memory`. #### Why this exists A node pack invokes advertised `host.*` services through `ctx`. Each section below states the contract a host takes on by advertising that family. #### `aiEnvelope` A host advertising `aiEnvelope` MUST expose `ctx.aiEnvelope.generate` to pack code, and MUST expose `ctx.aiEnvelope.await` when, and only when, it advertises `aiEnvelope.await`. A host MUST refuse a node whose `typeId` requires the family when it does not advertise it. #### `promptLibrary` A host advertising `promptLibrary` MUST expose `ctx.promptLibrary.get` and MUST return a pinned version verbatim, so a replayed run resolves the same template. It MUST fail the calling node rather than substitute an unpinned version. #### `prompts` A host advertising `prompts` resolves the PromptRefs in node `config` (`systemPromptRef`, `userPromptRef`, `fewShotPromptRefs`, `schemaHintPromptRef`); to any other host they are opaque strings. - `templateKinds` and `variableSources` narrow what the host accepts; the `secret` source SHOULD appear only with `secrets`. - `maxTemplateBytes` MUST NOT exceed 65536. `observability` is `off`, `hashed` (default) or `full`. - `endpointsSupported` gates the `/prompts*` operations, `mutableLibrary` their writes and `packsSupported` pack installs. An operation whose gate is off answers `404 not_found`. - `library` carries `id`, `renderEndpoint` and `maxRenderRequestBytes`. ##### Resolution For each `(nodeId, kind)`, the host MUST take the first non-null ref from these layers, and MUST emit `agent.prompt-resolved`, with one `chain[]` entry per layer tried, before `prompt.composed`: 1. node `config`, where a ref beats an inline body; 2. under `agentBindings`, the agent `config.agentId` names: for `system`, first its own prompt (tagged `agent-intrinsic`), then its `promptOverrides`, and last the host MAY use a `promptLibraryRef` default. An unknown agent MUST be logged as a warning and skipped; 3. the workflow's `defaults.promptRefs[kind]`; 4. `prompts.defaults[kind]`. When every layer is null, the node fails only if its node type says so. A host honoring `ai.promptOverrides` applies it before any layer and MUST record a `run-configurable` entry. ##### Composition - `{{varName}}` substitution is literal. An unbound required variable MUST fail the node (`node_config_invalid`); an unbound optional or undeclared one renders empty, and install SHOULD warn on an undeclared one. - `secret` values MUST appear only as `[REDACTED:<secretId>]` in observability output. - Untrusted input MUST be wrapped verbatim in `<UNTRUSTED>…</UNTRUSTED>`, which makes `contentTrust` `untrusted`. - Unless `observability` is `off`, the host MUST emit `prompt.composed` for each composition, carrying bodies only under `full`. - On replay, `hash`, `variableHashes`, `refs`, the resolved ref and `chain[].applied` MUST match the recording, and a mismatch MUST emit `replay.diverged`. Bodies MAY be omitted. `chain[].source` SHOULD match, and a rotated `host-defaults` source MUST be tolerated. ##### Library - A ref without a version resolves to the latest. A string ref matching several templates MUST be refused with `validation_error`, `details.field: libraryId`. - `renderPromptTemplate` dispatches nothing, and its `hash` MUST equal the dispatch-time `prompt.composed` hash. - `getPromptTemplate` SHOULD send `ETag` and `max-age=60`, plus `immutable` when `version` is pinned. - Writes MUST be authenticated and SHOULD be role-scoped. An update MUST carry a greater SemVer; built-in and pack templates are read-only. - For a workspace-scoped read or write, the host MUST verify membership from the authenticated identity, never from a caller's `workspaceId`. The check fails closed (canonically `403 workspace_membership_required`) and runs in the application tier, even behind a privileged database client. - A `kind: "prompt"` pack MUST NOT carry `nodes[]` or `chains[]`; its templates MUST carry `meta.packName` and `meta.packVersion`. #### `aiProviders` A host advertising `aiProviders` MUST expose `ctx.callAI`. A `provider` that a `ctx` call names MUST be in `providers`; `byok` is in [runs.md](https://openwop.dev/spec/v2/core/runs.html) §`ai` section. - A part whose modality is absent from `input` MUST be rejected with `capability_not_provided`, never dropped. Non-text input is untrusted. - A `media.*` envelope inlines base64 only up to `maxInlineMediaBytes` (default 256 KiB); above that the host MUST use a `url`. - `imageGeneration`, `videoGeneration` and `speechSynthesis` add `ctx.callImageGenerator`, `callVideoGenerator` and `callSpeechSynthesizer`; `realtimeVoice` adds `callTranscriber` and streamed synthesis. Unadvertised synthesis or transcription MUST be rejected, never a no-op or whole-file fallback. - A video caller MUST honor `ctx.signal`. Synthesis MUST return exactly one of `url` or `base64`, and a `url` is served through the SSRF guard. The transcriber MUST reject inline or `mediaRef` audio. - `voiceId`, `streamRef` and `cachePrefixId` MUST NOT encode secrets. Transcripts are untrusted; an interim one MUST NOT be persisted or drive a side-effecting tool. `selfHosted` (operator-configured OpenAI-compatible endpoints) MUST be advertised only while one is configured and reachable. A host MUST NOT disclose an endpoint's location on any wire surface or in a provider id. A client MUST NOT infer capabilities from the id; an unadvertised one fails `capability_not_provided`. `authModes` (`apiKey`, `oauth-pkce`, `oauth-device`, `none`, `subscription`) is capability, not policy: a client MUST ignore an unknown mode and MUST NOT infer policy from one. - OAuth and `subscription` credentials go by `ref`, never key material; an OAuth mode SHOULD come with `oauth`. - A `subscription` credential MUST bind at user scope; a tenant or workspace binding MUST be rejected with `credential_scope_forbidden`. - A host MUST NOT advertise `subscription` without a reachable acquisition mechanism. `policies.modes` lists the enforced modes; a client MUST tolerate any subset, and absence means `optional` only. The host MUST document its `scopes` precedence. - `disabled` always refuses (`provider_disabled`). - `required` refuses without a `credentialRef` (`byok_required`) or a usable secret (`byok_required_but_unresolved`). - `restricted` refuses a model matching no `allowedModels` glob (`model_not_allowed`). An empty `restricted` policy MUST fail closed. A refusal MUST carry `policies.errorCode` (default `provider_policy_denied`; any other value is a vendor code), SHOULD carry `reason`, and MUST NOT echo the policy. Each decision SHOULD be audited. A resolver outage SHOULD fail open. A host advertising `promptPrefixCache` MAY honor `cachePrefixId` per routed provider, and otherwise MUST ignore it. It MUST key the cache by (authenticated tenant, `cachePrefixId`) and MUST NOT persist prompt or response substrings keyed by it. The envelope and `provider.usage` token counts MUST match on hit and miss, and on replay. #### `agentRuntime` A host advertising `agentRuntime` MUST expose `spawn`, `delegate`, `consensus` and `messageSend`. Advertising it implies `agents.manifestRuntime`, which the host MUST satisfy. #### `mcp` A host advertising `mcp.client` MUST expose to pack code `ctx.mcp.callTool`, `listTools`, `readResource` and `serverHealth`, each against a host-configured `serverId` at the revision `mcp` negotiates. Each rejects only for an unknown `serverId` (`not_found`), an MCP error response (carried unaltered), or a transport failure (`upstream_unavailable`). - `callTool` MUST resolve to the server's `CallToolResult` unaltered (`content[]`, `structuredContent`, `isError`, `_meta`), including when `isError` is true. The host handles an `InputRequiredResult` itself and never returns one. - `listTools` MUST resolve to one `ListToolsResult` page unaltered, `outputSchema`, `annotations`, `nextCursor`, `ttlMs` and `cacheScope` included, and MUST forward a pack's `cursor`. - `readResource` resolves to the `ReadResourceResult` unaltered. - `serverHealth` MUST report `reachable`, `unreachable` or `incompatible` from a `server/discover` probe no older than its `ttlMs`, with the `DiscoverResult` when one was received. It MUST NOT report a connection or session state. #### `secrets` A host advertising `secrets` MUST resolve secrets to opaque references. Raw key material MUST NOT appear in any event, log, trace, prompt, error, export or screenshot, and the host MUST test this before exposing BYOK. `scopes` lists the storage scopes the host implements, and a client MUST tolerate any subset. `resolution` is `host-managed`. A host offering `resolveInPack` MUST expose `ctx.secrets.resolve({ ref, purpose })`, returning `plaintext` and optional `expiresAt` and `rotatedAt`. It: - MUST keep the plaintext out of events, spans, logs, snapshots and replay state. A replay re-resolves it, and the host SHOULD record only `ref`, `purpose` and time; - MUST resolve `ref` only to a credential the calling run may read, fail a `ref` from another workspace, and never substitute another credential; - MUST reject a missing, revoked or expired secret with `credential_not_found`, a denied one with `credential_forbidden`, and an exhausted quota with `rate_limited`. A pack MUST pass a non-empty `purpose`, which the host audits. The pack MUST NOT log the plaintext, keep it past the consuming call, or pass it to any other `ctx` method, and MUST treat it as run input that may differ between runs. ##### Run-supplied secrets A host advertising `runSecrets: { maxEntries }` accepts `runSecrets` on `createRun` ([runs.md](https://openwop.dev/spec/v2/core/runs.html) §Create): an array of `{ ref, value }`. It MUST also list `run` in `scopes`. A client MUST NOT send `runSecrets` to a host that does not advertise it. - **Bounds.** `ref` MUST match `^run:[A-Za-z0-9_.-]{1,64}$`, and `value` is a string of 16 to 4096 characters. The array holds at most `maxEntries` entries, each `ref` once. A request that breaks any of these MUST be refused `400 validation_error`, naming the field and never the value. - **Bound to the run.** A `run:` ref MUST resolve only to a value in the `runSecrets` of the run resolving it, never from another run, a user, tenant, workspace, the platform or the process environment. An unsupplied `run:` ref MUST fail `credential_not_found`. A ref without the `run:` prefix MUST NOT resolve to a `runSecrets` value. - **Lifetime.** The host MUST NOT write a value to any store an operator or client can read in cleartext, and MUST discard it by the time the run is terminal. A fork does not inherit it, so a `run:` ref in the fork fails `credential_not_found`. - **Redaction.** A value is a resolved run-scoped secret, and every redaction rule of this section and of §`memory` binds it. The host MUST NOT echo it on any response, the `createRun` answer and the snapshot included; a read MAY return the refs. It MUST NOT log the request's values. - **No digest.** A value MUST NOT enter any hash, digest, fingerprint or cache key the host persists or derives from the request, including any replay, witness or audit digest. The idempotency request digest ([idempotency.md](https://openwop.dev/spec/v2/core/idempotency.html)) is computed with `runSecrets` removed. A same-key retry that differs only there compares equal, is answered as any duplicate is, and its values are discarded unused. A host advertising `runSecrets` MUST execute the node type `core.secret.witness` and MUST advertise the fixture `openwop-secrets-run-witness`. The node's configuration is `{ ref, expectedSha256 }`. - A `ref` without the `run:` prefix MUST fail `credential_forbidden` without resolving anything. - Otherwise the node resolves `ref` under the rules above and outputs `{ matched }`: whether the lowercase-hex SHA-256 of the value's UTF-8 bytes equals `expectedSha256`. - It MUST NOT output, log or emit the value, its digest, its length, or any other function of it beyond `matched`. #### `modelCapabilities` `modelCapabilities.advertised` lists the capability identifiers the active model offers; a host-private identifier MUST be prefixed `x-host-<host>-`. When a host advertising `modelCapabilities` dispatches a node that declares `requiredModelCapabilities`, it MUST check them before dispatch and act on the result: - all met: dispatch; - unmet, with a `fallbackModel` declared, `substitutionSupported` advertised, and the fallback's provider in `aiProviders.providers` with a resolvable credential: emit `model.capability-substituted`, then dispatch the fallback; - otherwise: emit `model.capability-insufficient` and fail the run with `capability_not_provided`. The host MUST NOT substitute silently or dispatch an unsuitable model. It MUST check a fallback's full capability set, and refuse a fallback that falls short with `fallbackAttempted: true` rather than chain another. Checking capabilities before resolving prompts is RECOMMENDED. #### `scheduling` A host advertising `scheduling` starts runs from the `schedule` trigger in the forms it advertises (`cron`, `delayed`, `calendar`). It MAY do so without `queueBus`. For each schedule it MUST: - persist it so it survives a restart and fires on time; - fire once per tick, never as duplicate concurrent runs; - reject a fire time beyond `maxFutureHorizon` with `schedule_horizon_exceeded`, whose `details.maxFutureHorizon` SHOULD echo the cap; - after missing a tick while down, either fire once on recovery or skip to the next tick, as it documents, never the whole backlog. #### `queueBus` A host advertising `queueBus` MUST expose `ctx.queueBus.publish`, `consume`, `ack` and `nack`; `deadLetter` when it advertises `deadLetterSupported`; and `streamSubscribe` when it advertises `stream`, honoring `fromBeginning` only under `stream.fromBeginning`. It MAY use any of its advertised `backends`. - A tenant's consumer MUST NOT receive another tenant's messages, even on the same topic. - `ack` MUST remove a message, `nack` MUST return it for redelivery, and `deadLetter` MUST route it to the configured dead-letter queue, which holds messages, not runs ([runs.md](https://openwop.dev/spec/v2/core/runs.html) §Dead letters). - A workflow triggered by a queue consume MUST get one run per inbound message, with no batching or skipping. - The wire shape MUST NOT vary by backend. An unknown topic or expired delivery token rejects `not_found`; an unreachable backend, `upstream_unavailable`. #### `toolHooks` A host advertising `toolHooks` extends `agent.toolCalled` and `agent.toolReturned` for every external tool call: - **`prePostEvents`.** The host MUST set `argsHash`, `principal` and `transport` on the call, and `status` and `durationMs` on the return. `argsHash` is SHA-256 over the RFC 8785 canonical arguments with secrets already redacted. A non-agent egress uses the principal `core.system`. `durationMs` is re-emitted verbatim on replay or fork, never recomputed. - **`perToolAuthorization`.** Before invoking, the host MUST check the principal's scopes against the tool's `requiredScopes`. If one is missing or cannot be evaluated, it MUST NOT invoke, MUST emit `agent.toolReturned` with `status: forbidden`, and MUST answer `403 forbidden` with `details.scope: "tool"`, `toolName` and `requiredScopes`. - **`perToolRateLimit`.** The host MUST keep a token bucket per `(principal, toolName)`. When the bucket is empty, the host MUST NOT invoke; it emits `status: rate_limited` and answers `429 rate_limited` with `details.scope: "tool"`. A host MAY refuse an allowlisted tool at loop start. Such a return has no call: the host MUST synthesize its `callId` (a stable derivation is RECOMMENDED), MAY omit `causationId`, and MUST NOT invent an `agent.toolCalled`. A consumer MUST tolerate an unpaired `forbidden` or `rate_limited` return. #### `httpClient` A host advertising `httpClient` MUST advertise `ssrfGuard: true` and a positive `maxResponseBodyBytes`. Before connecting it MUST resolve the target, reject loopback, RFC 1918, link-local and cloud-metadata addresses, and pin the resolved address for the connection (invariant `http-client-ssrf-guard`). A refused target is `egress_denied`, `reason: ssrf-blocked`; an unreachable one, `upstream_unavailable`. `methods` lists the HTTP methods it accepts. A host MAY expose `ctx.http.safeFetch(url, init?)` to pack code under `safeFetch`. It then: - MUST apply that guard, enforce `maxResponseBodyBytes` and any `requestTimeoutMs`, and refuse a connection upgrade; - MUST emit the `agent.toolCalled` and `agent.toolReturned` pair (`transport: http`) for every call when it also advertises `toolHooks.prePostEvents`; - SHOULD NOT forward an `Authorization` header the pack did not build from a host-issued credential. A host advertising `egressPolicy`, which requires `safeFetch`, attaches a `CredentialProvenance` (never the secret) when it binds a host-issued credential to an egress, and: - MUST emit a content-free `egress.decided`, whose `destination` is the authority alone and whose `reason` is from its closed set; - MUST NOT attach the credential to a destination outside its `audiences` (exact host or `*.domain`): the egress is `denied` or, where policy permits, `downgraded`; - MUST deny when provenance cannot be evaluated, and MUST NOT attach an expired credential. An egress is `allowed` only when the address guard and the audience check both pass. #### `workspace` A host advertising `workspace` keeps agent files (`schemas/v2/workspace-file.schema.json`) scoped to one `{tenant, workspace}`; no protocol path serves them. The host: - MUST make each write atomic, bumping `version`, and emit `workspace.updated` on each write or delete; a versioned delete leaves a tombstone; - MUST refuse a stale `If-Match` etag with `409 workspace_conflict` (`details.currentVersion`), and content over `maxFileBytes` with `workspace_too_large`; - with `versioned`, MUST serve the latest and any retained version, retaining best-effort up to `maxVersions`; `maxFiles` caps the file count; - MUST give a run, through `ctx.workspace`, an immutable snapshot taken at `run.started`, so a replay on any host sees the same files; its writes reach later runs only; - MUST derive the scope from the authenticated identity and MUST NOT return or disclose another scope's file; `404` MAY stand for `403` (invariant `workspace-cross-tenant-isolation`); - MUST persist `[REDACTED:<secretId>]` for any value the run's vault resolved at user, tenant or run scope (longest first, 8-character minimum). A workflow calling `ctx.workspace` MUST NOT register on a host without the family. The memory-index manifest is the workspace file `MEMORY-INDEX.json`. #### `memory` A host advertising `memory` serves agent memory (`schemas/v2/memory-entry.schema.json`) to pack code as `ctx.memory`; no protocol path serves it. `list` returns `[]` for an unknown ref and `get` returns `null`; writes are host-internal, and a read-only host sets `writable: false`. - **Refs.** `memoryRef` is opaque; a host MUST NOT assume another host's ref resolves. A node MUST guard `ctx.memory`, which may be undefined. - **Tenant isolation.** A ref MUST resolve to one tenant's entries, whatever the caller's permissions. A malformed ref (traversal, embedded null, oversize) MUST return `[]` or `null`. An adapter sharing a store MUST gate on the ref's shape, not trust the store. An adapter error MUST NOT carry entry data. - **Redaction.** A persisted entry MUST carry `[REDACTED:<secretId>]` in place of any value the run's vault resolved at user, tenant or run scope; platform scope is excluded. - **Size and expiry.** A host SHOULD reject a `put` over `maxEntrySizeBytes` with `validation_error`. Under `ttlSupported` or `retention.ttl`, an entry past `expiresAt` MUST NOT surface, purged or not. - **Long-term.** A host whose `agents.memoryBackends` includes `long-term` MUST honor isolation, redaction and expiry end to end. A validator MUST NOT look for `memoryBackends` under `memory`. - **`search`** advertises query `modes` beyond `list`. **`retention.forget`** is a tenant-scoped delete-by-subject of live memory only; replay reads the recorded snapshot and the log is untouched. - **`attribution`.** Under `emitsWriteEvents: true` the host MUST emit a content-free `memory.written` for every memory write a run makes; otherwise a consumer MUST tolerate its absence. - **`injectionBudget`** makes `list` honor `tokenBudget`, in `tokenCounter` units; without it the budget is ignored. The host MUST return a prefix of the ranked list within budget, omitting (never truncating) an entry that alone exceeds it. With `limit` as well, it MUST honor whichever yields fewer entries. - **Ranking.** `rank: relevance` MUST carry `query` and requires `search` mode `semantic`; otherwise a host MUST reject it or fall back, as documented, to `recency` (the default), and never fabricate a ranking. Ranking MUST run over the redacted, single-tenant set. - **`compaction`** (`trigger: host-managed`) emits `memory.compacted`. Derived content MUST pass the same redaction as a fresh `put`. A client MUST NOT infer compaction or distillation from entry counts. - **`distillation`** is budgeted compaction ([runs.md](https://openwop.dev/spec/v2/core/runs.html) §`distillation` section); an absent budget MUST default to `maxTokenBudget`, counting input and output. It emits `memory.compacted` with `distillation` and `trigger: host-managed`, and tenant isolation covers its archive and index. - **Distillation runs.** A distillation run MUST read the ref's snapshot, MUST NOT re-expose a redacted secret at any recursion level, and MUST write an immutable, addressable archive, byte-stable per source set and budget, kept for `archiveRetention`. Under `indexEmitted` it updates `MEMORY-INDEX.json`, and a `.md` sibling MAY accompany it. - **Degraded agents.** When an agent's `memoryShape` needs a dimension the host lacks, its inventory entry MUST set `memoryDegraded` and `degradedMemoryDimensions`; the agent MAY still dispatch. A `role: skill` manifest MUST keep `memoryShape` scratchpad-only, enforced by schema. *Sources: RFCs 0004, 0012, 0017, 0027, 0028, 0029, 0031, 0048, 0052, 0055, 0057, 0059, 0062, 0064, 0067, 0076, 0079, 0080, 0091, 0105, 0106, 0108, 0113, 0116, 0121, 0131, 0144, 0228, 0229.* ### Internationalization Source: https://openwop.dev/spec/v2/core/i18n.html > **Status: Stable.** > **Normative home:** `i18n`, `content`. #### Why this exists A host renders some text for humans: interrupt prompts, error messages, extension UI strings. A client asks for a locale and the host answers with the one it used. Machine-readable identifiers stay ASCII and dates and times stay ISO 8601; number, currency and text-direction formatting are out of scope. #### Language tags Every locale identifier — `Accept-Language`, `Content-Language` and every `locale` field — is a [BCP 47](https://www.rfc-editor.org/rfc/rfc5646) tag. - Tags compare case-insensitively (RFC 5646 §2.1.1, RFC 4647 §2). - A host SHOULD advertise and emit tags in the case RFC 5646 §2.1.1 recommends: lowercase language, titlecase script, uppercase region (`zh-Hant-TW`, `es-419`). #### `Accept-Language` A client MAY send `Accept-Language` ([RFC 9110 §12.5.4](https://www.rfc-editor.org/rfc/rfc9110#section-12.5.4)) on any request. A host MAY honor it to localize human-facing text in the response. A host: - MUST parse the header without failing the request. A malformed value MUST NOT cause `400`; the host proceeds in its default locale. - MUST NOT reject a request because it does not support the requested locale. - MUST honor q-values: the highest-q locale it supports wins, and ties go to request order. - MUST NOT infer a locale from the request body to override the header, which is authoritative. - SHOULD set `Content-Language` to the canonical tag of the locale used when it localized the response. When it localized nothing, it SHOULD omit `Content-Language` or set it to its default locale. - MAY cache localized text. When localization is expensive, caching by `(localeTag, sourceText)` is RECOMMENDED. - MAY return default-locale text at once and emit `node.completed` when the localized version is ready. This is discouraged for an interrupt prompt, which a human needs localized now. #### Fallback When a host cannot localize to the requested locale, it: 1. walks the list in q-value order and uses the first locale it supports; 2. otherwise tries the language family: asked for `ja-JP` and holding only `ja`, it uses `ja` and sets `Content-Language: ja`; 3. otherwise uses its default locale, which it SHOULD advertise as `i18n.defaultLocale`; 4. sets `Content-Language` to the locale it actually used, never another. #### Error envelopes A host MAY localize an error `message`, and MAY then add `details.locale`. - The `error` code MUST be the same registered identifier in every locale ([errors.md](https://openwop.dev/spec/v2/core/errors.html)); it is never translated. - `details` keys are schema field names and are not localized. - A human-facing `details` value MAY be localized, and SHOULD carry a `locale` sibling on the same object. #### The `i18n` record A host that negotiates locale SHOULD advertise `i18n`. A host advertising it honors `Accept-Language` on every protected route; a host that omits it serves one locale, its default, and ignores the header. - **`defaultLocale`** — the locale returned when no `Accept-Language` entry matches `supportedLocales`; `"en"` when omitted. - **`supportedLocales`** — the locales the host can return for human-facing text. It MUST contain `defaultLocale`, and its order carries no meaning. - A host that translates by machine SHOULD list only the locales it has validated end to end. #### Replay and fork Locale is chosen at request time. `Content-Language` is request-scoped and not recorded in the event log, so a replay re-projects the recorded text and does not re-render it. A fork localizes by its own request's `Accept-Language`, not the parent run's. #### Localized content A host advertising `content` serves authored pages (`schemas/v2/localized-content-*.schema.json`) on the `/content/*` operations. A section is one record, base `data` plus sparse `localizations`, never one record per locale. - **Advertisement.** A host advertising `content` MUST advertise `i18n`, with `content.baseLocale` equal to `i18n.defaultLocale` and every locale in `baseLocale` ∪ `supportedLocales` also in `i18n.supportedLocales`. It SHOULD advertise only locales it delivers, in the case §Language tags recommends. - **Delivery.** Locale is negotiated only by `Accept-Language` (§Fallback); there is no `?locale=` parameter. A section negotiated to `baseLocale` resolves to `data`. Any other section resolves to `data` shallowly overlaid by the first `localizations` entry for the exact tag, its `ll-Ssss` script family, or its language, else to `data` alone. Every host MUST resolve identically. - **Writes.** A write names its locale in the body: `baseLocale` upserts `data`, and any other locale upserts its overlay. - **Publication** is atomic across locales. Public delivery MUST serve published content only and MUST NOT cache a draft in a shared entry. - **Tenancy.** Every read and write MUST be tenant-scoped, and another tenant's id MUST get the same `404` as a missing one. Anonymous tenant resolution is host-defined, so identical resolution holds per resolved `(tenant, locale)`. #### Clients A client that wants localized content MUST check discovery for `i18n` before sending `Accept-Language`. *Sources: RFCs 0103, 0206.* ### Idempotency Source: https://openwop.dev/spec/v2/core/idempotency.html > **Status: Stable.** > **Normative home:** `idempotency`. #### Why this exists A retried request MUST NOT create a second run, and a retried node MUST NOT issue a second external effect. A host MUST implement Layer 1 for every mutating endpoint; a host advertising `idempotency` MUST implement Layer 2 for every node executor that performs an external side effect ([security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html)). #### Layer 1: `Idempotency-Key` The header keeps its standard name and applies to every mutating operation in `api/v2/openapi.yaml`; `GET` operations MUST NOT honor it. ##### Key and record - **Grammar.** The value MUST match `^[A-Za-z0-9._~-]{22,128}$` and MUST carry at least 128 bits of entropy (a UUIDv4 in canonical or base64url form satisfies it). A host MUST reject a value outside the grammar with `400 idempotency_key_invalid`. - **Record key.** A record MUST be keyed by `(authenticatedTenantId, canonicalEndpointId, callerIdempotencyKey)`; the tenant MUST come from the credential, never the body. - **Retention.** A record MUST be retained for at least 24 hours. - **Keyspace.** Host-minted identifiers MUST NOT share the caller idempotency store. Logs and spans MUST NOT expose keys. ##### Outcomes - **Final.** A host MUST cache `2xx` and non-retryable `4xx` responses (status, headers, body) and MUST return the cached response to a same-key duplicate. - **Retryable.** `429` and `5xx` MUST NOT be replayed from cache; a same-key retry MUST re-execute, and a later final outcome replaces the record. - **Not cached.** `400 idempotency_key_invalid`, `400 validation_error`, `401` and `403` MUST NOT be cached. - **Digest mismatch.** A different request digest under the same record key MUST fail with `409 idempotency_key_mismatch` and MUST NOT return the cached body. This is the only mismatch code. - **Replay marker.** A response served from cache MUST carry `OpenWOP-Idempotent-Replay: true`. ##### Concurrency Of two concurrent same-key requests a host MUST process exactly one to completion and MUST NOT process both. - The other MAY wait, bounded by the host's request timeout, and receive the winner's response only if it is a final outcome (one this record caches), marked `OpenWOP-Idempotent-Replay: true`. - Otherwise the host MUST answer `409 idempotency_in_flight` with no retry timing in `details`, and SHOULD set `Retry-After`. The code's registry `retriable: false` means not retryable without waiting. #### Layer 2: effect identity Layer 2's unit is the **effect**, identified once and stable across every transport or provider retry. - **Keying.** An effect MUST be keyed on its business identity (`keying: business-identity`): derived from the business operation, stable across every entry point, containing no `runId`, `nodeId` or ordinal. The activity recipe (`keying: activity-recipe`: tenant, run, node, ordinal, `providerKey`) is the fallback for a provider with no business key. - **Attempts.** The retry counter MUST NOT participate in the identity. Two distinct logical invocations MUST receive different identities. - **Claim.** The persist that guards the effect MUST be an atomic claim (compare-and-set or insert-if-absent) that at most one executor can win, so at most one concurrent duplicate performs the effect. - **Provider key.** When the provider accepts an idempotency key, the host MUST inject the effect identity (or a documented deterministic derivative), stable across retries. A host that cannot use the provider's convention MUST still persist the outcome. - **Streaming.** A streamed body MUST NOT be cached in the ledger; the host SHOULD record the request and its final outcome. - **Retention.** An effect record MUST be retained for at least 14 days. ##### Witness: `GET /runs/{runId}/effects` A host that advertises `idempotency` MUST serve `schemas/v2/effect-ledger-projection.schema.json` at `GET /runs/{runId}/effects` (`getRunEffects`): `{ runId, effects[] }`. Each record carries: - `effectId` (tenant-bound, `schemas/v2/ids.schema.json`), `nodeId`, `attempt`, `keying`, `at`; - `state`: `claimed` | `completed` | `released` | `escaped`; - optional `invocationId` and a redaction-safe `providerKey`. The projection MUST be content-free of provider payloads and credential material. #### Composition Layer 1 deduplicates the caller's request; Layer 2 deduplicates the run's effects. A retried provider call inside a run MUST resolve to the same effect record. Effects under replay and fork are in [replay.md](https://openwop.dev/spec/v2/core/replay.html); identifier grammars are in [identity.md](https://openwop.dev/spec/v2/core/identity.html). #### Multi-region The two region facets of `idempotency`, `multiRegion` and `crossRegion`, claim different things. - `crossRegion` names the host's deployment posture for this axis and MUST be held constant for the life of an advertisement. - `multiRegion` is the behavioural claim. When a host advertises it, both layers MUST hold across regions: an `Idempotency-Key` replayed into a second region MUST resolve to the first region's response rather than starting new work, and effect identity MUST collapse a duplicate effect wherever it is observed. - A host that does not advertise `multiRegion` makes no cross-region promise, and a client MUST NOT infer one from `crossRegion` alone. *Sources: RFCs 0170, 0171, 0173, 0213.* ### Identity Source: https://openwop.dev/spec/v2/core/identity.html > **Status: Stable.** > **Normative home:** `auth`, `authorization`, `anonymousActor`. #### Why this exists The Subject owns every run. Every lane binds to a trust root and a revocation rule, the link and every id have a grammar, and tokens carry a prefix so a host can rotate them. Idempotency-key grammar is in [idempotency.md](https://openwop.dev/spec/v2/core/idempotency.html). #### 1. The Subject is the owner ##### 1.1 Shape (`schemas/v2/subject.schema.json`) `RunSnapshot.owner` is `{ tenant, workspace?, subject }` with `subject` REQUIRED. `run.started` MUST echo the same block ([runs.md](https://openwop.dev/spec/v2/core/runs.html), [events.md](https://openwop.dev/spec/v2/core/events.html)). The Subject is closed (`additionalProperties: false`): | Field | Rule | | --- | --- | | `issuer` | REQUIRED; the lane's trust root (§2.2); `^\S+$`, 1–1024 | | `subjectId` | REQUIRED; `ids.schema.json#/$defs/subjectId` — issuer-scoped, stable, opaque, never PII | | `tenant` | REQUIRED; `tenantId` | | `lane` | REQUIRED; `api-key \| oauth2 \| oidc \| mtls \| saml \| scim \| ldap \| workload \| session \| anonymous` | | `kind` | REQUIRED; `user \| agent \| anonymous \| workload` | | `keyClass` | `opaque-idp \| configured-immutable`; MUST be present iff `lane ∈ {saml, scim}` | | `actor` | OPTIONAL; a nested Subject that acts on this subject's behalf; depth bounded at four | - `kind: anonymous` REQUIRES `lane: anonymous`, and `lane: anonymous` REQUIRES `kind: anonymous`. - The `actor` depth bound (4) is a four-level `$ref` chain (`actor1`…`actor4`), not a recursive `$ref`. A fifth level MUST fail validation. - `session` is a host-native credential the host itself issued (a durable login session, a local password). `anonymous` is a public surface. - The lane enum grows only under [overview.md](https://openwop.dev/spec/v2/core/overview.html) §0. ##### 1.2 The legacy subject rule A run created before the host began emitting subjects has a legacy subject: - On every read, the host MUST stamp `issuer: "urn:openwop:legacy"`, `lane` as attested else `api-key`, and `kind` as recorded else `user`. - A host MUST stamp the legacy subject at first read and MUST NOT rewrite it later. - A legacy subject MUST NOT participate in a link (§3), an actor chain, or a delegation decision. ##### 1.3 Fork On fork the host MUST copy `owner` verbatim onto the child: `tenant`, `workspace` and `subject`. ##### 1.4 A2A anonymous end users An end user reaching the host through an A2A peer is `kind: anonymous`, `lane: anonymous`, with the forwarding peer's subject as `actor`. Such a subject MUST NOT be linked. ##### 1.5 `anonymousActor` A public agent surface is an operator-configured entry point for unauthenticated callers. A host advertising `anonymousActor` MUST give a run dispatched through one an anonymous subject. - **The subject.** Its `subjectId` MUST be host-minted, opaque, PII-free and scoped to one surface session. It MUST NOT correlate two sessions or resolve to another subject, workspace or session. - **Authority.** The subject's only authority is the surface's explicit tool allowlist. A host MUST NOT resolve a role, scope or default tool baseline for it, or widen it within a session. - **`failClosed`.** A call whose grant is absent, unresolvable or errors MUST be denied. - **`tiers`.** A host MUST list only tiers it enforces: - `read` — tenant-scoped tools with no egress and no secret or BYOK reach; - `bounded-write-egress` — writes or egress behind a control, over the SSRF-guarded egress path, attaching a credential only when its audience covers the destination and policy permits anonymous use. - **`writeEgressControls`.** REQUIRED when `bounded-write-egress` is listed, and absent otherwise: `hitl` or `rate-limit-session-cap` (a hard rate limit plus a per-session action cap). - **Audit.** Every anonymous tool call MUST emit `authorization.decided` carrying no PII or credential. A denial's `reason` is `anon-not-granted`, `anon-write-ungated` or `anon-egress-denied`. `listTools` scoped to the subject reads the effective grant. #### 2. One binding pipeline, every lane ##### 2.1 The pipeline Every lane MUST verify the credential against the lane's trust root; bind the verified identity to the request, never to an asserted header; check audience; resolve to a Subject before any authorization decision; and fail closed. - On the `oidc` lane an ID token MAY be a bearer only when its `aud` equals the host's configured audience; any other `aud` is `audience_mismatch`. - The closed reason vocabulary is the family-wide error set in §6. Every lane is advertised as one member of the `auth.lanes[]` facet (`spec/v2/facets/auth.schema.json`): ```json { "lane": "oidc", "issuers": ["https://idp.example"], "revocation": "exp-and-recheck", "revocationWindowSeconds": 300, "minimumAssurance": "sender-constrained", "delegationProofs": ["dpop"] } ``` `lane`, `issuers[]` (min 1), `revocation` and `minimumAssurance` are REQUIRED on each member. `auth.lanes[].issuers[]` is the realm ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)). `authorization.failClosed` advertises the fail-closed rule and MUST be `true` when present; it does not gate it (invariant `authorization-fail-closed`). `authorization.roles` is the host role catalog: a request is authorized when any role-derived scope matches the required scope, under the same scope-match semantics this document applies to a credential. ##### 2.2 Trust roots and revocation Every lane MUST name its trust root as `subject.issuer` and MUST advertise it in `issuers[]`. Revocation exists for every lane; the `revocation` value names the rule. | Lane | `subject.issuer` (trust root) | Revocation MUST | `revocation` | | --- | --- | --- | --- | | `api-key` | the key realm (`urn:<host>:api-key` or a host-chosen URI) | refuse a revoked key on the next request (`credential_revoked`) | `next-request` | | `oauth2` | the token issuer | honor `exp` and re-check the issuer within the advertised `revocationWindowSeconds`; or honor `exp` alone under an enforced lifetime bound (`exp-only`, below) | `exp-and-recheck \| exp-only` | | `oidc` | `iss` | as `oauth2` | `exp-and-recheck \| exp-only` | | `mtls` | the CA subject | check CRL or OCSP, or issue certificates whose lifetime is at most the advertised window | `crl \| ocsp \| short-lived` | | `saml` | the IdP entityID (`<saml:Issuer>`) | honor `NotOnOrAfter`; consult the SCIM link deny-set when both lanes are advertised (§3) | `not-on-or-after` | | `scim` | the SCIM connection id bound at configuration to one IdP entityID | bind each client credential to one IdP entityID; refuse an unbound request | `bound-connection` | | `ldap` | the directory base DN | re-bind on each request or advertise a session window | `rebind` | | `workload` | the scheme's trust root | enforce `delegation_expired` | `delegation-expiry` | | `session` | `urn:<host>:session` | refuse a revoked session on the next request (`credential_revoked`) | `next-request` | | `anonymous` | `urn:<host>:anon-surface` | — | — | - `revocationWindowSeconds` (integer ≥ 1) MUST be advertised wherever the rule names a window: `exp-and-recheck`, `exp-only`, `short-lived`, `rebind`. On every lane it is an upper bound on the interval between a revocation at the trust root and the host's first refusal. A host MUST NOT advertise a window it does not enforce. - A host MUST NOT advertise a `revocation` value the row above for its lane does not list. - A consumer meeting an unrecognized value MUST NOT act on it: it MUST read the lane as stating no revocation latency, never as `next-request` or any other member ([overview.md](https://openwop.dev/spec/v2/core/overview.html) §0). ###### `exp-only` `exp-only` names a host that honors `exp` and never re-checks revocation: it consults no introspection endpoint, userinfo endpoint, revocation list, or host-side epoch or `validAfter` record. A credential revoked at the trust root is accepted until its own `exp`, so the enforced window is the only bound. - A host advertising `exp-only` on a lane MUST refuse a credential presented on that lane with `401 credential_lifetime_exceeded` when **either** `exp − iat` (total lifetime) **or** `exp − now` (remaining lifetime) exceeds the advertised `revocationWindowSeconds`. - A credential carrying no `iat` MUST be refused with the same code (§2.1 fail-closed). - A host that cannot enforce both bounds MUST NOT advertise `exp-only`. - `exp-only` SHOULD be advertised with a window of one hour or less. No maximum is set. - `exp-only` MUST NOT be advertised on the `api-key` or `session` lane; `auth.schema.json` refuses that pairing (invariant `lane-exp-only-lifetime-bounded`). ##### 2.3 Minimum assurance - Each lane MUST advertise `minimumAssurance: bearer | sender-constrained | key-bound`. - A request below the lane's floor MUST be refused with `sender_constraint_missing`. - An audit fact MUST record the assurance actually used. - A bearer fallback MUST NOT inherit a sender-constrained label (invariant `sender-constraint-no-bearer-downgrade`, `SECURITY/invariants.yaml`). ##### 2.4 Delegation proofs The proof format is lane-scoped: mTLS key binding or DPoP for the two JWT lanes (`oauth2`, `oidc`), SVID chains for `workload`. - A host MUST advertise the proofs it accepts under `auth.lanes[].delegationProofs[]` (`mtls-key-binding | dpop | svid-chain`). - A chain with no acceptable proof MUST be refused as `identity_unverified`. - A chain longer than the bound is `delegation_chain_too_long`, a cyclic chain is `delegation_chain_cyclic`, and a link that widens scope is `delegation_scope_amplified` (invariants `delegation-chain-bounded-acyclic`, `delegation-no-scope-amplification`, `delegation-provenance-not-authorization`). ##### 2.5 Protected-resource metadata and challenges A host advertising an `oauth2` or `oidc` lane MUST serve RFC 9728 metadata, unauthenticated, at the well-known URI formed from its resource identifier (the base URL it serves this API under) with `/.well-known/oauth-protected-resource` inserted before any path: `https://h.example/api` → `https://h.example/.well-known/oauth-protected-resource/api`. - `resource` MUST equal that identifier. - `authorization_servers` MUST list exactly the URL-form issuers of those lanes. - `scopes_supported` MUST list the scopes the host enforces. - `dpop_bound_access_tokens_required` or `tls_client_certificate_bound_access_tokens` MAY be `true` only where every such lane's `minimumAssurance` requires that binding (§2.3). Challenges on such a host: - A `401` MUST carry `WWW-Authenticate: Bearer resource_metadata="<url>"`, adding `error="invalid_token"` when a credential was presented and no error code when none was. - A `403` for insufficient scope MUST carry `error="insufficient_scope"` with `scope` listing every scope the operation requires. A `403` for resource binding carries no `insufficient_scope`. Other hosts SHOULD send `WWW-Authenticate: Bearer` on a `401`. A challenge attaches only to a response already `401` or `403` and MUST NOT change a status: where a rule requires `404` for an unknown or unauthorized resource, the `404` stands and carries none (invariant `auth-challenge-no-oracle`). #### 3. The link is a record A subject link is a `schemas/v2/subject-link.schema.json` record: ```json { "a": { "issuer": "…", "subjectId": "…" }, "b": { "issuer": "…", "subjectId": "…" }, "keyClass": "opaque-idp" | "configured-immutable", "issuer": "<IdP entityID>", "tenant": "<tenantId>", "formedAt": "<date-time>", "deniedAt"?: "<date-time>" } ``` The record and both `SubjectRef`s are closed; `a`, `b`, `keyClass`, `issuer`, `tenant`, `formedAt` are REQUIRED. A link: - MUST be tenant-scoped; - MUST join exactly two subjects whose `issuer` values are bound to one IdP entityID (`issuer` on the record); - MUST NOT include a legacy (`urn:openwop:legacy` is schema-rejected) or anonymous subject. Deactivation sets `deniedAt`; the SAML decision path MUST consult it (the leaver contract). The link is a reference, not a merge: nothing rewrites a subject already stamped on a run. Advertising both `saml` and `scim` lanes implies the link contract. The `auth.subjectLinkKey` facet (`opaque-idp | configured-immutable`) names the key class the host forms links under. #### 4. Resume tokens An interrupt resume token is `ow2.<alg>.<kid>.<payload>.<mac>`: - `alg ∈ {hs256}`, advertised in `interrupt.tokenAlgs[]`; - `kid` (`keyId` grammar) selects the verification secret; - `payload` and `mac` are as in v1. A host MUST refuse a token whose `alg` it does not advertise or whose `kid` it does not hold with `401` `interrupt_token_invalid`. The `{token}` path parameter carries the grammar (`api/v2/openapi.yaml`). Interrupt semantics are in [interrupt.md](https://openwop.dev/spec/v2/core/interrupt.html). A token not `ow2.`-prefixed was issued under v1; the rule is the prefix, never a segment count. Such a token MUST remain resolvable under `kid: legacy` until its `expiresAt`. A run suspended on an interrupt at the cut continues under [persistence.md](https://openwop.dev/spec/v2/core/persistence.html), and its outstanding token resolves the same way. #### 5. Identifier grammars (`schemas/v2/ids.schema.json`) Every id field in every v2 schema and every `api/v2/openapi.yaml` parameter and response body MUST `$ref` its kind. `x-openwop-minted` records who mints the id: `host` (opaque and checkable), `author` (chosen in a workflow or pack), or `registry`. | Kinds | Grammar | Minted | | --- | --- | --- | | `runId`, `interruptId`, `subscriptionId`, `deliveryId`, `effectId` | tenant-bound: `^(anon:)?[A-Za-z0-9._~-]{1,128}/[A-Za-z0-9._~-]{16,128}$` | host | | `eventId` | `^[A-Za-z0-9._~-]{16,128}$` | host | | `tenantId`, `workspaceId` | `^[A-Za-z0-9._~-]{1,128}$` | host | | `subjectId` | `^[^\s/]{1,256}$` (the issuer's grammar) | host | | `traceId`, `spanId` | W3C `^[0-9a-f]{32}$`, `^[0-9a-f]{16}$` | host | | `keyId` | `^[A-Za-z0-9._~-]{1,128}$` (signing keys, resume-token `kid`, bundle signatures) | registry | | `nodeId`, `workflowId`, `agentId`, `chainId`, `pluginId`, `templateId`, `libraryId` | `^[A-Za-z0-9._~:-]{1,128}$` | author | | `typeId` | `^[a-z][a-z0-9_-]*(\.[a-z][a-zA-Z0-9_-]*)+$`, maxLength 256 | author | `spec/v2/id-field-bindings.json` sorts every `*Id` property in a v2 schema into two sets: it **is** a kind above (and MUST `$ref` it), or nothing here governs it (reason recorded). - A host MUST reject a tenant-bound id whose tenant segment is not the caller's with `403` `id_tenant_mismatch`. - A host-minted opaque segment MUST match `^[A-Za-z0-9._~-]{16,128}$`. - Ids in documents and bodies are bound, always. A client MAY bind at its request seam. - Handle grammars (`memoryRef`, workspace `path`/`etag`, the plugin version token) and their `resolvability` class are specified where each handle is used. An importer MUST re-mint every `host`-scoped handle ([portability.md](https://openwop.dev/spec/v2/core/portability.html)). ##### Wire form On the wire a tenant-bound id is one path segment, projected: every UTF-8 byte outside `[A-Za-z0-9._-]` becomes `~` plus two uppercase hex digits, so `acme/r-9f3c…` travels as `acme~2Fr-9f3c…`. - A host MUST emit the projected form in every link and MUST accept it on every tenant-bound parameter. - It MUST still accept `tenant%2Fopaque`, and MUST decode either form before matching the grammar. - A host MUST NOT mint a tenant-bound id containing `~`; ids already minted MUST still resolve. - A host MUST project exactly once, where an id leaves it, and MUST NOT re-encode its own output. ##### Bare ids during the v1 overlap Through the overlap the bare form is admitted on a major-2 path parameter. - A parameter carrying only the opaque segment (what a `/v1/` create hands out) MUST resolve under the caller's tenant and never another's, and the response MUST name the resource bound ([versioning.md](https://openwop.dev/spec/v2/core/versioning.html) §5). The credential supplies the segment the `403` check would read. - Once a host advertises no `1.x` member it MUST refuse the bare form `400 validation_error` (not `id_tenant_mismatch`, not `not_found`). #### 6. Identity error codes (`spec/v2/errors.json`) Every code below is a row with `retriable: false` and no `details` contract; the envelope is in [errors.md](https://openwop.dev/spec/v2/core/errors.html). | Code | HTTP | Raised when | | --- | --- | --- | | `identity_unverified` | 401 | the credential fails verification against the lane's trust root, or a delegation chain has no acceptable proof | | `identity_unresolvable` | 401 | a verified identity resolves to no Subject | | `audience_mismatch` | 401 | the credential's audience is not this host | | `credential_revoked` | 401 | a revoked key or session is presented (§2.2) | | `credential_lifetime_exceeded` | 401 | a credential on an `exp-only` lane exceeds the advertised lifetime bound, or carries no `iat` (§2.2) | | `delegation_expired` | 401 | a delegation or workload credential is past its lifetime | | `sender_constraint_missing` | 401 | the request is below the lane's `minimumAssurance` (§2.3) | | `delegation_chain_too_long` | 400 | the actor chain exceeds depth 4 | | `delegation_chain_cyclic` | 400 | the actor chain repeats a subject | | `delegation_scope_amplified` | 403 | a delegated link claims more scope than its delegator | | `id_tenant_mismatch` | 403 | a tenant-bound id's tenant segment is not the caller's (§5) | | `interrupt_token_invalid` | 401 | an unadvertised `alg` or an unheld `kid` (§4) | `unauthenticated` and `run_forbidden` also apply. Every code is a registry member under [overview.md](https://openwop.dev/spec/v2/core/overview.html) §0. #### 7. Invariants The identity invariants are registered in `SECURITY/invariants.yaml` with their scenarios. An invariant without a witness is demoted from `protocol` tier. *Sources: RFCs 0132, 0165, 0170, 0176, 0184, 0200, 0210.* ### Interop Source: https://openwop.dev/spec/v2/core/interop.html > **Status: Stable.** > **Normative home:** `a2a`, `mcp`. #### Why this exists A2A and MCP are embedded protocols: a host advertises them, negotiates a version, and records every negotiation. Capability shapes are in [capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html); the peer identity is the Subject of [identity.md](https://openwop.dev/spec/v2/core/identity.html). #### REST is the wire REST and SSE are the wire. A2A and MCP are compositions over it, advertised by their own facets and nothing else. - A host MUST NOT advertise a transport list. - A discovery document carrying `supportedTransports` MUST fail validation against `schemas/v2/capabilities.schema.json`. #### The facets A host that speaks either protocol MUST advertise the corresponding facet — `a2a` or `mcp` — with every required field (`spec/v2/facets/a2a.schema.json`, `spec/v2/facets/mcp.schema.json`). | Facet field | A2A (`a2a`) | MCP (`mcp`) | Rule | | --- | --- | --- | --- | | Offered versions | `versions[]` (`major.minor`) | `revisions[]` (dates) | REQUIRED, at least one entry | | Default | `preferredVersion` | `preferredVersion` | REQUIRED; served when the peer names none | | Floor | `minimumVersion` | `minimumRevision` | REQUIRED; below it negotiation fails closed | | Freshness | `refreshedAt` | `refreshedAt` | REQUIRED; see the refresh SLA | | Profiles | `profiles[]` `a2a-<major.minor>` | `profiles[]` `mcp-<date>` | no `-legacy` suffix | | Protocol-specific | `agentCardUrl`, `streaming`, `pushNotifications`, `durableTasks` | `features[]`, `serverUrls[]`, `serverMount.transports[]` (`stdio` \| `streamable-http`), `mrtr.maxRounds` | optional | `mcp.serverMount.transports[]` is the MCP server's own transport enum; it is not a host transport advertisement. #### Legacy profiles are absent The `profiles[]` item patterns admit no `-legacy` suffix, so `a2a-0.3-legacy` and `mcp-2025-06-18-legacy` are not valid profile ids. A host that still speaks a legacy version does so as a private, non-advertised behavior. When no `A2A-Version` header is present, a host MUST serve the agent card of `preferredVersion`. #### Negotiation is a protocol **Authentication.** A version-negotiation exchange on either protocol MUST be authenticated: the peer identity is the caller's Subject (identity.md) or the host's own outbound identity. An unauthenticated exchange MUST NOT lower the negotiated version below `preferredVersion`. **The floor.** A negotiation that would land below `minimumVersion` / `minimumRevision` MUST fail closed with `interop_version_unsupported` (`spec/v2/errors.json`), whether or not host policy permits an explicit downgrade above the floor. **The audit event.** Every negotiation outcome, including the refused one, MUST emit a `negotiation.decided` event on the host's own event log: ```jsonc { "protocol": "a2a" | "mcp", "peer": "<origin digest>", "requested": "…", "negotiated": "…" | null, "outcome": "accepted" | "downgraded" | "refused", "reason": "…" } ``` The event is content-free: `peer` MUST be a digest of the peer origin, never the origin in clear. The event is the normative witness of the invariants `a2a-version-no-silent-downgrade` and `mcp-version-no-silent-downgrade`. **The refresh SLA.** A host MUST re-evaluate its advertised `versions[]` / `revisions[]` against the upstream registry within the window its `refreshedAt` declares, and that window MUST NOT exceed 90 days. An advertisement older than its window is non-conformant. **Downgrade above the floor.** A host MAY accept an authenticated request for a version between the floor and `preferredVersion`; the event then reports `outcome: downgraded`. #### The operation mappings `spec/v2/interop-map.json` (schema `interop-map.schema.json`) maps each profile's upstream operations, states, fields and errors to the v2 wire, pinned to an upstream release. A host advertising a profile: - MUST serve every row it implements as the row states, under the caller's Subject with the authorization, tenant scoping and state of the v2 operation the row names; - MUST refuse a row whose `requires` facet it does not advertise with the row's error; - MUST list every feature the map requires for that profile. What the map does not name is opaque: it MUST round-trip where upstream requires it and MUST NOT become authority, a prompt segment, a tool call or a workflow variable. A patch release that re-maps a row is a map edit; patch numbers are never negotiated. **Isolation.** On either interface: - A task the caller could not read through `getRun` MUST be answered exactly as a nonexistent one, including a tenant mismatch REST refuses `403`. - `ListTasks` MUST return only runs `listRuns` would return to the same Subject, whether or not `runList` is advertised. - `contextId`, `tenant` and `_meta` never select a tenant, workspace or principal. **A2A multi-turn (A2A §3.4.3).** - A message carrying `taskId` without `contextId` MUST be answered with the task's `contextId`. - A message whose `contextId` is not its task's MUST be refused with its binding's invalid-parameters error and MUST NOT change the run. - A message to a retained terminal task MUST be refused `UnsupportedOperationError`; `TaskNotFoundError` is for unknown, purged and unreadable tasks. **A2A error details.** A host: - on a 1.0 `JSONRPC` interface, MUST make an A2A error's `error.data` an array of objects each carrying `@type`, including exactly one `type.googleapis.com/google.rpc.ErrorInfo` whose `reason` is the map row's `reason` and whose `domain` is `a2a-protocol.org`; - on an `HTTP+JSON` interface, MUST answer A2A §11.6's `google.rpc.Status`; - MUST NOT answer with the OpenWOP error envelope on an interface URL the card lists, including a refusal before dispatch; - MUST NOT vary `TaskNotFoundError` details between an unknown task and an unreadable one, apart from an echo of the requested id; - on `VersionNotSupportedError`, SHOULD set `metadata.supportedVersions` to a comma-joined list; a client falls back to the card's `supportedInterfaces[].protocolVersion`. A client MUST identify an error by code or by the ErrorInfo `reason`, MUST accept `data` as an array, and SHOULD accept a `data` object carrying `reason` through 2.x. #### MCP tasks and cancellation A host MAY serve the MCP Tasks extension `io.modelcontextprotocol/tasks` (revision `2026-07-28`) on its server mount. It advertises it in its `server/discover` `capabilities.extensions` and by listing `extensions` in `mcp.features[]`, and nowhere else. A host that advertises it MUST implement the extension as published and the map's `mcp.tasks` rows, and: - MUST answer a `tools/call` that declared the extension with `CreateTaskResult` whenever the run is not terminal when the host answers, never with `InputRequiredResult`; - MUST use the run's projected `runId` (identity.md §5) as `taskId`, with an opaque segment of at least 128 bits of entropy. A `taskId` is never a credential; - MUST NOT append to a run's log to answer `tasks/get`. **Cancellation.** Until the host has sent its whole response to a request that starts or continues a run, the run belongs to that request. - Before the response is sent, a client disconnect on streamable HTTP, or a stdio `notifications/cancelled` naming the request, MUST cancel the run as `cancelRun` would, with `run.cancelled.reason` `mcp-request-cancelled`. - Once the response is sent, a disconnect MUST NOT affect the run; a task ends through `tasks/cancel`, `cancelRun`, or its own terminal state. - A host MUST NOT send `notifications/cancelled` except to end a `subscriptions/listen` stream. #### The MCP round ceiling `mcp.mrtr.maxRounds` (integer, 1–16) is the advertised ceiling on multi-round tool-result rounds. A host MUST refuse an `input_required` round beyond `maxRounds` with `mcp_mrtr_rounds_exceeded` (`spec/v2/errors.json`). The `requestState` rules are the map's `mcp.mrtr` rows. #### The durable-task projection `auth-required` remains a member of the persisted A2A task state enum (`schemas/v2/a2a-task-state.schema.json`) for the reverse direction (consuming an external A2A agent). The forward projection MUST emit it, with `interruptKind: credential` and a status message carrying `connectUrl`, for a run suspended on a `credential` interrupt (interrupt.md), and MUST NOT emit it otherwise. #### A2A push delivery A host advertising `a2a.pushNotifications` treats each push as a webhook egress: webhooks.md §Egress binds at delivery time as well as registration, a `3xx` is a failed delivery, and the push credential is bound as security-defaults.md §"Onward hops" states. - A host MUST attempt each push at least once; a host that retries follows `webhooks.md` `retryPolicy` semantics. - The body is an A2A 1.0 `StreamResponse` sent as `application/a2a+json`, carrying `Authorization: {scheme} {credentials}` from the config. When only `token` is set, a host SHOULD send it as `Authorization: Bearer <token>`. - A host MUST NOT add an OpenWOP signature. - Push dead-letters are not visible to A2A clients; a client recovers with `GetTask`. - A `replay` fork MUST NOT push re-emitted history, and no fork inherits a source run's push configs. - A push-config read or delete on a task the caller cannot read, or naming a `configId` that is not that task's, MUST answer exactly as for an unknown id, apart from the JSON-RPC `id`. - A `configId` MUST NOT encode a tenant, workspace or principal. Delete is idempotent. #### Per-agent cards A host advertising `a2a.agentCards` MUST also offer the `a2a-1.0` profile and `agents.manifestRuntime`, and MUST declare `capabilities.extendedAgentCard: true` on its public card. It publishes each entry of a caller's agent inventory (`GET /agents`) as an A2A `AgentCard`, reached through the entry's `a2aTenant`: an opaque routing value `R` the host mints, stable for the agent and host version, that MUST NOT encode a tenant, workspace, or principal. `GetExtendedAgentCard` with `tenant: R` MUST return that agent's card: - `name` is the entry's `persona`, `version` its `packVersion`, `description` its `description` or else `label`; - `supportedInterfaces[]` are the host card's interfaces, each carrying `tenant: R`; - `capabilities` and `securitySchemes` equal the host card's; - `skills[]` holds one skill per workflow the host routes to the agent for this caller. The card MUST NOT carry anything the inventory entry may not, and does not replace it: `degraded[]` and `memoryDegraded` stay on the entry. **Non-disclosure.** - A request carrying `R` MUST be authenticated and authorized as `GET /agents/{agentId}` is, before `R` is resolved. - For an `R` naming an agent outside the caller's inventory, every A2A operation MUST return what it returns for an `R` the host never minted, apart from the JSON-RPC `id`. - The public card at `agentCardUrl` MUST NOT list any `R`. - `R` is a `tenant` value under §"The operation mappings" **Isolation**. #### gRPC gRPC is not part of the core wire. Its document lives at `spec/v2/ext/grpc-transport/` with `witness: unwitnessable` and `adoption: none`; its requirements are SHOULDs of that extension. - A host MUST NOT advertise a `grpc` capability block. - `api/v2/openapi.yaml` and the AsyncAPI document are the only canonical API descriptions. #### Trace context A host that propagates W3C Trace Context: - into an MCP request MUST carry it in that request's `params._meta` (unprefixed `traceparent`, and `tracestate` when present; MCP 2026-07-28 `_meta`, SEP-414) or in the HTTP `traceparent` header, and SHOULD use `_meta`, the only carrier on stdio; - into an A2A message MUST carry it in `Message.metadata.openwop.traceparent` and `.tracestate` or in the HTTP header, and SHOULD use the metadata. A receiver prefers the in-message value, ignores a malformed one, and MUST NOT derive tenant, principal or scope from either. #### Threat model `SECURITY/threat-model-interop.md` is the threat model for this document. Peer identity and authorization at the boundary are governed by security-defaults.md; a peer MUST NOT gain authority the caller's Subject does not hold. *Sources: RFCs 0175, 0198, 0207, 0208, 0211, 0214.* ### Interrupt Source: https://openwop.dev/spec/v2/core/interrupt.html > **Status: Stable.** > **Normative home:** `interrupt`. #### Why this exists `interrupt` is how a run waits for something outside itself: a decision, an answer, an event, a conversation turn. Every kind shares one payload shape, event pair, resolve contract and token scheme. #### Payload `schemas/v2/suspend-request.schema.json` (`InterruptPayload`) is closed and discriminated by `kind`; `kind`, `key` and `data` are REQUIRED. | Kind | `data` (required fields) | Snapshot status / gate | | --- | --- | --- | | `approval` | `artifactId`, `artifactType`, `title`, `actions` | 5-action vocabulary, quorum and eligibility (§Approval) | | `clarification` | `questions[]` (`id`, `question`, optional `schema`) | `waiting-input` | | `external-event` | `eventType`, `correlation` | `waiting-external` | | `custom` | `customKind`, optional `payload` | — | | `conversation.start` | `conversationId` | Gated on `conversation` ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)) | | `conversation.exchange` | `conversationId`, `prompt` | — | | `conversation.close` | `conversationId` | Gated as above | | `low-confidence` | `agentId`, `threshold`, `observed` | — | | `credential` | `provider`, `scopes`, `reason`, `connectUrl` | `waiting-input`; gated on `oauth.credentialInterrupt` ([oauth.md](https://openwop.dev/spec/v2/core/oauth.html)) | Per-kind rules: - `external-event`: the snapshot status MUST be `waiting-external`. - `custom`: a host MUST accept and persist it; rendering is best-effort. - `conversation.start`: `conversationId` MUST be tenant-unique and MUST NOT be assumed resolvable on another host. - `conversation.exchange`: the resume value MUST validate against `outcomeSchema` when supplied. - `low-confidence`: an `agent.decided` with `confidence` below the threshold MUST be followed by `node.suspended { reason: 'low-confidence' }`. The per-run threshold is `configurable.run.escalationThreshold` ([runs.md](https://openwop.dev/spec/v2/core/runs.html)). - `credential`: the resume value is `{ outcome }` and carries no credential. - An interrupt of any kind MUST NOT solicit credential material; a credential is acquired through `credential`. ##### Re-entry and resume values `key` is the deterministic re-entry key of one invocation. A host MUST derive it from at least the run, the node and the node's visit index: how many of that node's interrupts in this run had their resolution consumed before this execution began. Its spelling is host-defined. - A replay or recovery of the same execution MUST re-derive the same key. - A later execution of the node, reached over an edge, MUST derive a different key, MUST raise a new `interrupt.requested`, and MUST NOT return an earlier visit's `resumeValue`. - Two interrupts raised in one execution MUST have distinct keys. - A host MUST invoke an interrupt with key `K` at most once for the lifetime of the run. - On recovery the engine MUST consult the event log, find the prior `interrupt.resolved`, and return the persisted `resumeValue` without emitting a second `interrupt.requested`. - An in-memory cache MAY serve in-process replays but MUST NOT replace the event log for cross-process replays. - A host MUST validate the resume value against `resumeSchema` when one is declared, and MUST refuse a failing value with `400 validation_error`. `timeoutMs`, when set, is the interrupt's own deadline; for an approval gate see §Rejection. #### Events Every kind uses two registered types ([events.md](https://openwop.dev/spec/v2/core/events.html)): - `interrupt.requested` — the payload is the `InterruptPayload` verbatim. - `interrupt.resolved` — the closed payload is `interruptResolved`. Resolving an approval-kind interrupt MUST record the applied `action` there, with the field §Approval requires. The kind-specific `approval.*` and `clarification.*` types remain registered; their payloads in `schemas/v2/run-event-payloads.schema.json` are `$ref` aliases of `interruptRequested` and `interruptResolved`. A host emitting `interrupt.requested` MAY also emit the kind-specific type. Both events are durable and appear in the `updates` and `debug` stream modes. While suspended, `RunSnapshot.currentNodeId` names the node and `status` is `waiting-approval`, `waiting-input` or `waiting-external`. #### Resolve surfaces | Operation | Path | Auth | Body | | --- | --- | --- | --- | | `resolveInterruptByRun` | `POST /runs/{runId}/interrupts/{nodeId}` | `approvals:respond` | `{ resumeValue }` (closed) | | `inspectInterruptByToken` | `GET /interrupts/{token}` | the token | — (returns the `InterruptPayload`) | | `resolveInterruptByToken` | `POST /interrupts/{token}` | the token | `{ resumeValue }` (closed) | - A host MUST expose the run-scoped surface, and SHOULD expose the signed-token surface for callers not authenticated to the protocol. - Every resolve MUST honor `Idempotency-Key` ([idempotency.md](https://openwop.dev/spec/v2/core/idempotency.html)). - Of two concurrent resolves, exactly one MUST succeed; the other MUST receive `409 interrupt_already_resolved`. ##### Callback delivery `createRun.callbackUrl` names where a host advertising `interrupt.callbackDelivery: true` delivers notice of an interrupt, for resolution through the token surface. Payload, timing and signing are host-defined. A host advertising the facet: - MUST refuse at `createRun`, with `400 validation_error` and `details.field: "callbackUrl"`, a URL the `webhooks.md` §Egress registration guard would refuse; - MUST re-validate every resolved address at delivery; - MUST NOT follow a redirect. A host that does not advertise it SHOULD refuse the member and MUST NOT claim delivery it does not perform. ##### Errors | Status | Code | Condition | | --- | --- | --- | | `400` | `validation_error` | `resumeValue` fails `resumeSchema`, or the approval action is not in `actions` | | `401` | `interrupt_token_invalid` | MAC, `alg` or `kid` not accepted | | `404` | `not_found` | No such run or node | | `409` | `interrupt_already_resolved` | Already resolved; the run is cancelled or completed (both surfaces); or the token was invalidated | | `410` | `interrupt_expired` | Token past `expiresAt` (token surface only) | #### Tokens The token grammar and the `interrupt.tokenAlgs[]` / `kid` check are [identity.md](https://openwop.dev/spec/v2/core/identity.html) §4 (`401 interrupt_token_invalid`). - **Expiry.** Every token MUST carry `expiresAt`. The default SHOULD be 30 minutes, and a host MUST cap the lifetime at the interrupt's `timeoutMs` when one exists. A token MUST NOT outlive the interrupt it resolves; past `expiresAt` the host MUST answer `410 interrupt_expired`. - **Invalidation.** A token MUST be invalidated when its interrupt is resolved or its run is cancelled or completed. Later use MUST answer `409 interrupt_already_resolved`. - **Verification.** MAC comparison MUST be constant-time. `kid` selects the verification secret, so secrets rotate without orphaning tokens. - **Intent.** A token minted with `intent: resolve` authorizes both operations. A host MAY mint `intent: inspect` tokens; a resolve with one MUST be refused with `403`. #### Approval `actions` is a non-empty subset of `accept`, `reject`, `refine`, `edit-accept`, `ask`; a host MUST enforce it on resolve. `ask` does not exit the suspend. | `action` | Required field | | --- | --- | | `accept` | — (`feedback?`) | | `reject` | — (`feedback?`) | | `refine` | `refineFeedback { scope: whole \| section \| items, sectionPath?, itemIds?, tags?, text? }` | | `edit-accept` | `editedArtifactData` | - Every resume carries `decidedAt`. `decidedBy` MAY be omitted by an authenticated caller, and every consumer MUST treat it as an opaque string. - `requiredApprovals` sets the quorum (default 1). `rejectionPolicy` is `single-veto` (default) or `majority`. - When `overrideBypassesQuorum` is `true`, a configured override principal MAY release the gate alone; otherwise its vote counts once. ##### Rejection A `reject` exits the suspend. The host MUST record `action: "reject"` and `decision: "rejected"` on `interrupt.resolved`, and MAY also emit `approval.rejected`. The resume value returns to the raising node (§Re-entry and resume values). - A node that does not turn the rejection into an output MUST fail with `approval_rejected` and `retryable: false` on the `node.failed` error, and MUST NOT be retried. - A rejected gate is a failed source. It MUST NOT satisfy an `all_success`, `any_success` or `none_failed` edge. The run continues past it only over an edge whose `triggerRule` admits a failed source (`all_complete` or `any_failed`). - When no such edge exists, the run MUST terminate `failed` with `run.failed.error.code` `approval_rejected` and `failedNodeId` naming the gate. - The gate resolves rejected on one eligible `reject` under `single-veto`, or when rejects exceed half of `requiredApprovals` under `majority`. A vote that does not decide the gate MUST NOT emit `interrupt.resolved`. - When a non-zero `timeoutMs` elapses with no resolution: - the host MUST resolve the gate rejected, recording `action: "timeout"`, `decision: "rejected"` and `reason: "timeout"`, whatever `onTimeout` holds, and MUST apply the rules above; - a timeout MUST NOT grant a gate. A host MUST treat `onTimeout: "approve"` as `reject` and SHOULD NOT emit it; - `escalate` MAY notify a host-defined target but MUST NOT extend or grant the gate; - a host MUST NOT accept `timeout` on a resume request. - On replay the failure MUST be derived from the recorded `interrupt.resolved`, never re-decided. #### Approver enforcement The facet `spec/v2/facets/interrupt.schema.json` carries `tokenAlgs[]` (REQUIRED) and `refKinds[]` ⊆ `principal`, `group`, `role`. - **`approversList`** (explicit principals) binds everywhere: a host advertising `interrupt` MUST refuse a resolver not in the list. - **`approverGroupRefs`** binds only where `refKinds` includes `group`: the host MUST surface the field unchanged and MUST resolve and enforce its members as eligible approvers. - **`approverRoleRefs`** binds only where `refKinds` includes `role`: as for groups, with holders. - **`audience`** is a notification hint, never eligibility. When omitted, the host SHOULD notify the union of the eligibility refs. - A host that does not advertise a ref kind MUST ignore that field. Eligibility binds every writer of the suspension record. A host whose durable store is writable by a principal other than the engine MUST enforce the same eligibility at the store, or MUST NOT expose the record to that principal for write. Refs are opaque to the engine. Membership MUST be resolved at decision time and MUST NOT be re-resolved during replay or `forkRun`: the recorded eligibility decision is fixed history ([replay.md](https://openwop.dev/spec/v2/core/replay.html)). #### During the v1 overlap The v1 interrupt-token drain is [identity.md](https://openwop.dev/spec/v2/core/identity.html) §4. *Sources: RFCs 0170, 0171, 0173, 0187, 0196, 0223.* ### Node-pack runtimes Source: https://openwop.dev/spec/v2/core/node-pack-runtimes.html > **Status: Stable.** > **Normative home:** `nodePackRuntimes`. #### Why this exists This is the v2 contract for the language a node pack runs in. It also lets a `remote` pack name its MCP server by an inline MCP Registry record instead of a bare URL. The shape is `$defs/Runtime` in `schemas/v2/node-pack-manifest.schema.json`. #### Languages `runtime.language` is one of `javascript`, `python`, `go`, `wasm`, `wasm-component`, `remote`. `entry` is a path inside the tarball, or, for `remote`, the URL of an MCP server the host calls as an MCP client. A host MAY refuse a language it cannot execute, at workflow registration and with `unsupported_runtime`. #### WASM - A host that loads `wasm` packs MUST advertise `nodePackRuntimes.wasm` with at least one `abiVersions[]` entry, and MUST reject at load a pack whose `openwop_abi_version()` is not listed. - When it advertises `maxMemoryBytes` it MUST enforce it and emit `cap.breached` with `kind: "wasm-memory"` on a breach. - `nodePackRuntimes.wasmComponent` advertises `wasm-component` loading; its interfaces are reserved for a later RFC. #### The MCP registry record A `remote` runtime MAY carry `mcpServer`, an inline subset of an MCP Registry `server.json` (schema `2025-12-11`): - `name`, `description`, `version`; - optional `title`, `websiteUrl` and `repository`; - exactly one `remotes[]` entry of `type: "streamable-http"` with an `https://` URL. It has no `packages[]`, `headers`, `variables` or `_meta`, so it carries no install instruction and no credential. The record is by value; this contract defines no registry lookup. - `mcpServer` under any other language, or an `entry` that differs from `remotes[0].url`, makes the manifest invalid (`pack_validation_failed`). - A host SHOULD NOT treat `name` as a verified identity: an inline record carries no proof of namespace ownership. - A host that authenticates to the server does so through the node's `requiredCredentials` or `auth`. *Sources: RFCs 0008, 0203.* ### OAuth Source: https://openwop.dev/spec/v2/core/oauth.html > **Status: Stable.** > **Normative home:** `oauth`, `credentials`. #### Why this exists A connector node needs a token a user granted to a third party. The host obtains, stores and refreshes it and hands it to the node's sandbox; a pack names a provider and scopes and never touches the grant. Here the host is an OAuth client; [identity.md](https://openwop.dev/spec/v2/core/identity.html) covers it as a protected resource. #### Credentials A host advertising `credentials` MUST resolve a `{ ref, scope }` reference (`schemas/v2/credential-reference.schema.json`) at node execution and inject the material only into the node sandbox. - The material MUST NOT appear in inputs, variables, events, the debug bundle or replay state (invariant `credential-payload-redaction`). - A failed resolution is `credential_not_found`, `credential_forbidden` (outside the caller's scope; fail closed) or `credential_scope_unsupported` (a scope not in `credentials.scopes`). - With `credentials.sharing`, every reference within a scope resolves one stored credential. - With `credentials.rotation` `two-key-overlap`, old and new material both resolve during the grace window; after it the old fails `credential_not_found`. - `credentials.encryptionAtRest` is a claim about storage; it gates nothing. #### Token lifecycle A host advertising `oauth`: - MUST perform only the grants in `oauth.grants`. - MUST refuse to register a node whose `auth.provider` or scope is not in `oauth.providers` (`oauth_provider_unsupported`, `oauth_scope_unsupported`). - Drives the redirect and callback host-side. The code, redirect URI, `state` and PKCE verifier MUST NOT enter a run-visible surface. - Persists tokens as a `credentials` entry at scope `user` or `workspace`, and refreshes them host-side. - On terminal refresh failure, MUST emit `connector.auth-expired` and fail the node with `connector_auth_expired`, unless it advertises `oauth.credentialInterrupt`. #### The authorization-code client On every `authorization_code` grant the host MUST: 1. send PKCE with `S256` and never `plain`, omitting PKCE only for a provider advertised with `pkce: "unsupported"`; 2. send a fresh `state` of at least 128 bits from a CSPRNG, bound host-side to the initiating Subject and the provider, with a lifetime of at most 10 minutes, and refuse a callback whose `state` is absent, unknown, reused or expired, making no token request for it; 3. complete the callback only for the initiating Subject (invariant `oauth-same-user-binding`): store the credential under the Subject bound to `state`, and if the Subject authenticated on the callback request (session or bearer) differs from it, refuse and store nothing; 4. validate `iss` per RFC 9207 §2.4 where the provider's issuer is known (`oauth.providers[].issuer`), and otherwise give the provider a redirect URI no other provider shares (RFC 9700 §4.4.2); 5. use one fixed, registered redirect URI per provider. ##### MCP-reach providers Where the provider is reached as an MCP server: - The host MUST also send `resource` (RFC 8707), the server's canonical URI, in both requests. - The host MUST refuse a provider whose authorization-server metadata omits `S256`. - It fetches the server's Protected Resource Metadata (RFC 9728) only from URLs derived from the manifest's server URL. - Discovery verifies and never selects. A discovered issuer or endpoint that differs from the manifest's, or from the tuple pinned at registration, MUST be refused `connection_auth_metadata_mismatch`. A grant for such a provider whose manifest declares no `issuer` MUST be refused the same way. This binds a host-configured provider as well as a connection-pack one: the configured server URL and issuer stand in for the manifest's, pinned when the host loads that configuration. #### The credential interrupt A host advertising `oauth.credentialInterrupt` MUST suspend the node with a `credential` interrupt ([interrupt.md](https://openwop.dev/spec/v2/core/interrupt.html)) instead of failing it when a node declaring `auth: { type: "oauth2", provider, scopes }` is about to run and: - no credential resolves for the Subject, provider and scopes (`reason: "missing"`); - one resolves with fewer scopes (`"insufficient_scope"`); or - refresh failed terminally (`"expired"`). Then: - `connectUrl` MUST be host-owned, MUST NOT be pre-authenticated, and MUST complete only for the initiating Subject. - The host resolves the interrupt when the grant completes. A resolve of `authorized` MUST be refused `400 validation_error` unless a credential now resolves. - `declined` fails the node with `connector_auth_declined`. - A host that binds a node to one credential reference (such as a connection) reads "resolves" as that reference resolving with the node's scopes, carries it as `credentialRef`, and still completes `connectUrl` only for the initiating Subject. *Sources: RFCs 0046, 0047, 0199.* ### OpenWOP v2 Core — Overview Source: https://openwop.dev/spec/v2/core/overview.html > **Status: Stable.** #### Why this exists `spec/v2/core/` is what a host implements to pass the 2.0.0 floor. This document sets the reading order, states the six axioms, and holds the rules other documents only reference. #### Reading order 1. [`overview.md`](https://openwop.dev/spec/v2/core/overview.html) — axioms, §0, §0a, claim vocabulary, the `ext/` rule 2. [`versioning.md`](https://openwop.dev/spec/v2/core/versioning.html) — major negotiation, `OpenWOP-Version`, the 18 axes, release identity 3. [`capabilities.md`](https://openwop.dev/spec/v2/core/capabilities.html) — the well-known resource, record type, closed root, derived profiles 4. [`identity.md`](https://openwop.dev/spec/v2/core/identity.html) — Subject, lanes, `SubjectLink`, id grammars, resume tokens 5. [`runs.md`](https://openwop.dev/spec/v2/core/runs.html) — create, get, cancel, fork; `configurable`; snapshot; owner 6. [`events.md`](https://openwop.dev/spec/v2/core/events.html), [`errors.md`](https://openwop.dev/spec/v2/core/errors.html), [`headers.md`](https://openwop.dev/spec/v2/core/headers.html) — event `oneOf`, payload and error registries, `OpenWOP-*` headers 7. [`interrupt.md`](https://openwop.dev/spec/v2/core/interrupt.html), [`idempotency.md`](https://openwop.dev/spec/v2/core/idempotency.html), [`replay.md`](https://openwop.dev/spec/v2/core/replay.html), [`conversation.md`](https://openwop.dev/spec/v2/core/conversation.html), [`execution.md`](https://openwop.dev/spec/v2/core/execution.html) — run-side surfaces 8. [`persistence.md`](https://openwop.dev/spec/v2/core/persistence.html) — era key, v1 reader rule, pinned runs 9. [`security-defaults.md`](https://openwop.dev/spec/v2/core/security-defaults.html), [`webhooks.md`](https://openwop.dev/spec/v2/core/webhooks.html), [`interop.md`](https://openwop.dev/spec/v2/core/interop.html), [`host-services.md`](https://openwop.dev/spec/v2/core/host-services.html), [`storage.md`](https://openwop.dev/spec/v2/core/storage.html), [`tool-catalog.md`](https://openwop.dev/spec/v2/core/tool-catalog.html), [`i18n.md`](https://openwop.dev/spec/v2/core/i18n.html), [`portability.md`](https://openwop.dev/spec/v2/core/portability.html) — surface obligations, signatures, A2A and MCP, host services, storage, locale, estate export 10. [`packs.md`](https://openwop.dev/spec/v2/core/packs.html), [`node-pack-runtimes.md`](https://openwop.dev/spec/v2/core/node-pack-runtimes.html), [`connection-packs.md`](https://openwop.dev/spec/v2/core/connection-packs.html), [`form-content-packs.md`](https://openwop.dev/spec/v2/core/form-content-packs.html), [`workflow-chain-packs.md`](https://openwop.dev/spec/v2/core/workflow-chain-packs.html), [`artifact-type-packs.md`](https://openwop.dev/spec/v2/core/artifact-type-packs.html) — pack identity, engines ceiling 11. [`conformance.md`](https://openwop.dev/spec/v2/core/conformance.html) — requirement ids, witness classes, bundle v3, seams profile #### Axioms in force 1. A MUST without a witness class is not a requirement. 2. One name per thing. Every alias has a removal date in `spec/v1/deprecations.json` and a codemod id. 3. Closed by default. The discovery root, event envelope, payload and error registries, bundle, and `configurable` are `additionalProperties: false`. Vendor extension is one positive pattern in one namespace. 4. Registers are data. Gaps, risks, deprecations, migrations, witness classes, and dispositions are files with schemas and gates; prose is checked against them, never the reverse. 5. Security defaults are obligations of the surface. A protecting behavior binds when the surface is advertised, never when a flag is set. 6. Nothing persisted under v1 is orphaned. Every v1 artifact has a disposition in the migration register. #### §0 Closed-enum growth rule A registry-backed enum (event types, error codes, envelope kinds, reason vocabularies, lanes) grows by adding a row to its registry and regenerating. - Consumers MUST accept an unknown member of a registry-backed enum and MUST NOT act on it. - Producers MUST NOT emit an unregistered member. - Adding a member is additive in v2.x. Renaming one is a major; removing one is a major except under §0a. #### §0a Retiring a v2 surface A 2.x minor MUST NOT change the shape of an existing v2 surface; a new shape is a new surface added beside the old one. A 2.x minor MAY remove a surface only when `scripts/check-v2-retirement.mjs` proves all of the following; otherwise the removal waits for 3.0: 1. Its replacement shipped in an earlier 2.x minor. 2. A `v2-minor` row in `spec/v1/deprecations.json` named the removal minor at least two minors and 30 days earlier. 3. No committed v2 host bundle and no published registry manifest carries it. 4. It is an optional family, facet, enum member or envelope kind, so its absence is already a 2.0 state. 5. Its family is `experimental` in `spec/v2/declaration.json`. 6. No independent host is in the INTEROP-MATRIX v2 table. Readers MUST keep accepting a retired shape on replay, fork and poll; only emission narrows. #### v1 end-of-support v1 support ends at the later of: - (a) every INTEROP-MATRIX host's non-vacuous v2 bundle, plus 90 days; - (b) 18 months from the v2 release — applied if and only if an independent host is in the matrix at release. The date is computed from the matrix, or set earlier under (c); nothing else MAY set it. - (c) An `Accepted` RFC MAY set an earlier date once every counted host has a certified non-vacuous v2 bundle in `evidence/v2-host-bundles/` and reports no old-major traffic from third parties (anyone but the host's operator and the conformance suite) over at least the 7 days before the RFC. The date and that evidence are in `spec/v2/eos-override.json`. - **After the date** a host MAY drop the old major from `protocolVersions[]`; it is not required to that day. - **The old-major tree** is then frozen as history by a marker naming the date, not edited. - **Counted hosts.** Those with a row in the INTEROP-MATRIX v2 table. A reference host that stays on the 1.x line through the overlap (the matrix says which) is not a v2 host and does not count. - **Non-vacuous.** At least one claimed profile carries `witnessCount ≥ 1`. - **Anchor.** The date the host's signed bundle was committed to `evidence/v2-host-bundles/` in the spec repository, read from the public history (`git log --diff-filter=A`). Never `generatedAt` inside the bundle, which nothing signs. A later re-certification replaces the file and does not move the anchor. - **The computed date.** `evidence/v1-end-of-support.json`, GENERATED by `scripts/generate-v1-eos-clock.mjs`, which honours a (c) override only when it meets (c). On or after the date, `check-removal-dates.mjs` fails the v1-tree sources of every `v1-end-of-support` row unless that tree is frozen. ##### Old-major retention floors Every published old-major artifact MUST remain installable at its last 1.x version for 12 months from the 2.0.0 publish (the `v2.0.0` tag's commit date). This holds independent of v1 end-of-support, which may come first. A consumer pinned to the old major MUST be able to rebuild through that window. The artifacts are the npm packages `@openwop/openwop` (1.x) and `@openwop/openwop-conformance` (1.x), the PyPI package `openwop-client` (1.x), and the Go module `github.com/openwop/openwop-sdks/go` (v1.x). Their identities and last 1.x versions are in `spec/v2/retention-floors.json`. `scripts/check-retention-floors.mjs` prints the floor state and, with `--network`, probes each registry for the pinned version. Unpublishing, deprecating-with-removal, or retracting a listed version inside the window is a Phase 5 exit failure. #### Profile claim vocabulary These rules bind any public conformance statement: - An unqualified "OpenWOP conformant" or "OpenWOP compatible" statement MUST mean `openwop-core-standard`, the executable floor, never the discovery predicate. - A discovery-only claim MUST say `openwop-discovery-core` and MUST NOT use the same badge as `openwop-core-standard`. - Every claim MUST state every additional profile it relies on; an omitted profile is an unclaimed one. - A certification bundle MUST name canonical profile ids; `openwop-core` is deleted (see [`capabilities.md`](https://openwop.dev/spec/v2/core/capabilities.html)). - A vendor extension MUST NOT use an `openwop-*` id without an accepted RFC. #### What is `ext/` `spec/v2/core/` stays within the word budget that `scripts/check-core-budget.mjs` enforces; `spec/v2/ext/` holds the rest. - Every `spec/v2/ext/<key>/` document MUST declare `witness` and both maturity axes (`technical`, `adoption`) in its header. - A MUST with `witness: unwitnessable` MUST NOT appear in `core/`. A document whose only witness is "deferred to Active → Accepted" enters `ext/` or is deleted. - An `ext/` family is advertised only under a wire-legal witness class (see [`capabilities.md`](https://openwop.dev/spec/v2/core/capabilities.html)). #### What a MUST means (Axiom 1) Every MUST, SHOULD, and MAY in `core/` is a requirement with an id in `requirements.json` and a `witness` from `witnessable-unaided | witnessable-gated | seam-gated | claims-check | negative-existence`. A seam-gated MUST is governed by [`conformance.md`](https://openwop.dev/spec/v2/core/conformance.html) §"Witness class". *Sources: RFCs 0155, 0167, 0168, 0169, 0171, 0174, 0190, 0197.* ### Packs Source: https://openwop.dev/spec/v2/core/packs.html > **Status: Stable.** > **Normative home:** `packs`, `uiPlugins`. #### Why this exists The v2 contract for pack manifests, the registry tree, peer-dependency identifiers, and signing. The per-kind rules live in [connection-packs.md](https://openwop.dev/spec/v2/core/connection-packs.html), [form-content-packs.md](https://openwop.dev/spec/v2/core/form-content-packs.html), [workflow-chain-packs.md](https://openwop.dev/spec/v2/core/workflow-chain-packs.html), and [artifact-type-packs.md](https://openwop.dev/spec/v2/core/artifact-type-packs.html); the capability vocabulary a pack requires is [capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html). #### The engine range A manifest's `engines.openwop` MUST match the grammar in `schemas/v2/node-pack-manifest.schema.json`: a `>=` lower bound and an explicit `<` major ceiling (`^>=\d+(\.\d+){0,2} <\d+\.0\.0$`). - A v2 host MUST treat a range with no upper bound as bounded by `<2.0.0`. - A host MUST refuse to install a version whose range does not admit the host's protocol major with `pack_engine_unsupported` (`spec/v2/errors.json`). - `pack_runtime_requirement_unmet` remains a runtime-requirement code and MUST NOT be used for the protocol major. - Both checks MUST run at install on every publication path — the canonical registry, a vendor registry's write API, and a mirror ingest. The range is a claim about the pack's own surface, not about run semantics. Admitting major M asserts that: - the manifest validates against the `schemas/v2/` manifest schema for its `kind`; - every `peerDependencies` key resolves (§"Peer-dependency identifiers"); - the version carries a §Signing signature. Each conjunct keeps its own refusal code. A mechanical ceiling bump is not a verification. #### The `packs` capability A host advertises `packs` when it serves the registry surface above: - A host MUST NOT advertise `packs` unless it resolves pack references through a registry reachable from its discovery document. - A client MUST treat an absent record as "no registry resolution", not as an unknown. It says nothing about pack validation or execution, which `sandbox` and §"The engine range" bind. `testMode` is DEPRECATED and MUST NOT be relied on by a client (§"During the v1 overlap"). A host mounting a test catalog SHOULD advertise the seams profile instead, and MUST NOT treat `testMode` as a second way to claim one. #### The registry tree The registry is versioned by tree, not header. It publishes `registry/v2/packs/<name>/-/<version>.{json,sbom.json,sig,tgz}` as a parallel tree of re-signed manifests with regenerated SBOMs and index. - A signed compatibility overlay MUST be rejected; a mirror re-derives the signer at ingest. - `.well-known/openwop-registry.json` `endpoints` is the negotiation: it names both trees, and a client MUST resolve every registry path through it rather than construct one. - `publicKey` is unversioned: keys are not protocol-versioned. #### Peer-dependency identifiers A `peerDependencies` key MUST either: - be a `families[].key` in `spec/v2/declaration.json` whose `anchor` is not `deleted` — equivalently a root key of the generated `schemas/v2/capabilities.schema.json`; or - carry a row in `spec/v2/peer-dependency-aliases.json` (§"The alias table"). The declaration key, the peer-dependency identifier, and the capabilities.md section anchor are one identifier. A host MUST refuse a key the declaration file does not name with `pack_peer_dependency_undefined`. Facet paths are not identifiers: a pack requires a family by its key and names facets in `peerDependenciesMeta.<family>.facets[]`. ```jsonc "peerDependencies": { "aiProviders": "required" }, "peerDependenciesMeta": { "aiProviders": { "facets": ["imageGeneration"] } } ``` #### The alias table `spec/v2/peer-dependency-aliases.json` is generated from the declaration file and the published-manifest inventory, never hand-kept. It is how a v1-era key reaches a v2 family through the overlap. Each row is `{ alias, family, facets?, publishedUses, removalTrigger }` and covers a v1 grammar found in the wild (`host.*` dotted twins, `openwop.agents.memoryBackends`, facet paths such as `aiProviders.imageGeneration`). A v2 host MAY resolve an alias through the table during the overlap, and MUST NOT resolve one after v1 end-of-support (`removalTrigger: v1-end-of-support`). #### The manifest schema family The manifest schemas carry `$id` under `https://openwop.dev/spec/v2/`; the v1 `$id`s are immutable and served read-only. | Schema (`schemas/v2/…`) | Author | Vendor hatch | | --- | --- | --- | | `node-pack-manifest`, `prompt-pack-manifest`, `workflow-chain-pack-manifest`, `artifact-type-pack-manifest`, `chat-card-pack-manifest`, `connection-pack-manifest`, `form-content-pack-manifest`, `frontend-plugin-manifest`, `registry-version-manifest` | pack | REQUIRED | | `agent-manifest`, `prompt-template` | pack (nested under a pack root) | REQUIRED | | `pack-lockfile` | host | closed | | `security-advisory` | registry | closed | | `prompt-ref` | leaf | none | Every pack-authored document MUST admit `patternProperties` `^(openwop-|x-|vendor\.)`. A consumer that does not recognize a hatch property MUST ignore it and MUST NOT reject the document; the value is pack-authored and therefore untrusted ([security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html)). #### Signing There is one signing scheme. `signing` on a version manifest (`schemas/v2/registry-version-manifest.schema.json`) is the closed object `{ keyId, scheme }`, both REQUIRED: - **`scheme`** MUST be `ed25519-canonical-json`: a detached 64-byte Ed25519 signature over the RFC 8785 (JCS) bytes of `pack.json` inside a deterministic tarball. The input MUST satisfy `conformance.md` §"Canonical JSON". - **`keyId`** is the signing key id. - A block carrying `method`, `publicKeyRef` or `signatureRef` fails validation. The same block applies to a **bare manifest** — the `pack.json` inside the tarball, which is the document the signature covers. `signing` is OPTIONAL there; when present it MUST be the identical closed `{ keyId, scheme }` object, on every bare manifest kind. - A verifier MUST verify the signature against the issuing registry's key for `keyId`, and MUST check the pack name against that key's `permittedNamespaces`. - A signature over tarball bytes is not a v2 signature; such a pack MUST be re-signed, not relabeled. Only a `signingKeys[]` entry whose `status` is `active` MAY sign a new publication. A key MUST stay listed while a served version names it, and a verifier MUST NOT refuse a version because its key is not `active`. #### Version manifests `kind` is REQUIRED on every version manifest and every bare manifest. Lifecycle flags sit outside the signature; changing one republishes the version manifest. - `versionDeprecated: true`: still served; a consumer MAY refuse to install it. - `yanked: true`: its manifest, tarball and signature stay served, and the pack index MUST NOT name it `latest` while an unyanked version exists. A range MUST skip it; a pin MAY resolve it. Advisory-listed versions MUST be yanked. A registry MUST refuse a submission that breaks these rules or republishes a version, with `pack_integrity_failure`, `pack_validation_failed`, `pack_signature_invalid`, `pack_engine_unsupported`, `pack_peer_dependency_undefined` or `version_conflict`. #### The registry's own schemas A registry MUST validate submissions against vendored copies of these schemas pinned to a corpus tag, and MUST re-sync them from that tag before any v2 publication. An unpinned or drifted vendored schema is a registry defect. #### Errors | Code | Raised when | | --- | --- | | `pack_engine_unsupported` | the range does not admit the host's protocol major (install, every path) | | `pack_peer_dependency_undefined` | a peer-dependency key is not a declaration-file key or an overlap alias | | `pack_signature_invalid` | the signature, key, or namespace check fails | #### Front-end plugin packs A `frontend-plugin` pack (`schemas/v2/frontend-plugin-manifest.schema.json`) ships opaque UI bundles that reach the host only over `ui-plugin/1` (`schemas/v2/ui-plugin-message.schema.json`); both schemas' rules bind. A host advertising `uiPlugins`: - MUST verify the pack signature before loading, failing closed; - MUST run every entry sandboxed from host context, DOM, origin storage and credentials, whatever `uiPlugins.isolation` names; never in-process, same-origin or federated (invariant `frontend-plugin-isolation`); - MUST serve a deny-egress policy apart from declared `connectSrc` (invariant `frontend-plugin-egress`), and let no BYOK material cross the boundary (invariant `frontend-plugin-no-byok`); - MUST ignore a message whose version tag it does not know, and refuse a method outside both allowlists with `method_not_allowed`, authorizing every call itself; - with `artifact.write`, MUST return an opaque `version` from each read and write and refuse a stale one with `artifact_conflict`, persisting nothing; - MUST treat a plugin needing an unadvertised `surfaces` or `hostApi` entry as inert there, not an error. A host without `uiPlugins` MUST reject the pack and render its own way. On the message protocol: - A breaking method change is `ui-plugin/2`. - `host.announce` MUST be length-capped (SHOULD ≤ 400 characters) and SHOULD be rate-limited. - `host.documentChanged` SHOULD be debounced. A plugin MUST treat the latest one as authoritative and tolerate unknown `host.selectionChanged` kinds. `maxEntryBytes` caps an entry bundle. #### During the v1 overlap - The v1 registry tree is frozen through the overlap, behind the v2 tree. - `testMode` advertises the v1 `/v1/packs-test/*` mirror, a conformance seam ([conformance.md §"The seams profile"](https://openwop.dev/spec/v2/core/conformance.html)). It remains advertisable through the overlap and is removed at 3.0. *Sources: RFCs 0117, 0119, 0130, 0177, 0212, 0222.* ### Persistence and Coexistence Source: https://openwop.dev/spec/v2/core/persistence.html > **Status: Stable.** > **Normative home:** `eventLog`. #### Why this exists How a v2 host reads what a v1 host wrote, what happens to a run in flight at the cut, and what each persisted store becomes — so two hosts read one log one way. #### The codemap is data `spec/v2/event-codemap.json`, shipped in `@openwop/spec-artifacts`, is the only authority for the v1→v2 event-type mapping. Every row is `decided`. - A host MUST NOT carry a private mapping. - A vendor-prefixed v1 type the codemap does not name MUST be read under its own name, unchanged. - "Vendor-prefixed" means the first segment is an org registered in the `extensions` object of `spec/v2/declaration.json` ([events.md §Rules](https://openwop.dev/spec/v2/core/events.html)). `openwop.` is the only reserved prefix. An unregistered first segment is not a vendor prefix and falls to the refusal in §"The reader rule". - An org is registered by pull request against the corpus and takes effect on the `@openwop/spec-artifacts` release that carries it. A shipped entry is append-only. #### The era key `eventLogSchemaVersion` is the era key. It is required on every run snapshot (`schemas/v2/run-snapshot.schema.json`). | Value | Meaning | | --- | --- | | absent | On a store a v1 host has ever written, the run MUST read as `2` (v1 era). | | `2` | v1 era; every reader translates through the codemap. | | `3` | v2 era; a v2 host MUST stamp `3` on every run it creates. | | `< 2` | The v1 rule is unchanged: snapshot fallback, no projection write-through. | Discovery MUST advertise the value the host writes for new runs and nothing else; a host MUST hold one constant for this axis. Absent stays era `2` forever; it is never backfilled. - A host MUST NOT rewrite historical rows to add an explicit `2`, and a reader MUST NOT require one. - A host with more than one creation path MUST begin stamping `3` on all of them in the same change. An unstamped path's runs read as era `2`. The snapshot field is required on the wire, and MAY be synthesized: for an era-`2` run with nothing stored, the host MUST supply `2` from the absent-⇒-`2` rule rather than fail the read. #### The `eventLog` family `eventLog` is the capability record by which a host advertises the era contract above. A host that advertises `eventLog`: - MUST stamp `eventLogSchemaVersion` on every run it creates; - MUST serve the cursor contract of [events.md §"Poll"](https://openwop.dev/spec/v2/core/events.html) over that log; - MUST NOT emit an event `type` the codemap does not name. The record makes no separate storage claim. Its `crossEngineOrdering` facet is a replay property, specified in [replay.md §"Cross-engine ordering"](https://openwop.dev/spec/v2/core/replay.html). #### The reader rule A v2 host reading a run in era `2` MUST translate every event through the codemap at the storage boundary: - `type` is mapped, and the payload projected with it. - `sequence` MUST be preserved verbatim, including `0`. - `eventId`, `timestamp`, `causationId`, and vendor fields pass through. - A type the codemap does not name and that carries no reserved vendor prefix MUST fail the read with `event_type_unmapped` (`spec/v2/errors.json`, `500`). - A malformed row MUST fail the read rather than default any field. The rule binds every reader: poll, SSE, fork, replay divergence, debug bundle, summary memory. The translation is a read projection. A host MUST NOT rewrite era-`2` rows in place. The one exception is a background backfill that stamps `3` and rewrites `type` under the same `(runId, sequence)` key: it is permitted only as an atomic per-run operation with the original preserved, so a fork prefix stays byte-equivalent ([replay.md](https://openwop.dev/spec/v2/core/replay.html)). ##### The writer rule The era key is fixed when the run is created and fixes the log's vocabulary for the run's lifetime. - An append to a run in era `2` MUST use v1 vocabulary — the name the codemap maps *from*, not the v2 name it maps to. - A host that upgrades mid-flight MUST NOT begin writing v2 names into a log the reader translates as era `2`. - A run created after the upgrade is era `3` and is written in v2 vocabulary, untranslated. - A writer that emits a property a closed def cannot seat MUST mark the row with what it could not seat, so the refusal names the writer. This binds every writer for as long as an era-`2` run stays open (§"Runs pinned to v1"). Its witness is `v2-era-2-append-vocabulary`. ##### The v1 wire of an era-`3` log Through the overlap ([versioning.md §5](https://openwop.dev/spec/v2/core/versioning.html)), an era-`3` log, stored in v2 vocabulary, must still read on `/v1/…` exactly as before the cut. A host serving both majors: - MUST map an era-`3` log's `type` back to its v1 spelling on the v1 read path, through the same codemap row, inverted. - MUST verify at load that `spec/v2/event-codemap.json` is a bijection. If a row folds two v1 names onto one v2 name, the host MUST refuse to serve the v1 representation rather than guess a spelling. - MUST emit a type with no codemap row (v2-only vocabulary, which has no v1 spelling) unchanged on the v1 read path, MUST NOT drop the row, and MUST NOT refuse the read for it. ##### The seat - The adapter MUST sit at the storage boundary every reader passes through — the storage interface's event-list method, not a wrapper some call sites bypass. - A host leg MUST name its seat in its ADR. The seat is a claims-check ([conformance.md §Witness class](https://openwop.dev/spec/v2/core/conformance.html)): it is discharged by that disclosure and by audit, never by the wire. It binds every reader, including ones the suite has no name for. #### Runs pinned to v1 A non-terminal run a v2 host inherits carries `version.pinned` events naming change ids. The host MUST continue it or cancel it, never follow a pin silently. - **Every pinned change id is still implemented** — the run MUST continue under the reader rule. The pin is honored verbatim and `version.pinned` is never rewritten. - **Any pinned change id is no longer implemented** — the host MUST cancel the run with `run.cancelled` reason `v1_pin_unsupported` and `cancelledBy: "v2-cutover"`. The certification bundle reports the count. - **Suspended on an interrupt at the cut** — the run continues under the rules above; its token drains per §"Everything else a v1 host persisted". Multi-region skew is read-side only: after the cut, a v2 region MUST NOT accept an era-`2` write for a run it has already stamped `3`. Discovery's `minClientVersion` rule is in [versioning.md §1.5](https://openwop.dev/spec/v2/core/versioning.html). #### Everything else a v1 host persisted - **Certification bundles** — never upgraded. A v1 bundle substantiates no new certification after 2026-11-10; every host produces a fresh v2-rc bundle before the cut ([conformance.md](https://openwop.dev/spec/v2/core/conformance.html)). - **Webhook deliveries** — dual-emitted, and queued deliveries drained, per [webhooks.md §"Dual emission through the overlap"](https://openwop.dev/spec/v2/core/webhooks.html). - **Interrupt resume tokens** — drained, per [identity.md](https://openwop.dev/spec/v2/core/identity.html) §4. - **Layer-1 and Layer-2 records** (idempotency, idempotent responses, invocation claims and logs, effect-escape ledger, dispatch outbox, envelope correlations) — unchanged; keyed on ids the cut does not rename. `GET /runs/{runId}/effects` and `GET /runs/{runId}/compensation` are new reads over them ([security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html)). - **Owner stamps** — a run without a Subject is legacy-stamped per [identity.md](https://openwop.dev/spec/v2/core/identity.html) §1.2. A host's stored owner fields are projected to the Subject; the projection is the host's to name. - **Audit log** — never upgraded. #### Per-store disposition A host's ADR MUST name every store it persists and give each one disposition from the closed set below. A host MUST NOT decide a store's disposition during the migration. | Disposition | Meaning | | --- | --- | | `unchanged` | Rows keep their shape and keys; a v2 reader consumes them as they are. | | `translated` | Read through the codemap at the storage boundary; rewritten in place only by the atomic per-run backfill. | | `drained` | Rows complete under their own v1 contract until exhausted or expired; no new v1-shaped rows are written. | | `legacy-stamped` | A missing v2 field gets its legacy value at first v2 read and is never rewritten. | | `never-upgraded` | Rows remain v1 evidence only; v2 evidence is produced fresh. | | `not-persisted` | Nothing to migrate. | Template — one row per store: | Store | v1 artifact | Disposition | | --- | --- | --- | | events | v1 vocabulary; `UNIQUE (runId, sequence)` | `translated` | | runs | no `eventLogSchemaVersion`; owner fields | `legacy-stamped` | | interrupts | un-prefixed (v1) tokens | `drained` | | webhook subscriptions and queued deliveries | subscriptions; serialized deliveries | `unchanged`; `drained` | | idempotency, invocation, outbox, correlation tables | keyed records | `unchanged` | | audit log | audit facts | `never-upgraded` | | certification bundles | v1 bundles | `never-upgraded` | | host-internal tables | outside the wire | `unchanged` | #### Durable acceptance and recovery - **Acceptance** — a host that returns success for work it accepted MUST have made the intent to perform it durable in the same transaction as the work record. A wakeup, hint, or in-process dispatch MUST NOT be the only record. The host MUST resume accepted-but-unstarted work after the accepting process dies, without client action. - **Liveness** — a host MUST distinguish how long a unit of work may legitimately run from the interval in which a live worker demonstrates liveness, and MUST NOT use the duration bound as the sole liveness signal. - **Recovery bound** — a host MUST declare the longest interval between an instance ceasing to make progress and another becoming eligible to resume its work. It MUST derive the bound from the mechanism that enforces it: one bound per enforcing mechanism, never a single aggregate. Any length is conformant; an undeclared or unenforced bound is not. - **Duplicates** — duplicate delivery of accepted work MUST NOT produce duplicate external effects. The host MUST dedupe on an identity that survives redelivery ([idempotency.md](https://openwop.dev/spec/v2/core/idempotency.html)). - **Poison work** — work that fails deterministically MUST reach a terminal, operator-visible state within a bounded number of attempts. A host MAY claim a qualification rung (`durable-single-instance`, `durable-multi-instance`, `multi-region-qualified`; cumulative) only with the evidence [RFC 0158 §D](https://github.com/openwop/openwop/blob/main/RFCS/0158-durable-execution-and-disaster-recovery-qualification.md) names for it, and MUST NOT claim one from tests in which no process was terminated. A rung is evidence, not a capability: discovery carries none, and the certification bundle publishes the rung and its bounds ([conformance.md §Bundle v3](https://openwop.dev/spec/v2/core/conformance.html)). *Sources: RFCs 0041, 0158, 0170, 0171, 0172, 0176, 0180, 0185, 0186, 0187.* ### Portability Source: https://openwop.dev/spec/v2/core/portability.html > **Status: Stable.** > **Normative home:** `portability`. #### Why this exists A tenant's reusable estate moves between hosts as one export bundle, and lands on the destination under the importing caller's identity. An adapter that turns another platform's export into a bundle is host tooling. #### The export bundle An `ExportBundle` (`schemas/v2/export-bundle.schema.json`) carries `bundleVersion: "2"`, a `source` (its origin and an informational `originPrincipal`) and `items[]`. Each item has: - a `kind`: `agent`, `pack`, `prompt-template`, `connection-ref`, `schedule`, `roster` or `org-chart`; - a bundle-local `ref` and optional `dependsOn` edges; - a `payload` shaped by that kind's own schema. #### The `portability` record - **`export`** — the host emits a bundle for the caller's tenant or workspace, optionally limited to some `kinds`. - **`import`** — the host applies a bundle. - **`kinds`** — the item kinds the host exports and imports. - **`dryRun`** — import offers a plan preview that writes nothing. A host advertising `import` MUST advertise `dryRun: true`. #### Import rules - **No credential material.** A bundle MUST NOT contain credential values: a `connection-ref` item carries only references and provider ids. A host MUST reject with `422` an imported bundle whose payload carries a literal credential value. The importer MUST report unbound references in `secretsToRebind` and MUST NOT invent or transfer secret material. - **Dry run.** When `import` is advertised, import MUST offer a dry run, which MUST NOT write and MUST return the plan it would execute: creates, updates, skips, conflicts and unbound credential references. - **Idempotent.** Import MUST be idempotent: re-applying a bundle resolves each item to `skipped` or `updated`, never to a duplicate create. - **Ordered.** Items MUST be applied in `dependsOn` topological order. A cycle is a `422`. - **Re-owned.** Every imported entity MUST be re-owned to the caller's Subject at the destination ([identity.md](https://openwop.dev/spec/v2/core/identity.html) §1). `source.originPrincipal` is informational and MUST NOT grant any access. Host-scoped handles are re-minted as [identity.md](https://openwop.dev/spec/v2/core/identity.html) §5 requires. - **Executable behavior.** An imported agent or pack that is executable behavior SHOULD pass through the host's install policy ([RFC 0043](https://github.com/openwop/openwop/blob/main/RFCS/0043-registry-and-extension-policy.md)), and MAY be staged as a proposal ([RFC 0096](https://github.com/openwop/openwop/blob/main/RFCS/0096-reviewable-learning-skill-proposal-lifecycle.md)) instead of being activated. Applying an import returns an outcome per item — `created`, `updated`, `skipped` or `failed` — plus `secretsToRebind`. #### The `import.applied` event An applied import emits `import.applied` (`schemas/v2/run-event-payloads.schema.json`). A host MUST NOT emit it unless it advertises `portability`. It carries counts and references only, never item payloads or secret values. #### Routes No protocol path is defined for export or import: a host serves them on routes of its own. A host-private migration from an anonymous sandbox into a signed-in tenant fits this contract. *Sources: RFCs 0043, 0096, 0098.* ### Replay and Fork Source: https://openwop.dev/spec/v2/core/replay.html > **Status: Stable.** > **Normative home:** `eventLog`, `replay`, `nondeterminismPolicy`. #### Why this exists `POST /runs/{runId}:fork` makes any past run state re-executable: a replay proves current code reproduces recorded history; a branch explores an alternative from a recorded point. This document states what a fork MUST reproduce, what it MUST NOT re-fire, and how a host proves the second. #### The surface A host advertising `replay` ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)) serves `forkRun` (`api/v2/openapi.yaml`, `POST /runs/{runId}:fork`) and `getEffectSeamManifest` (`GET /host/effect-seams`). The `replay` facet (`spec/v2/facets/replay.schema.json`) is `{ modes[], retention?, effectSeamsManifest }`: - `modes` enumerates `replay | branch` (the `forkRun` `mode` values); - `effectSeamsManifest` is the constant `/host/effect-seams`. Suppression is the only conforming replay behavior; no field turns it off, and `none` is not a value. The request body, `fromSeq` defaults and `201` response are [runs.md §Fork](https://openwop.dev/spec/v2/core/runs.html). Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are re-executed. Refusals: - `fromSeq` out of range — `400`. - A sequence absent from the source log — `422`. - A `fromSeq` greater than the sequence of the source run's terminal run event MUST be refused `422 fork_point_invalid`: the fork would inherit a terminal event and then execute. This binds only where a compensation tail follows the terminal event. - Source run not visible to the caller — `404`. #### Modes **`replay`** re-executes the workflow against current code from `fromSeq`, consuming the source run's events as fixed history. **`branch`** starts from the projected state at `fromSeq` with caller-supplied `runOptionsOverlay`. A branch is an independent run and is NOT deterministic by design; determinism and suppression apply only to the inherited prefix. #### Byte-equivalence of the prefix The replay contract is observable-output-sequence determinism, not bit-equivalent execution: 1. The events at indices `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo per-region clock fields and ULID time-component entropy when ULIDs are minted fresh. The event at `fromSeq` is governed by §Divergence. 2. `variables`, `channels`, and `status` of the run snapshot at each index in that range MUST be byte-equivalent. 3. The bytes on the wire of underlying tool and LLM calls MAY differ, provided the observable state at each index is byte-equivalent. A host MUST cache the observable result (return value, workflow-state effects, emitted events), not merely the tool-call boundary. The cache key for LLM-calling nodes is the content-addressed invocation key defined in [RFC 0041](https://github.com/openwop/openwop/blob/main/RFCS/0041-multi-agent-replay-under-nondeterminism.md); for other tool-calling nodes it MUST be content-addressable, never a host-internal sequence number or timestamp. #### Determinism caveats (`replay` mode) 1. A side-effecting node MUST NOT call the external system twice; see §Suppression. 2. `ctx.interrupt(K)` MUST short-circuit to the persisted `interrupt.resolved`, raising no new `interrupt.requested`. 3. `ctx.getVersion` pins from the source run are fixed history; the replay MUST take the recorded branch. 4. Nodes MUST consume time via `ctx.now()` where available; direct clock reads are non-deterministic. 5. Recorded-fact events such as `memory.written` are fixed history. A replay MUST re-emit them verbatim from the log and MUST NOT regenerate their identifiers or timestamps — never a new `memoryId`. A `branch` MAY perform its own memory writes with fresh identifiers. 6. Approver eligibility recorded on a resume event is fixed history; a host MUST NOT re-resolve membership during replay. #### Divergence When a replayed node produces an event different from the source at the same sequence, the host: - MUST continue; - MUST emit `replay.diverged` `{ originalEventId, replayEventId, divergencePoint }`; - MUST surface it in `debug` stream mode and as OTel attribute `openwop.replay.diverged: true`. Divergence codes (`spec/v2/errors.json`): - **`replay_diverged_at_refusal`** (fork fails, `409`) — the source obtained a valid envelope and the replay a refusal, or the reverse. The host MUST NOT substitute silently; it MUST emit `replay.diverged-at-refusal` naming the node and both envelope kinds and fail the replay with this code. - **`replay_source_missing`** (`node.failed` payload; the fork request still returns `201`) — a side-effecting node reached with no recorded source outcome (§Suppression). - **`replay_memory_snapshot_unavailable`** (fork refused, `409`) — the host cannot serve memory state as-of `fromSeq`. It MUST refuse rather than substitute current memory; `details.fromSeq` SHOULD name the index. - **`replay_context_summary_unavailable`** (fork refused, `409`) — the host advertises `multiAgent.executionModel.contextBudget.summarization` and cannot serve, as-of `fromSeq`, a summary artifact (`context.summarized.summaryRef`) the replay would reuse. It MUST refuse rather than re-summarize; `details.fromSeq` SHOULD name the index. #### Suppression Suppression is an obligation of the `replay` surface: advertising `replay` binds it, and a host that cannot suppress MUST NOT advertise `replay`. A relaxation is an operator setting recorded in the certification bundle (security-defaults.md). For a fork with `mode: replay`: 1. A node that performs an external side effect — any operation observable outside the run's own event log — MUST NOT perform it. 2. The host MUST resolve the node's outcome from the source run's `n`th recorded terminal outcome for the node, keyed on `(sourceRunId, nodeId, n)`, never on the fork's own `runId`, where `n` is one more than the node's `node.completed` and `node.failed` events before this execution, the fork's inherited prefix included. 3. Absent a recorded outcome, the host MUST fail the node closed with `replay_source_missing`, MUST NOT perform the effect, and MUST NOT substitute a synthesized or empty success. 4. A node whose pack manifest declares `role: "side-effect"` MUST be treated as side-effecting; a host classifier MAY add nodes and MUST NOT remove any. A throwing seam satisfies rule 1 only. 5. The guarantee is whole-run and requires both classification before execution and a default-deny guard at every effect seam. 6. A dispatch to a peer host is an outbound call under rule 2; the peer is never contacted. Pure nodes and LLM calls served from the invocation log MUST re-execute live. **Fan-out.** A host that projects its log outward — webhook delivery, A2A push, outbound streams, analytics or audit sinks — MUST NOT deliver events a replay re-emits as fixed history, and a fork of either mode MUST NOT inherit its source's A2A push configs. Replay-ness MUST be read from the run, never from the event type; the fork's own log MUST still carry the re-emitted events ([webhooks.md](https://openwop.dev/spec/v2/core/webhooks.html)). **Branch.** A branch re-fires effects for sequences `>= fromSeq`. A host MAY suppress branch effects and MUST NOT report that as replay suppression. A host SHOULD surface the re-fire in operator-facing fork UI. ##### The effect-seam manifest A host advertising `replay` MUST publish `schemas/v2/effect-seam-manifest.schema.json`-shaped data at `GET /host/effect-seams`: `{ manifestVersion: "1", host: { name, build }, seams[] }`, one row per outbound effect path its node runtime can reach, `{ seam, kind, guarded: true, guardedBy, branchReFires?, note? }`. The host owns the list. The suite drives one seam of each kind it can reach and observes no re-fire (`effect-seam-manifest`, [conformance.md](https://openwop.dev/spec/v2/core/conformance.html)). `kind` names the outbound wire mechanism the seam leaves the host by — not the suite's driving mechanism, and not the business purpose. It MUST be one of `http`, `smtp`, `queue`, `storage`, `provider-sdk`, `webhook-fanout`, `other`. - Two seams a host guards through one code path but that leave by different mechanisms are different `kind`s; two that leave by the same mechanism for different business reasons are one. - `other` is the escape for a mechanism this list does not name — raw TCP, gRPC, a filesystem write, a device SDK. A row using it MUST carry `note` naming that mechanism. **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite cannot drive (typically `smtp` or `other`); the scenario records that one `inapplicable`, naming the mechanism. A host MUST NOT omit a seam because the suite cannot drive it, and MUST NOT relabel it as a `kind` the suite can drive. #### Replay-from-event-log internals 1. Load the source run's events with `sequence < fromSeq` through the storage boundary, where an era-`2` log is translated (persistence.md). 2. Fold them to a projected state. 3. Initialize the new run with that state, copy-on-write into its own log. 4. For `replay`, resolve side-effecting nodes from the source run's recorded outcomes keyed on `(sourceRunId, nodeId, n)`; LLM invocations additionally consult the invocation log via the content-addressed invocation key. 5. For `branch`, executor invocations create new invocation-log entries keyed on the new `runId`. #### Forking a v1 run A v2 host MUST fork a run created before the cut (era `2`, [persistence.md](https://openwop.dev/spec/v2/core/persistence.html)). The `fork-a-v1-run` scenario witnesses this. - The fork's prefix MUST be byte-equivalent to the *translated* parent — the parent as read through the codemap, not its stored bytes. - `run.started` on the fork MUST carry the legacy Subject (`issuer: urn:openwop:legacy`, [identity.md](https://openwop.dev/spec/v2/core/identity.html)) where the parent had none. - A backfill of an era-`2` log is permitted only atomically per run with the original preserved. #### Cross-engine ordering When a host advertises both `idempotency.multiRegion` and `eventLog.crossEngineOrdering`, a fork served by a region other than the one that wrote the run MUST produce the same observable state at the `fromSeq` boundary as a fork served by the writing region: - `status`, `variables`, and the projected event log up to `fromSeq` MUST be byte-equivalent across regions. - Per-region wall-clock and entropy fields in events after the boundary MAY differ. A host advertising only one of the two, or neither, keeps the single-region contract. #### Retention A host advertising `replay` MUST document retention for source snapshots, source logs, the invocation records replay depends on, and forked runs; `retention.days` MAY advertise the window. When the range `fromSeq` needs has expired, the host MUST reject the fork with `410 run_expired` or `422`; `details` SHOULD carry `sourceRunId`, `fromSeq`, and the boundary. #### Declared nondeterminism `nondeterminismPolicy` is the host's statement of which nondeterministic sources it declares rather than suppresses. A host advertising `nondeterminismPolicy.declared` MUST record every declared source in the run's event log at the point it is read, so a fork replays the recorded value rather than re-drawing it. A source the host neither declares nor suppresses is a replay defect, not a policy choice. *Sources: RFCs 0036, 0039, 0041, 0057, 0104, 0111, 0140, 0173, 0176, 0194, 0228.* ### Runs Source: https://openwop.dev/spec/v2/core/runs.html > **Status: Stable.** > **Normative home:** `runList`, `limits`, `conversationPrimitive`, `dataResidency`, `deadLetter`, `budget`. #### Why this exists A run is the unit of execution, ownership and observation. This document covers the run surface of `api/v2/openapi.yaml`: how a run is created, read, streamed, cancelled, paused, forked and diffed. Every host projects the same snapshot shape from the same event log ([events.md](https://openwop.dev/spec/v2/core/events.html)). #### Identity Every id `$ref`s `schemas/v2/ids.schema.json` ([identity.md](https://openwop.dev/spec/v2/core/identity.html)). A `runId` is host-minted and tenant-bound: `<tenantId>/<opaque>`. - A caller MUST treat every id as opaque. - A host MUST reject a `runId` whose tenant segment is not the caller's with `403 id_tenant_mismatch`, and MUST NOT disclose whether the run exists. #### Surface Every operation accepts `OpenWOP-Version` ([overview.md](https://openwop.dev/spec/v2/core/overview.html)) and every response carries it. Every mutating operation accepts `Idempotency-Key` ([idempotency.md](https://openwop.dev/spec/v2/core/idempotency.html)). Scopes are the vocabulary listed in `api/v2/openapi.yaml`'s security schemes, matched as [identity.md](https://openwop.dev/spec/v2/core/identity.html) §2.1 describes. | Operation | Method and path | Scope | Gate | | --- | --- | --- | --- | | `createRun` | `POST /runs` | `runs:create` | — | | `getRun` | `GET /runs/{runId}` | `runs:read` | — | | `listRuns` | `GET /runs` | `runs:read` | `runList` | | `streamRunEvents` | `GET /runs/{runId}/events` | `runs:read` | [events.md](https://openwop.dev/spec/v2/core/events.html) | | `pollRunEvents` | `GET /runs/{runId}/events/poll` | `runs:read` | [events.md](https://openwop.dev/spec/v2/core/events.html) | | `cancelRun` | `POST /runs/{runId}/cancel` | `runs:cancel` | — | | `bulkCancelRuns` | `POST /runs:bulk-cancel` | `runs:cancel` | — | | `pauseRun` | `POST /runs/{runId}:pause` | `runs:cancel` | — | | `resumeRun` | `POST /runs/{runId}:resume` | `runs:cancel` | — | | `forkRun` | `POST /runs/{runId}:fork` | `runs:create` + `runs:read` | `replay` | | `diffRun` | `GET /runs/{runId}:diff?against=` | `runs:read` on both | OPTIONAL | | `getRunAncestry` | `GET /runs/{runId}/ancestry` | `runs:read` | `multiAgent.executionModel.crossHostCausation.ancestryEndpointSupported` | | `createAnnotation` / `listAnnotations` | `POST` / `GET /runs/{runId}/annotations` | `runs:annotate` / `runs:read` | `feedback` | | `getArtifact` | `GET /runs/{runId}/artifacts/{artifactId}` | `artifacts:read` | — | | `getEvalSummary` | `GET /runs/{runId}/eval-summary` | `runs:read` | `agents.evalSuite` | | `getRunCompensation` | `GET /runs/{runId}/compensation` | `runs:read` | `compensation` | | `getRunEffects` | `GET /runs/{runId}/effects` | `runs:read` | `idempotency` | A gated operation the host does not advertise (or an absent `diffRun`) answers `404 not_found` ([errors.md](https://openwop.dev/spec/v2/core/errors.html)). #### Create The `createRun` body is closed (`unevaluatedProperties: false`). Its fields: - `workflowId` — REQUIRED unless `mode: eval`. - `inputs`, `residency`, `tenantId`, `scopeId`. - `callbackUrl` — see [interrupt.md](https://openwop.dev/spec/v2/core/interrupt.html) §Callback delivery. A refused value is `400 validation_error` with `details.field: "callbackUrl"`. - `mode`, `evalSuiteRef`, `agentId`. - `runSecrets` — run-supplied secrets, accepted only by a host advertising `secrets.runSecrets` ([host-services.md](https://openwop.dev/spec/v2/core/host-services.html) §Run-supplied secrets). It is never part of `RunOptions`. - The `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`. ##### Request headers | Header | Rule | | --- | --- | | `Idempotency-Key` | RECOMMENDED. A replayed create MUST NOT create a second run, and carries `OpenWOP-Idempotent-Replay: true`. | | `OpenWOP-Dedup: enforce` | The host MUST reject a duplicate `(tenantId, scopeId)` with `409 run_already_active` and `Retry-After`. | | `OpenWOP-Force-Engine-Version` | Test keys only (the seams profile). A host MUST reject it on a production credential with `403`. | ##### Response The `201` response is `{ runId, status, eventsUrl, statusUrl? }`. `status` is one of `pending`, `running`, `waiting-approval`, `waiting-input`, `waiting-external`. - `eventsUrl` and `statusUrl` MUST resolve under the origin the request was made to — a relative path, or an absolute URL on the same origin — and MUST NOT downgrade the scheme. A link naming a different host, or `http://` on an `https://` origin, is non-conformant. - The base that minted `runId` MUST resolve it: `GET /runs/{runId}` and `GET /runs/{runId}/events/poll` at that base MUST answer `200` for the returned id, percent-encoded per [identity.md](https://openwop.dev/spec/v2/core/identity.html) §5. ##### Refusals - `mode: eval` makes `evalSuiteRef` and `agentId` REQUIRED. It starts an eval-suite projection that emits the content-free `eval.*` family and terminates with an `EvalSummary`. A host that does not advertise `agents.evalSuite` MUST reject it `422 capability_not_provided`. - A host advertising `dataResidency` MUST reject a `residency.region` outside `dataResidency.regions` with `422 residency_unavailable` and create no run (see §"Conversation and residency capabilities"). - A workflow that references a capability-gated reserved node type on a host that does not advertise the capability MUST be rejected with `422 capability_required`. - A host that lists `budget.onExhaustion` serves exactly those values. It MUST reject any other `budget.onExhaustion` with `422 capability_not_provided` and create no run. It MUST NOT apply a different behaviour or ignore the field. ##### The start event `run.started` ([events.md](https://openwop.dev/spec/v2/core/events.html)) MUST echo the run's `owner` block exactly as `RunSnapshot.owner` carries it. Its `transport` records `rest`, `mcp`, `a2a` or `ui`. #### Run options `schemas/v2/run-options.schema.json` is `{ configurable?, tags?, metadata? }`. `configurable` is `schemas/v2/configurable.schema.json`: closed, nested and versioned. The request body `$ref`s it directly; there is no `allOf`-merge of an open map. `version` is REQUIRED and is `1`. It has five sections: | Section | Keys | | --- | --- | | `run` | `recursionLimit`, `runTimeoutMs`, `maxLoopIterations`, `escalationThreshold` | | `ai` | `provider`, `model`, `temperature` (0..2), `maxTokens`, `credentialRef`, `promptOverrides`, `mockProvider`, `reasoningVerbosity` (`none` \| `summary` \| `full`), `maxRefusals` | | `distillation` | `tokenBudget` | | `budget` | `schemas/v2/budget-policy.schema.json` — the run's budget policy | | `extensions` | `<org>: {…}` — a vendor key lives under its registered org and nowhere else | ##### `run` section - `recursionLimit` is clamped to `limits.maxNodeExecutions`. A breach, counted in node starts, MUST emit `cap.breached { kind: 'node-executions' }` and fail the run with `recursion_limit_exceeded`. - `runTimeoutMs` resolves to `min(runTimeoutMs, limits.maxRunDurationMs)`, measured from `run.started`. A breach MUST emit `cap.breached { kind: 'run-duration' }` and terminate the run `failed` with `run_timeout`. - `maxLoopIterations` resolves against `limits.maxLoopIterations`, counted in orchestrator turns. A breach MUST emit `cap.breached { kind: 'loop-iterations' }` and fail with `loop_limit_exceeded`. - An out-of-range `recursionLimit` or `runTimeoutMs` MUST return `400 validation_error` at create. After a breach the host schedules nothing more. - `escalationThreshold` is the `low-confidence` threshold ([interrupt.md](https://openwop.dev/spec/v2/core/interrupt.html)). ##### Limits `limits` always carries `clarificationRounds` (per task), `schemaRounds` (per envelope) and `envelopesPerTurn` (per chat turn). - A host advertising `maxNodeExecutions`, `maxRunDurationMs` or `maxLoopIterations` MUST enforce it. - On any breach the host MUST emit `cap.breached` and fail the node or run. The event carries `nodeId` for a `clarification` or `schema` breach. Its `observed` exceeds `limit` and MUST be reused on replay and fork, never recomputed. - `budget.maxTokens` and `budget.maxCostUsd` clamp to `maxBudgetTokens` and `maxBudgetCostUsd`. - `maxRequestBodyBytes` is the largest REST request body accepted. ##### `ai` section - `provider` MUST be in `aiProviders.providers`, else `400 validation_error`. - `credentialRef` MUST reference a provider in `aiProviders.byok`, else `403 credential_forbidden`. It never carries key material. - `mockProvider` is test-keys-only: a host MUST refuse it on a production credential with `403`. - `maxRefusals` is the refusal ceiling ([events.md](https://openwop.dev/spec/v2/core/events.html) E5). ##### `distillation` section `tokenBudget` resolves to `min(tokenBudget, memory.distillation.maxTokenBudget)`. A run that cannot distill within it MUST fail atomically with `token_budget_exceeded`. ##### `budget` section `budget` caps a run's spend. The effective budget is the minimum across the `scopes` that apply (run, workflow, agent, project), clamped per §Limits; only the run scope has a wire surface. Whichever of `budget` and the `run` section binds first fires its own `cap.breached` kind. - `budget.reserved` records the effective budget, and consumption is derived from `provider.usage`, `agent.toolCalled` and `node.retried`, never measured twice. A replay reuses both. - Under either `enforce` mode a host MUST emit `budget.reserved`, `budget.threshold-crossed` and `budget.exhausted`, and MAY coalesce `budget.consumed`. - `hard` exhaustion under `onExhaustion: fail` emits `cap.breached` (`kind: budget-*`) and fails the run `budget_exhausted`. Under `interrupt`, where the host serves it, it raises an approval whose `resumeValue` adds budget, recorded by a second `budget.reserved`. An `advisory` host MUST NOT stop the run. - A resolved model outside `modelAllow`, or in `modelDeny` (which wins), is refused `budget_model_denied` before the call. - `dimensions` lists only what the host enforces and MAY omit `cost`. The `budget.*` events and `cap.breached` MUST NOT carry rate cards, unit prices, cost breakdowns, credentials or model prose (`budget-no-pricing-leak`); the aggregate cost is allowed. ##### Validation and persistence - An unknown root key, an unknown key inside a section, or a dotted key (`ai.provider` as a string key) MUST be rejected with `400 validation_error`. - A host MUST persist `RunOptions` on the run at creation, MUST surface the same `configurable` to every attempt of a node, and MUST NOT allow `configurable` to change after creation. - A workflow's `configurableSchema` MUST be validated against at create time and MUST be surfaced on `getWorkflow`. ##### `tags` and `metadata` - `tags` is an opaque string array: at most 100 entries, each at most 256 characters, valid UTF-8. A host MUST NOT reject a tag on format, and MUST return `400 validation_error` over the limits. - `metadata` is a free-form JSON object. A host MUST persist it, and the engine MUST NOT consume it for any execution decision. - Both surface unchanged on `RunSnapshot`. #### Snapshot `getRun` returns `schemas/v2/run-snapshot.schema.json`: the fold of the event log through the run projection. The object is closed. `runId`, `workflowId`, `status`, `owner` and `eventLogSchemaVersion` are REQUIRED. | Field | Meaning | | --- | --- | | `owner` | `{ tenant, workspace?, subject }`, closed; `subject` REQUIRED (`schemas/v2/subject.schema.json`) | | `status` | Run state (below) | | `eventLogSchemaVersion` | The era key, integer ≥ 2 ([persistence.md](https://openwop.dev/spec/v2/core/persistence.html) §"The era key") | | `engineVersion` | Integer | | `compensationStatus` | `none`, `pending`, `running`, `completed`, `partial`, `failed`, `manual` | | `currentNodeId` | Set while suspended; names the node holding the interrupt | | `error` | `{ code, message, details? }` on terminal `failed` | | `configurable`, `tags`, `metadata` | The persisted `RunOptions` | | `agent`, `runOrchestrator` | `schemas/v2/agent-ref.schema.json` | | `metrics.openwopCost` | `{ usd, tokens { input, output }, model, provider, duration_ms }`; absence is not zero | Field rules: - `status` is one of `pending`, `running`, `paused`, `waiting-approval`, `waiting-input`, `waiting-external`, `completed`, `failed`, `cancelling`, `cancelled`. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event`. `cancelling` is the state between an accepted cancel and the terminal `cancelled`. The vocabulary grows by [overview.md](https://openwop.dev/spec/v2/core/overview.html) §0. - `owner`: a run created before the host emitted subjects reads with the subject rule in [identity.md](https://openwop.dev/spec/v2/core/identity.html), stamped at first read and never rewritten. - `compensationStatus`: a host that does not advertise `compensation` MUST omit it. A host that does MUST include it on every snapshot, `none` when never requested. - `runOrchestrator` MUST NOT change for the run's lifetime. ##### Caching and encoding - The `200` SHOULD carry a strong `ETag` derived from the latest persisted `sequence`. When present it MUST change on every observable transition and be stable otherwise. - When the host sends an `ETag`, a request whose `If-None-Match` matches it MUST receive `304` with no body. - A host MAY compress (`gzip` baseline; `br` and `zstd` only where advertised under `extensions["<org>.rest-transport"].contentEncodings`, [ext/restTransport](https://openwop.dev/spec/v2/ext/restTransport/)). It MUST then set `Content-Encoding` and `Vary: Accept-Encoding`. The decoded body is byte-identical. #### List `GET /runs` (gated on `runList`) returns `{ runs: RunSnapshot[], nextCursor? }`: the caller's runs, newest first. - Only runs whose tenant segment is the caller's appear, and every `runId` is bound ([identity.md](https://openwop.dev/spec/v2/core/identity.html) §5). A run the caller created MUST appear. - A page MUST NOT exceed `runList.maxPageSize`. - `cursor` is opaque. A cursor the host did not mint MUST be refused with `400 validation_error`. - `workflowId` and `status` are exact-match filters when `runList.filters` names them. An unadvertised filter is ignored. #### Cancel `cancelRun` accepts `{ reason? }` and answers `200 { runId, status }`, where `status` is `cancelling` or `cancelled`. The cascade MAY be asynchronous; the run emits `run.cancelled` when it completes. - A cancel on a terminal run (`completed`, `failed`, `cancelled`) MUST be refused with `409 run_terminal`. A `200` echoing the terminal state is non-conformant. - Cancelling a parent MUST NOT silently abandon an active compensation ([security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html)). - `run.cancelled.parentRunId` with `reason: parent-cancelled` records a cascade from a parent. ##### Bulk cancel `bulkCancelRuns` accepts `{ runIds[1..100], reason? }`. - Over the host's cap (RECOMMENDED 100) it MUST return `400 validation_error` with `details.maxRunIds`. - The host MUST process each id independently, and MUST return `200 { results[] }` in request order, even when every id failed. - The host MUST enforce authorization per id. A run the caller cannot see yields `ok: false` with an error envelope in that entry, never a top-level `403`. Per-entry errors ([errors.md](https://openwop.dev/spec/v2/core/errors.html)): | Condition | Code | | --- | --- | | The id's tenant segment is not the caller's ([identity.md](https://openwop.dev/spec/v2/core/identity.html) §5 applies inside an entry as on a path) | `id_tenant_mismatch`, or `not_found` where existence is not leaked | | A run in the caller's tenant the caller may not cancel | `run_forbidden` | | A run already terminal | `run_terminal` | `ok: true` carries `status` `cancelling` or `cancelled`; `ok: false` carries the error envelope. #### Pause and resume `pauseRun` accepts `{ reason?, drainPolicy? }` and answers `202 { runId, status: 'paused', pausedAt? }`. The transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word. `drainPolicy` is one of: - `drain-current-node` (default) — the executing node reaches a terminal first. - `immediate` — the run is snapshotted between events. The executing attempt is cut: it has no terminal node event, a host MUST NOT record `node.failed` (or any terminal node event) for it, and the resumed run's `node.started` begins a fresh attempt. `run.paused` itself records the interruption; its payload MAY carry `interruptedNodeId` and `interruptedAttempt`. `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, and emits `run.resumed`. Rules: - A pause on a run that is already paused, terminal, or otherwise unpausable MUST receive `409`. - A resume on a run that is not paused MUST return `409`. - In both cases the code is `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the refusing status. - Only `resumeRun` or a cancel exits `paused`. - A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state. #### Fork `forkRun` accepts `{ mode: replay | branch, fromSeq?, runOptionsOverlay? }`. Events with `sequence < fromSeq` are fixed history; events `≥ fromSeq` re-execute. - `fromSeq` is REQUIRED for `branch` and defaults to `0` for `replay`. - `runOptionsOverlay` is `branch`-only. A `replay` with a non-empty overlay MUST be rejected with `400`. - A `fromSeq` not in the source log MUST be rejected with `422 fork_point_invalid`. The `201` response is `{ runId, sourceRunId, fromSeq?, mode, status, eventsUrl }`. The child's `owner` is copied verbatim from the parent. Determinism, side-effect suppression, and forking an era-2 parent are in [replay.md](https://openwop.dev/spec/v2/core/replay.html). #### Diff and ancestry `diffRun` returns `schemas/v2/run-diff-response.schema.json`: `divergedAtSeq`, ordered `eventDiffs[]`, `stateDiff`, optional `truncated`. - The diff MUST be a pure function of the two logs. Identical logs MUST yield `divergedAtSeq: null` and empty `eventDiffs`. - `eventId`, `runId`, `timestamp` and other run-scoped fields MUST be excluded from comparison. - A host that diffs an in-flight prefix MUST set `truncated: true`. - A caller lacking `runs:read` on either run MUST receive `403`. `getRunAncestry` returns `schemas/v2/run-ancestry-response.schema.json` (`runId`, `hostId`, `parent` or `null`). A client walks the chain one hop at a time via `parent.wellKnownUrl`. `parent` is the dispatching run, `cause` its composition mechanism. A fork is not dispatched (its lineage is `parentRunId`): its `parent` MUST be `null`. #### Annotations, artifacts, eval summary ##### Annotations `createAnnotation` accepts `schemas/v2/annotation-create.schema.json` and returns `201` with `schemas/v2/annotation.schema.json`. `listAnnotations` returns `{ annotations[] }`. An annotation is a live notification (`run.annotated`), never a run event. It MUST NOT enter the event log and MUST be excluded from fork, replay and diff. ##### Artifacts `getArtifact` answers `application/json` with an implementation-defined object or, when `Accept` prefers `application/a2a+json`, an A2A `Artifact` (`schemas/v2/artifact.schema.json`). A host SHOULD offer the latter. - A body served as `application/a2a+json` MUST validate against that schema, with `artifactId` equal to the path's. - A `url` Part in it MUST NOT resolve beyond the caller's `artifacts:read` authorization. ##### Eval summary `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, and `404` when the run is not an eval run. The summary MUST be free of task output, rubric prose and credentials. #### Conversation and residency capabilities ##### `conversationPrimitive` `conversationPrimitive` carries no payload: its presence is the claim ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html) §2). A host that does not advertise it MUST refuse a workflow whose `nodes[].typeId` references `core.conversationGate` — at registration or at run creation — with `422 capability_required`, naming the family in `details.requiredCapability`. A conversation turn MAY carry `parts`: a non-empty array of A2A `Part` objects (`schemas/v2/part.schema.json`) that marks the turn A2A-shaped. A producer SHOULD emit it and keep `content` readable by consumers that predate it. A turn without `parts` stays valid on emission, replay and fork. ##### `dataResidency` A host advertising `dataResidency` MUST honor-or-reject, and MUST NOT silently accept-and-ignore: - accept a `residency` constraint naming a region in `dataResidency.regions`; - refuse one it does not advertise with `residency_unavailable`. A host that does not advertise `dataResidency` MAY ignore or reject a `residency` constraint, but MUST NOT claim to honor it. #### Dead letters A host advertising `deadLetter` MUST, when a run or node exhausts its retry policy: - route it to the dead-letter sink and emit `run.dead-lettered`, whose `reason` is redaction-safe and which carries no credential or payload material; - keep the failed run fork-eligible for `retentionDays`, and not purge it sooner; - purge it after `retentionDays`, after which a fork fails as for an unknown run. Queue messages ([host-services.md](https://openwop.dev/spec/v2/core/host-services.html) §`queueBus`) and webhook deliveries ([webhooks.md](https://openwop.dev/spec/v2/core/webhooks.html) §Dead letters) have sinks of their own. #### During the v1 overlap A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per [persistence.md](https://openwop.dev/spec/v2/core/persistence.html) §"Runs pinned to v1". *Sources: RFCs 0053, 0058, 0084, 0170, 0171, 0176, 0182, 0228, 0229, 0231.* ### Security Defaults Source: https://openwop.dev/spec/v2/core/security-defaults.html > **Status: Stable.** > **Normative home:** `sandbox`, `compensation`, `purposePropagation`, `auditLogIntegrity`. #### Why this exists In v2 a security behavior is not an opt-in flag. This document is the obligation table: which surface binds which behavior, the invariant, and the witness. #### The rule A security-load-bearing behavior is an obligation of the surface that needs it. Advertising the surface binds the behavior; no discovery field gates it. - Every obligation in `core/` MUST name a surface, an invariant, and a witness class other than `unwitnessable`. A row that cannot is not in `core/`. - A host MUST NOT advertise a surface whose obligation it has relaxed. #### The obligation table | Surface advertised | Obligation | Witness | Invariant | | --- | --- | --- | --- | | any lane in `auth.lanes[]` | the lane obligations (§Auth lanes) | unaided or seam-gated per lane | `sender-constraint-no-bearer-downgrade`; per-lane rows | | both `saml` and `scim` lanes | the leaver contract (mandatory) | seam-gated | `subject-link-leaver-deny`, `subject-link-mandatory-when-both-advertised` | | `replay` (any mode) | side-effect suppression (§Replay suppression) | witnessable-gated via the effect-seam manifest | `replay-fanout-no-refire`; effect-seam rows registered at Accepted | | `webhooks` | durable delivery (§Webhook durability) | witnessable-gated (`webhook-signed-delivery` + dead-letter leg) | registered at Accepted | | `interrupt` with `approversList` or `refKinds` | approver enforcement (§Approver enforcement) | witnessable-gated | registered at Accepted | | `packs` (pack execution) | isolation (§Sandbox isolation) | witnessable-gated (eight `sandbox-*` scenarios) | `node-pack-sandbox-*` | | `compensation` | plan, attempt and inverse-action obligations (§Compensation) | witnessable-gated (reads) + seam-gated (operator actions) | `compensation-replay-no-refire`, `compensation-effect-id-retry-stable` | | `idempotency` | Layer-2 effect identity (§Layer-2 effect identity) | witnessable-gated (fixture provider) | `logical-effect-id-retry-stable` | | an `oauth2` or `oidc` lane | protected-resource metadata and challenges ([identity.md §2.5](https://openwop.dev/spec/v2/core/identity.html)) | witnessable-gated | `auth-challenge-no-oracle` | | any outbound request | no inbound credential on an onward hop (§Onward hops) | seam-gated | `inbound-credential-no-passthrough` | | `auditLogIntegrity` | a chained, checkpointed, verifiable audit log (§Audit-log integrity) | witnessable-gated | `audit-checkpoint-signed-over-root` | ##### Auth lanes The obligations are [identity.md §2](https://openwop.dev/spec/v2/core/identity.html) and bind on advertisement: - the verify → bind → audience → resolve → fail-closed pipeline; - a named trust root as `subject.issuer`; - revocation for the lane; - the advertised `minimumAssurance` floor, with `mtls.required` becoming `key-bound`; - lane-scoped delegation proof. ##### Replay suppression Side-effect suppression with `recorded-outcome` semantics is the only conforming behavior; `none` is not a value. Stated in replay.md §Suppression and §"The effect-seam manifest"; the `replay-side-effect-suppression` scenario witnesses it. ##### Webhook durability Durable delivery means retries per the advertised policy with backoff, dead-letter on exhaustion, and at-least-once delivery; best-effort is not a conforming delivery mode. Stated in webhooks.md §Durability; the `webhook-durable-delivery` scenario witnesses it. ##### Approver enforcement A resolver not in the list, group, or role MUST be refused; `refKinds[]` stays a facet. Stated in interrupt.md §"Approver enforcement"; the `approver-enforced` scenario witnesses it. ##### Sandbox isolation A host that executes third-party packs: - MUST enforce the eight `node-pack-sandbox-*` invariants of `SECURITY/invariants.yaml`: `no-process`, `network-gated`, `fs-gated`, `no-env`, `timeout`, `memory-cap`, `isolated-context`, `no-eval`; - MUST advertise `sandbox.isolationModel ∈ wasm | process | container | vm` (`spec/v2/facets/sandbox.schema.json`). `node:vm` is not a value. `sandbox.isolationModel` names the mechanism and never relaxes the property. A host that cannot isolate MUST NOT execute third-party packs; it MAY register and validate them. The `no-eval` row stays reference-impl in `ext/sandbox-runtime-notes`. The `pack-isolation` scenario drives the eight legs. The remaining `sandbox` facets name the bound each invariant already carries: `allowedHostCalls` is the host-call allowlist `network-gated` and `fs-gated` enforce, `memoryLimitBytes` is the ceiling of `memory-cap`, and `wallClockLimitMs` is the ceiling of `timeout`. An advertised bound MUST be enforced; none of the three gates the invariant, which binds whether or not the facet is advertised. ##### Compensation A host that advertises `compensation` MUST serve `GET /runs/{runId}/compensation` (`schemas/v2/compensation-projection.schema.json`), keyed on the node and attempt the operator family uses. The read projection and the operator action family are the canonical wire. A host that does not advertise `compensation` has no obligation. The facets bind the policy shape (`schemas/v2/compensation-policy.schema.json`): - `compensation.orderingModels` MUST list `reverse-completion` and MAY add `dependency-graph`; a policy naming a model outside it MUST be refused at registration. - `compensation.profileVersion` participates in the inverse-action identity, so a policy naming a different one MUST be refused. - `compensation.manualIntervention` is the `manual` status — a host advertising it records the unwind rather than abandoning it. ##### Layer-2 effect identity Layer-2 effect identity is keyed on business identity; the activity recipe is the fallback. A host that advertises `idempotency` MUST serve `GET /runs/{runId}/effects`; the keying, provider-key, retention and projection rules are idempotency.md §"Layer 2: effect identity". ##### Onward hops A host MUST NOT attach a credential it received inbound to any outbound request: A2A, MCP, webhook, callback, `httpClient` or connector. Inbound credentials are an `Authorization`, `Cookie` or `Proxy-Authorization` value, a DPoP proof, an interrupt token, a peer's bearer, or credential material carried in a body. Outbound authentication uses only credentials the host holds for that destination (oauth.md). A verified delegation chain is not a passthrough: it carries provenance, never the inbound credential ([identity.md §2.4](https://openwop.dev/spec/v2/core/identity.html)). One credential is not inbound in this sense: an A2A push-config credential (`authentication.credentials`, `token`) is one the client gave the host for its registered push URL ([interop.md §"A2A push delivery"](https://openwop.dev/spec/v2/core/interop.html)). A host: - MAY attach it only to a push delivery to that URL's origin; - MUST NOT attach it after a redirect or to any other request; - MUST discard it when the config is deleted or the task's final push is attempted; - MUST hold it by reference outside the event log, run state, debug bundle and every response. A host advertising `purposePropagation` MUST re-emit a `permittedPurposes` label it received (A2A `metadata.openwop.permittedPurposes`, `TriggerEvent.permittedPurposes`) on every onward hop of the same data, narrowing and never widening, and MUST treat `[]` as no onward use. `purposePropagation.propagatesOnward` is `false` only on a host with no onward hop. The family advertises propagation, not enforcement. ##### Audit-log integrity A host advertising `auditLogIntegrity` MUST keep an audit log a privileged insider cannot silently rewrite. It: - MUST keep the log append-only, each entry carrying `prevHash`, the lowercase-hex SHA-256 of the prior entry's canonical JSON ([conformance.md §Canonical JSON](https://openwop.dev/spec/v2/core/conformance.html)), `null` for the first; - MUST sign a checkpoint anchoring at most `checkpointIntervalEntries` entries, and anchor an entry within `checkpointIntervalSeconds` of its append. The range, leaves, root and signature are [RFC 0218 §A](https://github.com/openwop/openwop/blob/main/RFCS/0218-audit-checkpoint-preimage.md); the signature is Ed25519 (`checkpointSignatureAlgorithm`) under `checkpointPublicKey`, a key used for no other surface; - MUST serve `GET /audit/verify` (scope `audit:read`), answering `schemas/v2/audit-verify-result.schema.json` with every checkpoint whose `atSequence` is in the range, ascending, and the anomalies of [RFC 0218 §C](https://github.com/openwop/openwop/blob/main/RFCS/0218-audit-checkpoint-preimage.md). A host that does not advertise the family MAY omit the operation. The `audit-log-integrity` and `audit-checkpoint-signature` scenarios witness the verify body, each signature, and the cadence. The root is not witnessable from outside, because entries are not on the wire; tamper detection is a host-internal test. #### Relaxations A relaxation, where one is legitimate — a development deployment, a single-tenant appliance — is an operator setting, never a discovery field. Every relaxation a host runs under MUST be recorded in its certification bundle as `host.relaxations[]` (`schemas/v2/certification-bundle.schema.json`): `{ obligation, durability, reason }`, `durability ∈ session | deployment | persisted`. | Durability | Meaning | | --- | --- | | `session` | Lost on restart. | | `deployment` | Set at deploy time. | | `persisted` | Survives restarts and is auditable. | A bundle that records a relaxation MUST NOT certify the profile the relaxed obligation belongs to; the `relaxation-recorded` scenario verifies it unaided (conformance.md). #### Three dispositions Every security obligation in `core/` is exactly one of: | Disposition | Where | Requirement | | --- | --- | --- | | core obligation with a declared witness | this table | MUST name surface, invariant, witness. | | extension | `spec/v2/ext/` | MUST declare a witness class and both maturity axes. | | removed | — | No text survives. | Operation ids in the declaration file are canonical, and aliases are migration register rows. The provider semantic-option registry is `spec/v2/ext/provider-idempotency/registry.json`; provider qualification uses a fixture that rejects a changed idempotency key. #### Threat models | Artifact | Requirement | | --- | --- | | `SECURITY/threat-model-replay.md` §6 Residual risks | MUST record branch re-fires, seams outside the manifest, and the manifest as a self-declaration. | | `SECURITY/threat-model-replay.md` §7 Verification, §8 References | MUST name the manifest scenario and `fork-a-v1-run`. | | `SECURITY/threat-model-interop.md` | MUST cover downgrade, identity, and cross-tenant risks in protocol composition. | A threat model missing a sibling section fails the template gate. #### Migration Rows `C6.1`–`C6.9` are `spec/v1/migrations.json` entries. *Sources: RFCs 0150, 0163, 0164, 0170, 0173, 0214, 0218, 0224.* ### Storage Source: https://openwop.dev/spec/v2/core/storage.html > **Status: Stable.** > **Normative home:** `fs`, `kvStorage`, `tableStorage`, `sql`, `nosql`, `vectorStore`, `searchIndex`, `blobStorage`, `cache`. #### Why this exists A node pack keeps files, records and indexes in storage services the host provides, reached through `ctx` ([host-services.md](https://openwop.dev/spec/v2/core/host-services.html)). Each family below is a contract a host takes on by advertising it. #### Operations A host advertising a family MUST expose each of its operations to pack code, returning at least the fields shown; it MAY return more. An operation marked † is required only when its facet is `true`: `kvStorage.atomicIncrement`, `kvStorage.compareAndSwap`, `sql.transactions` or `blobStorage.presignSupported`. - **`fs`** (`ctx.fs`): `read(path) → bytes, contentType?` · `write(path, bytes, contentType?) → path, sizeBytes` · `delete(path) → deleted` · `stat(path) → sizeBytes, modifiedAt, contentType?` · `list(prefix?, cursor?, limit?) → entries[path, sizeBytes], nextCursor?` - **`kvStorage`** (`ctx.storage.kv`): `get(key) → value?, expiresAt?` · `put(key, value, ttlSeconds?) → ok` · `delete(key) → deleted` · `list(prefix?, cursor?, limit?) → entries[key], nextCursor?` · † `atomicIncrement(key, delta?) → value` · † `compareAndSwap(key, expectedValue, newValue) → swapped` - **`tableStorage`** (`ctx.storage.table`): `createTable(name, schema) → ok` · `insert(table, row) → rowId` · `get(table, rowId) → row?` · `query(table, filter?, cursor?, limit?) → rows, nextCursor?` · `update(table, rowId, patch) → ok` · `delete(table, rowId) → deleted` - **`sql`** (`ctx.db.sql`): `query(datasourceId, sql, params) → rows, rowCount` · `execute(datasourceId, sql, params) → rowsAffected` · † `transaction(datasourceId, operations[sql, params]) → committed` - **`nosql`** (`ctx.db.nosql`): `insert(datasourceId, collection, doc) → id` · `get(…, id) → doc?` · `query(…, filter, cursor?, limit?) → docs, nextCursor?` · `update(…, id, patch) → ok` · `delete(…, id) → deleted` - **`vectorStore`** (`ctx.db.vector`): `upsert(collection, vectors[id, embedding, metadata?]) → upserted` · `query(collection, embedding, k, filter?) → matches[id, score, metadata?]` · `delete(collection, ids) → deleted` - **`searchIndex`** (`ctx.db.search`): `index(index, docs[id, fields]) → indexed` · `query(index, q, k?, filter?) → hits[id, score, fields?]` · `delete(index, ids) → deleted` - **`blobStorage`** (`ctx.storage.blob`): `put(bucket, key, bytes, contentType?) → url, sizeBytes` · `get(bucket, key) → bytes, contentType?` · `delete(bucket, key) → deleted` · `list(bucket, prefix?, cursor?) → entries[key, sizeBytes], nextCursor?` · † `presign(bucket, key, expiresInSeconds, method) → url, expiresAt` - **`cache`** (`ctx.storage.cache`): `get(key) → value?, expiresAt?` · `put(key, value, ttlSeconds) → ok` · `delete(key) → deleted` `presign` takes `method` `GET` or `PUT`, and `…` in a `nosql` operation repeats `datasourceId, collection`. #### Shared rules - **Tenant isolation.** A read for one tenant MUST NOT return data another tenant wrote, even under an identical key or name: `kvStorage` `get` and `list`, `tableStorage` `get` and `query`, `vectorStore` and `searchIndex` `query`, `blobStorage` and `cache` `get`. - **Datasources.** `sql` and `nosql` datasources are scoped per tenant; access to another tenant's datasource MUST be refused. - **Backend-invariant.** The operation shapes of `sql` and `nosql` MUST NOT vary with the advertised `drivers`, nor those of `vectorStore` and `searchIndex` with the advertised `backends`. A host MAY back `sql` with any driver it advertises. - **Size limits.** A key over `kvStorage.maxKeyBytes`, and a write over `fs.maxFileSizeBytes`, `kvStorage.maxValueBytes`, `blobStorage.maxObjectBytes` or `cache.maxValueBytes`, MUST be refused. A `tableStorage` insert MUST be refused once `maxRowsPerTable` is reached. - **Expiry.** `kvStorage` and `cache` MUST honour an entry's expiry, as a read sees it, with at most one second of drift. `maxTtlSeconds` caps `ttlSeconds` for each. A family's advertisement also names its targets: `sql.datasources`, `nosql.datasources`, `vectorStore.collections`, `searchIndex.indexes` and `blobStorage.buckets`. `tableStorage` advertises `maxColumnsPerRow`, `indexable` and `fullTextSearch` as limits and features, with no further rule. A refused call carries `not_found`, `forbidden`, `validation_error`, or `storage_limit_exceeded` for a limit or quota ([errors.md](https://openwop.dev/spec/v2/core/errors.html) §Host-service refusals). A sandbox escape is `forbidden` with `details.reason: path-outside-sandbox`. #### `fs` - Every `path` MUST be normalized and resolved relative to `fs.sandboxRoot`. - A path that escapes the root, whether absolute, through `..` segments or through a symlink, MUST be refused. The host MUST NOT follow such a link partially. - A permission denial MUST fail the call, never succeed silently or fall through. - A read of a file over `maxFileSizeBytes` MAY fail rather than stream. - The `image` (with its `formats`), `pdf` and `transport` (`ftp`, `sftp`, `ssh`) sub-surfaces are optional, and gate the pack delegates that use them. #### `kvStorage` - When `atomicIncrement` is `true`, increments MUST be atomic across concurrent callers. - When `compareAndSwap` is `true`, a swap MUST be atomic, with no read-modify-write race. A stale `expectedValue` returns `swapped: false` without mutation. #### `tableStorage` - Rows MUST conform to the table's declared schema: an insert or update whose column types diverge from it MUST be refused. - `query` MUST support cursor pagination, and `nextCursor` MUST be opaque and stable across calls. #### `sql` and `nosql` - `sql` MUST be treated as a parametric template: bound values MUST flow through `params`, never through string interpolation. A pack MUST NOT concatenate user input into `sql`, and a host SHOULD verify parameter binding before execution. - When `sql.transactions` is `true`, a partial failure inside `transaction` MUST roll back the whole batch, which resolves `committed: false`. - `nosql` filter operators MUST NOT permit injection. Server-side script evaluation, such as MongoDB `$where`, MUST be refused unless an explicit allowlist is configured. #### `vectorStore` and `searchIndex` - `vectorStore`: an `upsert` followed by a `query` with the same embedding MUST return the inserted ids in the top `k` matches when `k` is at least the number inserted. - `searchIndex`: an `index` followed by a `query` with a substring of an indexed field MUST return the indexed id with `score` above 0. #### `blobStorage` Presigned URLs MUST expire at the advertised TTL. A presigned request after expiry MUST fail at the storage layer, not after an authorization skip. *Sources: RFCs 0014, 0015, 0016, 0018, 0019, 0228.* ### Tool catalog Source: https://openwop.dev/spec/v2/core/tool-catalog.html > **Status: Stable.** > **Normative home:** `toolCatalog`. #### Why this exists A host's tool catalog lists the tools a caller may invoke, with a projection onto MCP `ToolAnnotations`. The shapes are `schemas/v2/tool-descriptor.schema.json` and `schemas/v2/compact-tool-descriptor.schema.json`. #### The catalog A host advertising `toolCatalog` MUST serve `GET /tools` (a `ToolDescriptor[]`) and `GET /tools/{toolId}`, both read-only. - The list MUST hold only tools the caller may invoke in its tenant. - An unknown or unauthorized `toolId` MUST return `404`. - `toolCatalog.sources` names the sources projected; a consumer MUST tolerate any subset. - A host SHOULD return tools sorted by `toolId`, so an unchanged catalog reads identically. #### The descriptor - `toolId` MUST be unique in the catalog and stable for a host version. - `safetyTier: "exec"` MUST carry `source: "host-extension"`. - A descriptor MUST NOT carry credential material. - The host MUST assign `safetyTier`, `replayPolicy` and `egress` itself, and MUST NOT copy them from an MCP server's `annotations`, which are untrusted. - A `source: "mcp"` tool the host has not classified MUST be `safetyTier: "write"`. `annotations`, when present, MUST carry all four MCP hints, derived from those fields and not from MCP defaults (upstream, `destructiveHint` and `openWorldHint` default to `true`): | Hint | True iff | | --- | --- | | `readOnlyHint` | `safetyTier` is `pure` or `read` | | `destructiveHint` | `safetyTier` is `write` or `exec` | | `idempotentHint` | `replayPolicy` is `deterministic` or `idempotent` | | `openWorldHint` | `egress` is not `none` | #### Views and sessions A host advertising `toolCatalog.compactView` MUST answer `?view=compact` on both endpoints with `CompactToolDescriptor`s (the list as `{ tools: [...] }`) carrying the standard view's `toolId` set. - A compact `inputSchema` MUST NOT use `$ref`, `oneOf`, `allOf`, `anyOf`, `not`, `patternProperties` or `dependentSchemas` at any depth. - Any other `view`, or a host not advertising it, yields the standard view. A host advertising `toolCatalog.sessionLifecycle` MAY bracket calls with content-free `tool.session.opened` and `tool.session.closed`; a consumer MUST tolerate their absence. *Sources: RFCs 0078, 0112, 0204.* ### Versioning and Release Source: https://openwop.dev/spec/v2/core/versioning.html > **Status: Stable.** #### Why this exists How a v2 host selects a major, what each version axis means, and what a release is. #### 1. Major negotiation ##### 1.1 Advertisement A v2 host MUST advertise two root fields, both REQUIRED in `schemas/v2/capabilities.schema.json` ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)): - `protocolVersions[]` — every `<major>.<minor>` it serves, each matching `^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$`. Through the overlap that is `["1.<n>", "2.<m>"]`; after v1 end-of-support, `["2.<m>"]`. - `preferredVersion` — MUST be a member of `protocolVersions[]`. Through the overlap it MUST name a 1.x member ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html) §1; §1.3). On a host serving a single major, it MUST equal `protocolVersion`. A host that drops v1 advertises a `2.x` `preferredVersion`; its header-less representation becomes the closed v2 root. A v2 consumer reads `preferredVersion` as the header-less default. When it is absent on a v1 document, the default is `max(protocolVersions[])`, else `protocolVersion`. ##### 1.2 Paths - v1 operations keep their `/v1/…` path keys unchanged through the overlap. - v2 operations are unversioned path keys on a bare origin (`servers[].url = https://{host}`): `/runs`, `/runs/{runId}`, `/.well-known/openwop`. There is no `/v2/` path space. - An unversioned path is the v2 surface; v1's rule that it answers `400` does not apply. Advertising a major is a claim about the whole **path space**, not about `/.well-known/openwop` alone (§1.3 selects that resource's representation): - A host that advertises a major in `protocolVersions[]` MUST reach, under that major, every operation named in `spec/v2/path-manifest.json` that it serves under the other. - If `/v1/<op>` answers and the unversioned `/<op>` returns `404` under the advertised major, the host MUST NOT advertise that major until the surface is reachable. - Seam and proprietary paths are not manifest operations and need no per-major twin. `spec/v2/path-manifest.json` (generated) lists operations (`method`, `path`, `operationId`) and channels (`name`, `address`) on a bare origin. Every path in it is unversioned; the pairing above compares each row with its `/v1` twin, derived by prefixing. OpenAPI (`api/v2/openapi.yaml`), AsyncAPI (`api/v2/asyncapi.yaml`) and any kept proto MUST resolve to identical absolute paths for the shared event stream (`scripts/check-path-parity.mjs`). Seams are in [conformance.md](https://openwop.dev/spec/v2/core/conformance.html). ##### 1.3 The request header A request on an unversioned path MAY carry `OpenWOP-Version: <major>` or `OpenWOP-Version: <major>.<minor>`. `2` and `2.0` select the same major, and a host MUST accept both. Only the major selects: a minor in the header is informational. What pins a minor is `minClientVersion` plus the additive rules (`COMPATIBILITY.md` §2.4). | Condition | Host behavior | | --- | --- | | Header names a major in `protocolVersions[]` | MUST serve that major | | Header names a major not in `protocolVersions[]` | MUST answer `406` `protocol_version_unsupported`, with `details.protocolVersions[]` echoing the list | | Header absent on `/.well-known/openwop` | MUST serve `preferredVersion`'s major | | Header absent on any other unversioned path | MUST serve major 2: the path is the v2 surface (§1.2) | | `/v1/…` path with `OpenWOP-Version` other than `1` | MUST answer `400` `protocol_version_mismatch` | A request on a `/v1/…` path key MUST NOT carry `OpenWOP-Version` with a value other than `1`. `protocol_version_unsupported`, `protocol_version_mismatch` and `client_version_unsupported` (§1.5) are rows in `spec/v2/errors.json` ([errors.md](https://openwop.dev/spec/v2/core/errors.html)). ##### 1.4 The response header Every protocol response MUST carry `OpenWOP-Version: <major>.<minor>` naming the contract that produced it, `/v1/` responses included (REQUIRED in v2). Reporting a version other than the one used is a silent downgrade and non-conformant (scenario `dual-stack-negotiation`). A *protocol response* is one produced by an operation named in `spec/v2/path-manifest.json` (or its `/v1/` twin through the overlap). A shell, hosting fallback, conformance seam or proprietary route has no protocol version to name. On a manifest-named path: - A non-protocol response MUST NOT carry `OpenWOP-Version` and MUST NOT be `application/json`. - A reader, a cache or the suite MUST NOT count a response without the header, or with a `text/html` body, as reaching the operation (`reachedUnderMajor2`). A vendor path (§5) is not a shared name and is unconstrained. ###### Content negotiation on a shared name A host MAY serve a protocol operation and a page under one unversioned name, selecting on `Accept`, iff: 1. A request identifying as a protocol client — `OpenWOP-Version` present, **or** an `Accept` admitting `application/json` without preferring `text/html` (absent and `*/*` included) — MUST get the protocol response for the applicable major (§1.3) with `OpenWOP-Version`. Only an explicit `text/html` preference selects the page. 2. The page obeys the rules above. 3. The response carries `Vary: Accept, OpenWOP-Version`. Otherwise the page MUST move off the shared name. ##### 1.5 Client precedence and `minClientVersion` When both majors are advertised, a v2 client MUST select the highest major it implements that the host lists. A v1 client (no header, `/v1/` paths) is unaffected. A client announces the protocol version it implements in the `OpenWOP-Client-Version` request header, as `<major>.<minor>` or `<major>.<minor>.<patch>` (non-negative integers, no leading zeros). The value is the corpus release the client is built against (§4), not an SDK or product version. - A client SHOULD send it on every request under major 2. - The header is optional on every operation and never selects a major (§1.3). A host MUST NOT choose a major or a representation from it. - A host MUST NOT refuse a request because it omits the header or sends a malformed value. - A value outside the grammar MUST be treated as absent, and MUST NOT produce a `400`. - The value is the client's claim. A host MUST NOT use it as an authentication or authorization input. `minClientVersion` (axis 15) is optional. When a host advertises it: - It MUST use the axis-1 grammar. - A client is below it when the client's major and minor, compared as integers, are less than the floor's. The patch never decides. - A host MAY refuse a client below it. A refusal MUST be `426` `client_version_unsupported`, and MAY carry `details.minClientVersion` naming the floor ([errors.md](https://openwop.dev/spec/v2/core/errors.html)). - The discovery document is not exempt: a client refused there learns the floor from `details.minClientVersion`. A host MUST NOT answer `426` `client_version_unsupported` to a request that does not carry a well-formed `OpenWOP-Client-Version` below its advertised `minClientVersion`. This header rule binds requests served under major 2. A request served under major 1 follows that major's frozen text. Open gap: RFC 9110 §15.5.22 requires an `Upgrade` header on every `426`. This floor departs from it, and no `Upgrade` value is defined yet. #### 2. The 18 version axes Dispositions: - `unify` — one type and grammar, with a codemod. - `first-class` — own schema-enforced grammar and negotiation rule. - `retire` — absorbed into the capability record's `{status, since, until?}`. - `delete` — removed, with a register row. | # | Axis | Disposition | v2 grammar | Owner | | --- | --- | --- | --- | --- | | 1 | `protocolVersion` | first-class; `preferredVersion`'s twin for v1 readers through the overlap, removed after | `^(0\|[1-9][0-9]*)\.(0\|[1-9][0-9]*)$` | this document | | 2 | `protocolVersions[]` + `preferredVersion` | first-class, negotiation input | as #1 | this document | | 3 | `engineVersion` | unify: integer everywhere; codemod `openwop.codemod.engine-version-unify` | `integer, minimum 0` | this document | | 4 | `eventLogSchemaVersion` | first-class, the era key | integer; v2 writes `3` | `persistence.md` | | 5 | per-event `schemaVersion` | first-class; §0 growth rule | integer | `events.md` | | 6 | `schemaVersions` map | first-class; the map moves into the record's `kinds` seat | `propertyNames` = the envelope-kind grammar, values `integer, minimum 0`; not a closed enum — a vendor kind is host-published, never corpus-declared | `events.md` | | 7 | `version.pinned` | first-class; the v1-pinned-run disposition | integer min/max | `persistence.md` | | 8 | `contractProvenance` | delete | — | `capabilities.md` | | 9 | `minimumSuiteVersion` | retire into `spec/v2/declaration.json` | semver | `capabilities.md` | | 10 | `bundleVersion` | unify to one `const` family: certification v3 `"3"`, export `"2"`, debug `"2"` | string const | `conformance.md` | | 11 | A2A `versions[]` / `preferredVersion` | first-class facet of `a2a` | `^[0-9]+\.[0-9]+$` | `interop.md` | | 12 | MCP `revisions[]` / `preferredVersion` | first-class facet of `mcp` | date | `interop.md` | | 13 | `multiAgent.executionModel.version` | first-class | integer with a schema `maximum` the suite reads | `events.md` | | 14 | OpenAPI / AsyncAPI `info.version` | generated from the corpus tag | semver | this document | | 15 | `minClientVersion` | first-class (§1.5) | as #1 | this document | | 16 | channel `schemaVersion` / `compatibleWith` | first-class | integer / range | `events.md` | | 17 | webhook signature scheme | retire into `deprecations.json` | — | `webhooks.md` | | 18 | pack `engines.openwop` + `registryVersion` | first-class with the absent-ceiling rule | semver range / semver | `packs.md` | - One grammar covers protocol, envelope-kind and pack axes wherever a version is `<major>.<minor>` (#1, #2, #11, #15). - `typeId@<semver>` is a pack axis (`packs.md`); the `2` in `typeId@2.0.0` never means `OpenWOP-Version: 2`. ##### 2.1 `engineVersion` (axis 3) - `engineVersion` MUST be an integer (`minimum 0`) at the discovery root and on every per-event carrier. - A persisted v1 run document that carries the string form is legacy-stamped: the reader MUST normalise it to an integer and MUST NOT rewrite the stored document. - The codemod `openwop.codemod.engine-version-unify` MUST refuse any value not matching `^(0|[1-9][0-9]*)$`. ##### 2.2 `eventLogSchemaVersion` (axis 4) `eventLogSchemaVersion` is the era key; the schema floor is `minimum 2`. Its stamping, absent-⇒-`2`, discovery and reader rules are in [persistence.md](https://openwop.dev/spec/v2/core/persistence.html) §"The era key". #### 3. Where v2 lives - `spec/v2/core/` and `spec/v2/ext/<key>/` hold the prose. - `schemas/v2/` holds every v2 schema, with `$id` under `https://openwop.dev/spec/v2/`; the site publishes them at `/spec/v2/`. - The flat `schemas/` tree (v1 `$id`s) is read-only. v1 `$id` values are immutable identifiers: a domain move is answered by a redirect, never a rewrite. - AsyncAPI `servers.production.pathname` is empty; every channel address carries its own path, as OpenAPI path keys do. #### 4. One release identity The corpus tag `v2.<minor>.<patch>` (release candidates `v2.0.0-rc.<n>`) is the only release event; suite, SDKs, registry and site derive from it. `spec/v2/release.json` carries the next tag as `version`, and only the release PR that cuts the tag bumps it. - Every human-surface version (README banner, `docs/PROTOCOL-STATUS.md`, OpenAPI and AsyncAPI `info.version`, `conformance/package.json`) MUST be generated from it and checked with `--check` in the merge gate. - The published tarball digest MUST equal the tree's as a release precondition. - The identity and advertised-versions checks keep their three-outcome discipline ([conformance.md](https://openwop.dev/spec/v2/core/conformance.html)). - A consumer that vendors any file from `schemas/`, `api/` or `spec/` MUST pin to a published tag, record it, and refuse a sync from any other ref. - A v1.x consumer MUST NOT vendor `schemas/v2/`. #### 5. The overlap Through the overlap a host: - MUST advertise both majors (§1.1); - MUST emit `OpenWOP-Version` on every response (§1.4); - MUST serve `/.well-known/openwop` as one resource whose representation the request header selects ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)). A run minted under major 1 and read under major 2 MUST use the tenant-bound projection `<tenantId>/<v1-id>` ([identity.md](https://openwop.dev/spec/v2/core/identity.html) §5). A host MUST NOT return a bare v1 id in a major-2 response. ##### Retirement The overlap ends at v1 end-of-support ([overview.md](https://openwop.dev/spec/v2/core/overview.html)). From then a host MAY drop the `1.<n>` member from `protocolVersions[]`; when it does, every alias carrying the `v1-end-of-support` trigger goes with it. - **Retirement is atomic.** Dropping v1 retires the whole `/v1` path space at once (§1.1). - **Retirement changes every header-less request's default contract**, from major 1 to major 2. Before retirement, a host MUST check for collisions between manifest top-level path segments and non-protocol unversioned routes, and MUST move each colliding route or apply §1.4 content negotiation. ##### Host-proprietary paths: `/host/<org>/…` Every vendor namespace — capability records ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html) §3.2), error codes, event types, pack properties — is keyed to an org registered in `spec/v2/declaration.json`; paths join that pattern. - A host MAY serve operations the manifest does not name under `/host/<org>/…` for its registered org. Such a path has no major, is served regardless of `OpenWOP-Version`, is never a protocol operation, is never measured, and is outside §1.4. - An org MUST NOT be named after a manifest segment under `/host/` (`reservedOrgs`). - A host SHOULD advertise the mount under `extensions.<org>.<name>`. - A `/v1/host/<org>/…` twin MAY ride the overlap; it retires atomically with `/v1`. - After retirement a host MUST NOT serve an operation at the twin. It MAY answer a `GET` or `HEAD` there with `308`, no body, and `Location` set to the same `/host/<org>/…` path. #### 6. Migration rows Rows `C5.1`–`C5.9` are `spec/v1/migrations.json` entries (`C5.2` is owned by `events.md`). The persisted-data disposition for each is `not-persisted`, except `C5.1` (legacy-stamped) and `C5.7` (never-upgraded). *Sources: RFCs 0167, 0168, 0172, 0176, 0179, 0181, 0193, 0219.* ### Webhooks Source: https://openwop.dev/spec/v2/core/webhooks.html > **Status: Stable.** > **Normative home:** `webhooks`, `triggerBridge`. #### Why this exists A client registers a URL and an event filter once; the host POSTs matching events, signed, as they happen. Durable delivery binds with the surface — a signed event that may be dropped is not a delivery contract. #### Surfaces A host that advertises `webhooks` ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)) serves `registerWebhook` (`POST /webhooks`) and `unregisterWebhook` (`DELETE /webhooks/{webhookId}`) from `api/v2/openapi.yaml`. The facet (`spec/v2/facets/webhooks.schema.json`) carries `signatureAlgorithms[]`, which MUST list `"v1"`. | Operation | Request | Response | | --- | --- | --- | | `registerWebhook` | `{ url, events[], secret?, tags?, signatureAlgorithms? }` | `201 { webhookId, secret? }` | | `unregisterWebhook` | path `webhookId` | `204`; `404` when unknown; `403` when the caller is outside the subscription's tenant | On `registerWebhook`, `url` MUST be `https://` and `events[]` MUST be non-empty v2 event type names ([events.md](https://openwop.dev/spec/v2/core/events.html)). When `registerWebhook` omits `secret`, the host MUST generate one and return it as `secret` in the `201`. That response is the only one that carries it. A supplied secret MUST NOT be echoed. A `204` from `unregisterWebhook` ends the subscription's deliveries, including retries already scheduled (§Durability). A subscription MUST receive only events from runs within its tenant scope; cross-tenant delivery is a protocol violation whatever the filter says (invariant `webhook-cross-tenant-isolation`). `tags` narrows delivery to runs whose options carry an overlapping tag. #### Delivery The delivery envelope is generated from the event's payload definition (events.md §Payloads). The body is `{ runId, workspaceId?, event }`, where `event` is the verbatim run event. - The body MUST validate against `schemas/v2/webhook-delivery.schema.json`. - `workspaceId` is present exactly when `RunSnapshot.owner.workspace` is ([identity.md §1](https://openwop.dev/spec/v2/core/identity.html)); a host MUST NOT substitute its tenant id for an absent workspace. - The envelope's `runId` MUST use the tenant-bound form of [identity.md §5](https://openwop.dev/spec/v2/core/identity.html), matching the nested event and every response representation, even though no versioned request exists at delivery time. ##### Headers | Header | Value | | --- | --- | | `OpenWOP-Webhook-Id` | the subscription id | | `OpenWOP-Event-Type` | the v2 event type | | `OpenWOP-Timestamp` | Unix seconds at signing | | `OpenWOP-Signature` | `sha256={hex}`, HMAC-SHA256 over the signed bytes | | `OpenWOP-Signature-Algorithm` | `v1` | A host MUST send all five on every delivery. The signed bytes are `{timestamp}.{rawBody}`, where `rawBody` is the exact bytes delivered. Scheme `v1` is HMAC-SHA256 with the subscription secret (`hs256`). ##### Verification A subscriber MUST verify before acting: 1. Reject a timestamp more than ±5 minutes from its clock. 2. Compute `HMAC-SHA256({timestamp}.{rawBody}, secret)`. 3. Compare in constant time. A subscriber MUST reject an unrecognized `OpenWOP-Signature-Algorithm` value, and SHOULD track `(OpenWOP-Webhook-Id, runId, sequence)` for at-least-once deduplication. A host MUST NOT log the secret. ##### Standard Webhooks `standard-webhooks-1` names the symmetric scheme of Standard Webhooks 1.0.0. Its in-header `v1,` token is that standard's, not the OpenWOP scheme id `v1`; neither is read as the other, and `OpenWOP-Signature-Algorithm` stays `v1`. **Opt-in.** A subscription carries the scheme only when `registerWebhook` sends `signatureAlgorithms` listing it and `v1`, with a `secret` of the form `whsec_<base64 of 24–64 bytes>`; otherwise, or when the host does not advertise the id, `400 validation_error`. The `201` echoes the applied list. Every other subscription is unchanged. **Endpoint verification.** Before answering `201`, a host MUST send the request of `schemas/v2/webhook-verification.schema.json` to `url` under §Egress, signed as below. Unless a `2xx` arrives within 10 s whose JSON `challenge` equals the one sent, it MUST refuse `400 webhook_endpoint_unverified`, persisting nothing. A host MUST NOT verify a subscription that did not opt in. **Delivery.** Each delivery to an opted-in subscription adds `webhook-id`, `webhook-timestamp` (equal to `OpenWOP-Timestamp`) and `webhook-signature`: space-separated `v1,<base64 HMAC-SHA256>` entries over `{webhook-id}.{webhook-timestamp}.{rawBody}`, keyed by the decoded secret. `webhook-id` matches `^[A-Za-z0-9_-]{16,128}$`, MUST be identical on every attempt of one `(webhookId, runId, sequence)` and MUST differ across them. **Rotation.** A host advertising `webhooks.secretRotation` serves `rotateWebhookSecret`. For `overlapSeconds` after a rotation, `webhook-signature` MUST carry one entry per secret and `OpenWOP-Signature` stays on the previous secret; afterwards only the new secret signs. ##### Dual emission through the overlap - A host advertising both majors MUST send, on every delivery, the `X-openwop-*` family alongside the `OpenWOP-*` family with identical values. This adds no signature scheme. - A v2 receiver MUST accept a delivery carrying only the `X-openwop-*` family under scheme `v1`, verifying the same bytes. - Per-subscription secrets are unchanged across the cut. Deliveries queued before the cut drain under their own retry policy with the payload they were serialized with ([persistence.md](https://openwop.dev/spec/v2/core/persistence.html)). - The `X-openwop-*` family is removed on its register date. #### Durability Durable delivery is an obligation of the `webhooks` surface ([security-defaults.md](https://openwop.dev/spec/v2/core/security-defaults.html)). A host MUST: - retry a failed attempt per its advertised `retryPolicy` (`maxAttempts`, `backoff ∈ none | fixed | exponential`, optional `maxElapsedMs`) with backoff between attempts, and when it advertises `maxElapsedMs`, dead-letter an exhausted delivery within that many milliseconds of the first attempt's start; - route a delivery whose retries are exhausted to the dead-letter sink (§Dead letters), rather than drop it; - deliver each matching event at least once; a receiver MAY observe the same event more than once; - not make the start of an attempt to one subscription wait for an attempt to a *different* subscription to finish (answered, failed, or timed out) (§Delivery isolation); - sustain that while at least **8** subscriptions have attempts outstanding that their receivers have not answered; - after `unregisterWebhook` answers `204`, not start any further attempt for that subscription, including attempts already scheduled for retry (invariant `webhook-unregister-stops-delivery`); - dead-letter a `payload_unprojectable` delivery (events.md §Era-2) on the first attempt, never retry it. Best-effort delivery is not a conforming mode. A `3xx` response is a delivery failure, retried under the same policy. `webhook-durable-delivery` witnesses it. ##### Delivery isolation One subscription's slow or dead receiver MUST NOT delay another subscription's deliveries (invariant `webhook-delivery-isolation`). The rule constrains when an attempt starts, not how soon after its event, and names no mechanism; a sequential loop over a batch does not meet it. A host MAY bound concurrent attempts beyond the floor of 8, and SHOULD NOT let one tenant's unanswered attempts occupy capacity another tenant's deliveries need. ##### Unregister An attempt whose request the host had begun sending before `unregisterWebhook` answered `204` MAY complete. The unregister does not oblige the host to route that subscription's undelivered events to the dead-letter sink: the exhaustion rule governs deliveries of a live subscription. ##### Dead letters This is the delivery sink, not the run sink of the `deadLetter` family ([runs.md](https://openwop.dev/spec/v2/core/runs.html) §Dead letters). - A host advertising `webhooks.deadLetter` MUST serve `GET /webhooks/{webhookId}/dead-letters`. - A record names a delivery and MUST NOT carry the delivered body, the delivery headers, or the subscription secret. - The read belongs to a live subscription: after `unregisterWebhook` answers `204`, `GET /webhooks/{webhookId}/dead-letters` for that id MUST answer `404 not_found`, exactly as for a same-tenant id the host never minted. The host MAY discard that subscription's records at the unregister. - `expiresAt` bounds a record's retention only while its subscription exists. #### Replay A `replay` fork's re-emitted history is never delivered ([replay.md](https://openwop.dev/spec/v2/core/replay.html) §Suppression); a `branch` fork's events are. #### Egress At registration a host MUST reject (`400 webhook_url_rejected`) non-`https://` URLs, RFC 1918 and loopback and link-local ranges, IPv6 ULA, cloud metadata hosts, and `localhost`. Address classification: - An IPv4-mapped IPv6 address (`::ffff:0:0/96`) MUST be judged by the IPv4 address it embeds, whatever its spelling. - An address embedding IPv4 in another standard translation form (IPv4-compatible `::/96`, NAT64 `64:ff9b::/96`, 6to4 `2002::/16`) SHOULD be judged the same way, and those prefixes SHOULD NOT be denied wholesale. - A host SHOULD refuse every destination the IANA special-purpose address registries mark not globally reachable. At delivery time a host MUST re-resolve the hostname, validate every resolved address against the same denied ranges plus its own denylist, connect to the validated address without re-resolving, and refuse to follow redirects (invariant `webhook-delivery-egress-revalidation`, reference-impl tier). These delivery-time rules bind an A2A push identically (interop.md §"A2A push delivery"). #### Inbound triggers A host advertising `triggerBridge` runs inbound work through subscriptions (`schemas/v2/trigger-subscription.schema.json`), and `subscriptionStates` lists the states it implements: - `active`; - `paused` — not delivering; a schedule skips ticks; - `failed`; - `dead-lettered` — terminal; its deliveries are dead-lettered (below), not held by the `deadLetter` run sink. On an `active` subscription the host: - with `dedup`, MUST answer a `dedupKey` repeated within retention (at least 24 hours) with the prior `runId`; - retries a failed delivery per `retryPolicy`, then dead-letters it without starting a run; - MUST set the delivery id as `causationId` on `run.started`. A host whose `triggerBridge.ingestion.inboundSigning` lists `standard-webhooks-1` also serves `webhook` subscriptions this way: - registration returns a `whsec_` `signingSecret` once; - a sender signs the raw body with the `webhook-*` headers of §"Standard Webhooks", and that signature, not an OpenWOP credential, authenticates `ingestUrl`; - under `required` verification a bad signature, or a timestamp more than 300 seconds off, fails the check (below); - `webhook-id` is the identity dedup keys on; - the ingest answers `202` (delivered, `runId`), `200` (duplicate, prior `runId`), `401 signature_invalid`, or `409 subscription_not_active`. A source in `triggerBridge.sources` MUST move through these states and emit `trigger.subscription-state-changed` and `trigger.delivery-attempted`. These events MUST NOT carry inbound content or credentials (`schemas/v2/run-event-payloads.schema.json`). A dead-lettered attempt or a state change belongs to no run; for it, emit means the host keeps a content-free record of that payload. A host advertising `triggerBridge.deadLetter` MUST serve `GET /trigger-subscriptions/{subscriptionId}/dead-letters`: - a record carries the dead-lettered attempt's payload fields and, if the dead-lettering changed the subscription's state, that state change; - a record MUST NOT carry the inbound body, headers, signature, or secret; - a delivery refused by a `required` check appears with `reason: "verification_failed"` and no state change; - a cursor minted for another subscription MUST be refused `400 validation_error`. With `triggerBridge.ingestion`, each `externalSources` entry MUST turn an external event into a `TriggerEvent` (`schemas/v2/trigger-event.schema.json`, whose rules bind) and start a run. The host: - MUST verify per `verification` before delivery. A failed `required` check starts no run and dead-letters only that delivery. A refused event MUST NOT change the subscription's state or emit `trigger.subscription-state-changed`; - returns a binding secret or URL once; `stream` and `change` bindings are empty; - MUST pass the event only as `ctx.triggerData`, never in an event, and replay it from cache (invariant `trigger-ingestion-content-redaction`); - MUST refuse private, link-local and loopback targets and cap the body on any ingestion fetch, and never hand the run a URL (invariant `trigger-ingestion-ssrf`); - SHOULD key `stream` by topic, partition and offset, `change` by table and changelog id; a key MUST survive broker redelivery. *Sources: RFCs 0053, 0083, 0099, 0127, 0165, 0171, 0173, 0176, 0188, 0196, 0201, 0215, 0217, 0230, 0232.* ### Workflow Chain Packs Source: https://openwop.dev/spec/v2/core/workflow-chain-packs.html > **Status: Stable.** > **Normative home:** `workflowChainPacks`. #### Why this exists A workflow-chain pack ships a reusable fragment a host expands into a concrete definition, and may compose other chains as co-registered children. The manifest is `schemas/v2/workflow-chain-pack-manifest.schema.json`; installation and signing follow [packs.md](https://openwop.dev/spec/v2/core/packs.html). #### Exact pins Every reference a chain makes to a node type or an external chain MUST pin an exact version per referenced `typeId` (`core.ai.callPrompt@1.0.0`). A host MUST refuse to register a chain whose reference carries a range or no version. #### Co-registered children When a parent chain expands a `subChainRef`, the child is registered as its own workflow. A host MUST: - **Identity** — register the child under a deterministic id, so two parents composing the same child share one registration. - **Reference count** — count each parent that references the child; deleting a parent decrements the count. - **Deletion** — delete the child only when its last parent is deleted. - **Ownership record** — persist the resolved child version in the parent's ownership record, so a re-instantiation or `:fork` reproduces the same child. The ownership record is a [persistence.md](https://openwop.dev/spec/v2/core/persistence.html) store. #### Parameter substitution `{{params.<name>}}` tokens are substituted at expansion time, when the author drops the tile. A persisted definition MUST NOT contain a `{{params.*}}` token, and a host MUST NOT defer substitution to dispatch time. #### Composition depth A host MUST bound sub-chain nesting by `workflowChainPacks.subChains.maxDepth` ([capabilities.md](https://openwop.dev/spec/v2/core/capabilities.html)), default 8. A composition that exceeds the depth, or that transitively composes itself, MUST fail closed with `sub_chain_cycle`; the depth check and the cycle check compose as one guard. #### Edge conditions Fragment edges carry the same `condition` and `triggerRule` shapes as a top-level definition (`schemas/v2/workflow-definition.schema.json`). `EdgeCondition.type` is one of `expression`, `equals`, `notEquals`, `contains`, `regex`, `truthy`, `falsy`; `truthy` and `falsy` take `left` and no `right`. A host MUST carry both fields through expansion verbatim and MUST honor them on expanded edges as on authored ones. [form-content-packs.md](https://openwop.dev/spec/v2/core/form-content-packs.html) reuses this operator set for field visibility. *Sources: RFCs 0133, 0177.* ## Extensions ### a2uiSurface extension Source: https://openwop.dev/spec/v2/ext/a2uiSurface/ > **Status: Draft.** The normative contract for the `ui.a2ui-surface` envelope kind at per-kind schema version 2, and for the deprecated `deltaTransport` facet. | Field | Value | | --- | --- | | **witness:** | `seam-gated` | | **technical:** | `experimental` | | **adoption:** | `none` | | **peer-dependency id** | `a2uiSurface` | | **advertised as** | `schemaVersions.kinds["ui.a2ui-surface"]: 2`; the deprecated facet only under `extensions.<org>.a2ui-surface` | | **declared facets** | `deltaTransport` (deprecated) | #### Versions `ui.a2ui-surface` has two per-kind schema versions, both in [`schemas/v2/envelopes/ui.a2ui-surface.schema.json`](../../../../schemas/v2/envelopes/ui.a2ui-surface.schema.json): | Version | Branch | Shape | | --- | --- | --- | | `1`, `0` or absent | `$defs/payloadV1` | the seven-component tree pinned by `catalogVersion: "0.9.1"` | | `2` | `$defs/payloadV2` | `{ reasoning?, version: "v0.9", catalogId, surfaceId, messages[1..64] }` | A version-2 payload is an ordered run of A2UI v0.9 server-to-client messages in the profile below. ##### Choosing the branch An engine MUST validate an envelope against exactly one branch: the one for the version that [`core/events.md`](https://openwop.dev/spec/v2/core/events.html) §"The envelope-kind catalog" selects (the emitted version, or the advertised one under `warn` below the floor). - It MUST NOT validate against the union. A version-2 envelope carrying a version-1 payload is invalid, and so is the reverse. - The root `anyOf` exists for tooling. A host that asks a model to emit version 2 hands the provider `$defs/payloadV2` alone. A host admits version 2 by advertising `schemaVersions.kinds["ui.a2ui-surface"]: 2`. The catalog rules apply unchanged: a consumer that knows only version 1 refuses version 2 with `unknown_schema_version` and fails closed. ##### Cross-field rules In a version-2 payload, `catalogId` is an enum of the host-pinned catalogs, today exactly `https://a2ui.org/specification/v0_9/catalogs/basic/catalog.json`. - Every message's `surfaceId` MUST equal the payload's `surfaceId`. - A `createSurface.catalogId` MUST equal the payload's `catalogId`. The schema cannot express either rule, so the engine checks both at admission and refuses a violation with `envelope_invalid`. #### The profile Every message that validates against `payloadV2.messages[]` MUST also validate against upstream A2UI v0.9 `server_to_client.json` with the basic catalog and `common_types.json` bound. The profile only removes; it never adds a property A2UI lacks. Conformance vendors the upstream files, pinned by SHA-256 (`conformance/fixtures/upstream/a2ui-v0.9/`). | Message | Profile | | --- | --- | | `createSurface` | `surfaceId`, `catalogId`, optional `theme` (`primaryColor`, `agentDisplayName` only) | | `updateComponents` | `surfaceId` and `components[1..512]` from the set below | | `updateDataModel` | as upstream: `surfaceId`, optional `path` (JSON Pointer), optional `value` | | `deleteSurface` | as upstream | Components are each closed and discriminated by a single-string-enum `component`: `Text`, `TextField`, `CheckBox`, `ChoicePicker`, `DateTimeInput`, `Button`, `Column`, `Row`, `Card`, `Divider`. - A dynamic value is a literal or a `{path}` data binding. - `Column` and `Row` take static `children` id lists only. | Excluded | Why | | --- | --- | | `Button.action.functionCall` (including `openUrl`) | client-side execution; `a2ui-action-confinement` | | `FunctionCall` values, except `checks[].condition` calling `required` | code execution on the renderer; `a2ui-surface-no-code-exec` | | `TextField.variant: "obscured"` | a surface MUST NOT solicit a secret; `a2ui-surface-no-secret-input` | | `Image`, `Video`, `AudioPlayer`, `createSurface.theme.iconUrl` | each fetches a URL; `a2ui-surface-no-network-egress` | | `createSurface.sendDataModel` | collected data returns only through actions | | `Icon` | renderer-defined vocabulary; deferred | | `List`, `Tabs`, `Modal`, `Slider`, data-model templates | outside the day-one profile | #### Actions and collected data A `Button.action` MUST be the A2UI server-event arm `{ event: { name, context? } }` with `name` ∈ {`resume`, `exchange`}: an interrupt resume or a conversation exchange. When a user fires an action, the consumer resolves `event.context` against the surface's data model and submits the result: - for `resume`, as the interrupt `resumeValue`; - for `exchange`, as the exchanged turn's data; - with no `context`, it submits the whole data model at `/`. It MUST NOT submit anywhere else. A host MUST NOT emit a surface whose evident purpose is credential collection. Collected values become a `resumeValue` in the event log, where SR-1 redaction is a backstop, not a licence. #### The fold A surface is identified by `(runId, surfaceId)`. Its state is the fold, in event `sequence` order, of the `messages` of every version-2 `ui.a2ui-surface` envelope the run recorded with that `surfaceId`, under A2UI v0.9 semantics: - `updateComponents` upserts components by `id`; - `updateDataModel` replaces or removes the value at `path`; - `deleteSurface` ends the surface. The host MUST refuse, rather than record, an envelope that would make the fold invalid, with `envelope_invalid`: - the first envelope for a `surfaceId` does not begin with `createSurface`; - `createSurface` is sent for a live `surfaceId`; - any message follows `deleteSurface` without a new `createSurface`. A consumer MUST NOT render the surface, or enable any action on it, until the fold holds exactly one component with `id: "root"`. `partial: true` keeps its per-envelope meaning: a surface's actions stay disabled until the envelope that last touched it has finalized. #### Replay and fork Recorded surface envelopes are returned as recorded, never regenerated. - A reader MUST keep accepting version-1 envelopes on replay, fork and poll for the life of the major, including on a host whose floor is now 2. Recorded envelopes are read, not re-admitted through the envelope-kind catalog. - A fork at `fromSeq` folds only the envelopes in `[0, fromSeq)`. #### Trust If any envelope in a surface's fold carries `meta.contentTrust: "untrusted"`, the whole surface is untrusted. The `untrusted_content_blocks_approval` rule then applies to every action on it: an `approval` interrupt it is bound to MUST NOT advance. - A later trusted `updateComponents` does not launder an earlier untrusted one. - One untrusted update taints a surface a trusted node created. The SR-1 harness walks every message. #### `deltaTransport` (deprecated) `schemas/v2/a2ui-surface-delta-frame.schema.json` and the `deltaTransport` facet are deprecated for major 2 (`spec/v1/deprecations.json`, `openwop.deprecation.v2-a2ui-delta-frame` and `-delta-transport-facet`). A v2 host SHOULD NOT advertise the facet. Incremental updates are version-2 envelopes carrying `updateComponents` / `updateDataModel`, recorded and replayable by the fold. The rows carry `removeIn: "3.0"`. An earlier removal takes the `v2-minor` path of [overview.md §0a](https://openwop.dev/spec/v2/core/overview.html): a migration row for the replacement, and a row rescheduled to name the removal minor at least two minors and 30 days ahead. #### Conformance - **`conformance/src/coherence/a2ui-v09-profile.test.ts`** (corpus gate, no host): the profile ⊂ upstream check, each excluded affordance refused, the pinned catalog, the unchanged version-1 branch and the `ui.*`/`media.*` kind carve-out. - **`conformance/src/scenarios/v2-a2ui-v09-surface.test.ts`** (major 2, seams-gated on `POST /conformance/seams/sample/a2ui/emit-surface` in `api/seams-v2.yaml`): version selects branch, fold guarded, catalog equality, legacy readable and sticky taint. A host without the seam records `inapplicable`. - **No render before `root`** is a renderer guarantee the server-oriented suite cannot observe. A reference-app client probe witnesses it (`tier: reference-impl`). The family witness is `openwop.family.a2uiSurface`. It is the last leg of `v2-a2ui-v09-surface`: a version-2 surface is admitted and a version-1 body under schema version 2 is refused, in the same run. It shares the seam gate, so a host without the seam records `inapplicable`. *Sources: RFC 0102, RFC 0114, RFC 0197, RFC 0209, RFC 0220.* ### brand extension Source: https://openwop.dev/spec/v2/ext/brand/ > **Status: Stable.** `brand` names a host service for brand artifacts: themes and personas. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `single-witness` | | **peer-dependency id** | `brand` | | **advertised as** | `extensions.<org>.brand` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `brand` as `extensions["<org>.brand"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `brand` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.brand`: - the key `<org>.brand` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `brand` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### canvas extension Source: https://openwop.dev/spec/v2/ext/canvas/ > **Status: Stable.** `canvas` names a host service that reads and writes canvas state. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `canvas` | | **advertised as** | `extensions.<org>.canvas` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `canvas` as `extensions["<org>.canvas"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `canvas` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.canvas`: - the key `<org>.canvas` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `canvas` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### chat extension Source: https://openwop.dev/spec/v2/ext/chat/ > **Status: Stable.** `chat` names a host service that posts messages and cards into a host-established chat session. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `chat` | | **advertised as** | `extensions.<org>.chat` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `chat` as `extensions["<org>.chat"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `chat` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.chat`: - the key `<org>.chat` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `chat` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### coordination extension Source: https://openwop.dev/spec/v2/ext/coordination/ > **Status: Stable.** `coordination` names multi-participant coordination primitives that operate on roles, such as voters and competitors. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `single-witness` | | **peer-dependency id** | `coordination` | | **advertised as** | `extensions.<org>.coordination` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `coordination` as `extensions["<org>.coordination"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `coordination` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.coordination`: - the key `<org>.coordination` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `coordination` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### dataIntegration extension Source: https://openwop.dev/spec/v2/ext/dataIntegration/ > **Status: Stable.** `dataIntegration` names typed data-source operations: fetches from configured external sources, transforms and run-scoped variables. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `single-witness` | | **peer-dependency id** | `dataIntegration` | | **advertised as** | `extensions.<org>.data-integration` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `dataIntegration` as `extensions["<org>.data-integration"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `dataIntegration` as a dependency only when the host and pack share an out-of-band definition of it. For MCP access, `ctx.mcp` ([`host-services.md`](https://openwop.dev/spec/v2/core/host-services.html) §`mcp`) supersedes the v1 `ctx.dataIntegration.fetchMCP` operation and returns MCP results unaltered. Whether an organization's `dataIntegration` record keeps `fetchMCP` is that organization's decision. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.dataIntegration`: - the key `<org>.data-integration` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `dataIntegration` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144, RFC 0204.* ### entities extension Source: https://openwop.dev/spec/v2/ext/entities/ > **Status: Stable.** `entities` names generic entity CRUD over projects and workspace assets. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `single-witness` | | **peer-dependency id** | `entities` | | **advertised as** | `extensions.<org>.entities` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `entities` as `extensions["<org>.entities"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `entities` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.entities`: - the key `<org>.entities` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `entities` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### gRPC transport notes Source: https://openwop.dev/spec/v2/ext/grpc-transport/ > **Status: Note.** A non-normative note, not a declared family. v2 has no normative gRPC transport, because the conformance suite has no gRPC client or behavioral witness. | Field | Value | | --- | --- | | **witness:** | `unwitnessable` | | **technical:** | `experimental` | | **adoption:** | `none` | | **advertised as** | not advertisable; v2 has no `grpc` discovery record | [`openwop.proto`](https://github.com/openwop/openwop/blob/main/spec/v2/ext/grpc-transport/openwop.proto) is a sketch only. It is not a v2 contract. *Sources: RFC 0094, RFC 0175.* ### kanban extension Source: https://openwop.dev/spec/v2/ext/kanban/ > **Status: Stable.** `kanban` names board, task, timeline and automation operations. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `kanban` | | **advertised as** | `extensions.<org>.kanban` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `kanban` as `extensions["<org>.kanban"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `kanban` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.kanban`: - the key `<org>.kanban` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `kanban` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### knowledge extension Source: https://openwop.dev/spec/v2/ext/knowledge/ > **Status: Stable.** `knowledge` names knowledge-base retrieval through the host's RAG pipeline. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `knowledge` | | **advertised as** | `extensions.<org>.knowledge` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `knowledge` as `extensions["<org>.knowledge"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `knowledge` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.knowledge`: - the key `<org>.knowledge` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `knowledge` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### launchStudio extension Source: https://openwop.dev/spec/v2/ext/launchStudio/ > **Status: Stable.** `launchStudio` names launch-studio operations for the multi-canvas launch workflow. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `launchStudio` | | **advertised as** | `extensions.<org>.launch-studio` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `launchStudio` as `extensions["<org>.launch-studio"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `launchStudio` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.launchStudio`: - the key `<org>.launch-studio` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `launchStudio` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### messaging extension Source: https://openwop.dev/spec/v2/ext/messaging/ > **Status: Stable.** `messaging` names outbound chat-egress dispatch through host-owned connectors. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `messaging` | | **advertised as** | `extensions.<org>.messaging` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `messaging` as `extensions["<org>.messaging"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `messaging` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.messaging`: - the key `<org>.messaging` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `messaging` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### Portability notes Source: https://openwop.dev/spec/v2/ext/portability/ > **Status: Retired.** Superseded by: [portability.md](https://openwop.dev/spec/v2/core/portability.html). This page is not a declared extension family. `portability` is a core family, and its rules live there. ### Provider idempotency registry Source: https://openwop.dev/spec/v2/ext/provider-idempotency/ > **Status: Note.** A supporting registry, not a declared family. | Field | Value | | --- | --- | | **witness:** | `witnessable-gated` | | **technical:** | `experimental` | | **adoption:** | `none` | | **advertised as** | not applicable | [`registry.json`](./registry.json) lists providers known to expose a natural business-identity key. It informs the Layer-2 idempotency requirement in [`security-defaults.md`](https://openwop.dev/spec/v2/core/security-defaults.html). Hosts report the chosen strategy through `GET /runs/{runId}/effects` and the `keying` field of `effect-ledger-projection.schema.json`. *Sources: RFC 0150, RFC 0173.* ### restTransport extension Source: https://openwop.dev/spec/v2/ext/restTransport/ > **Status: Stable.** `restTransport` names conditional GET and response compression on run reads. The run-snapshot rules are in [`runs.md`](https://openwop.dev/spec/v2/core/runs.html) §"Caching and encoding". This page defines the claim and what advertising it adds to those rules. | Field | Value | | --- | --- | | **witness:** | `witnessable-gated` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `restTransport` | | **advertised as** | `extensions.<org>.rest-transport` | | **declared facets** | `conditionalRunGet`, `contentEncodings` | #### The claim A host MAY advertise `restTransport` as `extensions["<org>.rest-transport"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record's two facets are: | Facet | Type | Meaning | | --- | --- | --- | | `conditionalRunGet` | boolean | `true` claims a validator on every run-snapshot read | | `contentEncodings` | array of `gzip`, `br`, `zstd` | the codings the host produces for a run-snapshot read | Other members of the record are the organization's own. #### What the claim adds A host whose record sets `conditionalRunGet: true` MUST carry, on every `200` of `GET /runs/{runId}`, the strong `ETag` that runs.md §"Caching and encoding" makes a SHOULD. The stability and `304` rules there then always apply. For each coding listed in `contentEncodings`, a request that names only that coding in `Accept-Encoding` MUST receive it, subject to the rules in runs.md §"Caching and encoding". A client MUST NOT infer anything else from the record. The claim describes what a client receives at the host's advertised base URL, so a CDN or proxy in front of the host is part of it. A front that drops or rewrites a coding the origin produces makes the claim false. A host behind a front should list only the codings measured through that front. #### Conformance `conformance/src/scenarios/v2-ext-rest-transport.test.ts` (major 2) records the witness under `openwop.family.restTransport`: - **Conditional GET:** runs the `conformance-approval` fixture to `waiting-approval`. It checks for a strong `ETag`, a `304` with no body on a matching `If-None-Match`, and a changed `ETag` once the run completes. - **Codings:** for each advertised coding, the decoded body is byte-identical to the identity body. The probe goes through the base URL it is given, so it measures any front in the path. A host without the claim records `inapplicable`. A host that makes the claim but lacks the fixture records `blocked`. *Sources: RFC 0115, RFC 0220.* ### Sandbox runtime notes Source: https://openwop.dev/spec/v2/ext/sandbox-runtime-notes/ > **Status: Note.** A non-normative note, not a declared family. | Field | Value | | --- | --- | | **witness:** | `unwitnessable` | | **technical:** | `experimental` | | **adoption:** | `none` | | **advertised as** | not advertisable | `node:vm` is not an isolation model; its demonstrator is implementation history only. The v2 contract is in [`security-defaults.md`](https://openwop.dev/spec/v2/core/security-defaults.html): `sandbox.isolationModel` is one of `wasm`, `process`, `container`, or `vm`, and pack execution is bound to the advertised isolation mode. *Sources: RFC 0035, RFC 0173.* ### webResearch extension Source: https://openwop.dev/spec/v2/ext/webResearch/ > **Status: Stable.** `webResearch` names search, fetch and research orchestration through the host's search adapter. It is a discovery-only reservation: v2 defines no portable operations or payload contract for it. | Field | Value | | --- | --- | | **witness:** | `claims-check` | | **technical:** | `stable` | | **adoption:** | `multi-witness` | | **peer-dependency id** | `webResearch` | | **advertised as** | `extensions.<org>.web-research` | | **declared facets** | none defined | #### Contract boundary A host MAY advertise `webResearch` as `extensions["<org>.web-research"]`, where `<org>` is its registered organization ([capabilities.md §3.2](https://openwop.dev/spec/v2/core/capabilities.html)). The record is organization-defined, so: - a client MUST NOT infer portable operations, payloads, or authorization semantics from its presence; - a pack may name `webResearch` as a dependency only when the host and pack share an out-of-band definition of it. #### Conformance The `claims-check` witness checks only that the discovery claim is well formed, not runtime behavior. `conformance/src/scenarios/v2-ext-family-claims.test.ts` records it under `openwop.family.webResearch`: - the key `<org>.web-research` matches `extensionsKeyPattern`, and `<org>` is registered and not reserved; - the record is a JSON object; - `webResearch` is not also a member of the discovery root. A host that does not advertise the family records `inapplicable`. A `Stable` label on this page therefore means a host at evidence tier 2 or better advertises the reservation correctly. It does not mean two hosts interoperate on it ([`../README.md`](https://openwop.dev/spec/v2/ext/README.html)). *Sources: RFC 0144.* ### OpenWOP v2 extensions Source: https://openwop.dev/spec/v2/ext/README.html > **Status: Stable.** This page defines the maturity labels that extension documents carry. Extensions are outside the core profile unless a core document explicitly incorporates them. Each declared extension has a page at `spec/v2/ext/<name>/README.md` and a row in [`../declaration.json`](../declaration.json). A row reserves the identifier; it does not make the extension interoperable. Read the individual page to see whether it defines portable behavior or only a discovery claim. #### Maturity labels `scripts/check-ext-status-coherence.mjs` checks these labels against committed conformance evidence. ##### `Draft` Declared, but without qualifying interoperability evidence. **Predicate:** The family is declared in `spec/v2/declaration.json` with `anchor: ext`. ##### `Stable` Witnessed: at least one host at evidence tier 2 or better serves it, and a **certified** bundle in `evidence/v2-host-bundles/` records the family's declared witness class as satisfied. **Predicate:** The family has at least one `executed-pass` row under its witness id `openwop.family.<key>`, in a bundle that certifies at least one profile and whose `discovery.url` origin is listed at tier 2 or better in [`evidence/host-tiers.json`](https://github.com/openwop/openwop/blob/main/evidence/host-tiers.json). The seven-day comment window has also run since the promotion PR. The label means only what the family's witness class checks: - `witnessable-gated` or `seam-gated`: the host behaves as the page says. - `claims-check`: the host advertises the reservation in a well-formed record. Two hosts are not shown to interoperate on it, because the page defines no portable operations. ##### `Retired` Withdrawn: the family is removed from the declaration, and the document names its replacement or the RFC that retired it. **Predicate:** The family is absent from the declaration, and the document carries a `Superseded by:` or `Retired by:` line. The check works both ways: - A `Stable` document without qualifying evidence fails validation. - A `Draft` document with qualifying evidence is reported as eligible for review and promotion. ##### `Note` A directory with no declared family is an implementation or migration note. It carries `Status: Note.` and MUST say it is not a declared family. It is outside this maturity rule and never becomes `Stable`. Today the notes are `grpc-transport`, `provider-idempotency`, and `sandbox-runtime-notes`. #### What this does not decide Whether an extension should become core. That takes a separate RFC, normative text, machine-readable contracts, and a behavioral witness. *Sources: RFC 0174, RFC 0177, RFC 0220.*