@openwop/spec-artifacts 2.35.1 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/CORPUS-STAMP.json +103 -89
  2. package/api/openapi.yaml +325 -4
  3. package/api/seams-v2.yaml +342 -1
  4. package/api/v2/asyncapi.yaml +1 -1
  5. package/api/v2/openapi.yaml +564 -8
  6. package/package.json +1 -1
  7. package/schemas/a2a-task-state.schema.json +3 -3
  8. package/schemas/artifact-type-pack-manifest.schema.json +3 -3
  9. package/schemas/capabilities.schema.json +34 -1
  10. package/schemas/connection-pack-manifest.schema.json +14 -0
  11. package/schemas/debug-bundle.schema.json +20 -0
  12. package/schemas/dispatch-config.schema.json +1 -1
  13. package/schemas/localized-content-language-settings.schema.json +3 -3
  14. package/schemas/localized-content-page.schema.json +2 -2
  15. package/schemas/localized-content-section.schema.json +3 -3
  16. package/schemas/run-event-payloads.schema.json +4 -2
  17. package/schemas/suspend-request.schema.json +61 -4
  18. package/schemas/v2/a2a-task-state.schema.json +10 -9
  19. package/schemas/v2/agent-deployment-transition.schema.json +4 -4
  20. package/schemas/v2/agent-deployment.schema.json +7 -7
  21. package/schemas/v2/agent-eval-suite.schema.json +10 -10
  22. package/schemas/v2/agent-inventory-response.schema.json +8 -2
  23. package/schemas/v2/artifact-type-pack-manifest.schema.json +3 -3
  24. package/schemas/v2/artifact.schema.json +84 -0
  25. package/schemas/v2/capabilities.schema.json +82 -8
  26. package/schemas/v2/certification-bundle.schema.json +1 -1
  27. package/schemas/v2/channel-presence-payload.schema.json +3 -3
  28. package/schemas/v2/connection-pack-manifest.schema.json +14 -0
  29. package/schemas/v2/conversation-event.schema.json +9 -2
  30. package/schemas/v2/conversation-turn.schema.json +19 -12
  31. package/schemas/v2/dispatch-config.schema.json +1 -1
  32. package/schemas/v2/envelopes/ui.a2ui-surface.schema.json +920 -22
  33. package/schemas/v2/error-envelope.schema.json +11 -1
  34. package/schemas/v2/eval-summary.schema.json +1 -2
  35. package/schemas/v2/goal.schema.json +4 -4
  36. package/schemas/v2/localized-content-language-settings.schema.json +3 -3
  37. package/schemas/v2/localized-content-page.schema.json +2 -2
  38. package/schemas/v2/localized-content-section.schema.json +3 -3
  39. package/schemas/v2/node-pack-manifest.schema.json +105 -0
  40. package/schemas/v2/part.schema.json +81 -0
  41. package/schemas/v2/proposal.schema.json +6 -6
  42. package/schemas/v2/run-event-payloads.schema.json +5 -3
  43. package/schemas/v2/self-hosted-runner-dispatch-frame.schema.json +2 -2
  44. package/schemas/v2/self-hosted-runner-result-frame.schema.json +1 -1
  45. package/schemas/v2/suspend-request.schema.json +251 -6
  46. package/schemas/v2/tool-descriptor.schema.json +246 -2
  47. package/schemas/v2/trigger-event.schema.json +1 -2
  48. package/schemas/v2/trigger-subscription.schema.json +1 -2
  49. package/schemas/v2/webhook-verification.schema.json +22 -0
  50. package/schemas/v2/workflow-definition.schema.json +25 -25
  51. package/spec/v1/alias-detectors.json +13 -1
  52. package/spec/v1/core-standard-manifest.json +2 -2
  53. package/spec/v1/deprecations.json +45 -1
  54. package/spec/v1/deprecations.schema.json +119 -4
  55. package/spec/v1/gaps.json +1925 -4
  56. package/spec/v2/README.md +5 -2
  57. package/spec/v2/core/capabilities.md +13 -26
  58. package/spec/v2/core/conformance.md +5 -8
  59. package/spec/v2/core/connection-packs.md +1 -1
  60. package/spec/v2/core/conversation.md +1 -1
  61. package/spec/v2/core/errors.md +8 -3
  62. package/spec/v2/core/events.md +8 -20
  63. package/spec/v2/core/form-content-packs.md +2 -2
  64. package/spec/v2/core/headers.md +5 -4
  65. package/spec/v2/core/host-services.md +6 -2
  66. package/spec/v2/core/idempotency.md +1 -1
  67. package/spec/v2/core/identity.md +23 -12
  68. package/spec/v2/core/interop.md +34 -4
  69. package/spec/v2/core/interrupt.md +6 -3
  70. package/spec/v2/core/node-pack-runtimes.md +20 -0
  71. package/spec/v2/core/oauth.md +32 -0
  72. package/spec/v2/core/overview.md +19 -6
  73. package/spec/v2/core/packs.md +4 -6
  74. package/spec/v2/core/persistence.md +20 -55
  75. package/spec/v2/core/replay.md +5 -11
  76. package/spec/v2/core/runs.md +9 -8
  77. package/spec/v2/core/security-defaults.md +18 -20
  78. package/spec/v2/core/tool-catalog.md +22 -0
  79. package/spec/v2/core/versioning.md +10 -28
  80. package/spec/v2/core/webhooks.md +16 -4
  81. package/spec/v2/core/workflow-chain-packs.md +1 -1
  82. package/spec/v2/corrections.json +32 -0
  83. package/spec/v2/corrections.schema.json +49 -0
  84. package/spec/v2/declaration.json +18 -13
  85. package/spec/v2/declaration.schema.json +3 -1
  86. package/spec/v2/errors.json +46 -1
  87. package/spec/v2/ext/a2uiSurface/README.md +131 -17
  88. package/spec/v2/ext/dataIntegration/README.md +5 -0
  89. package/spec/v2/facets/a2a.schema.json +4 -0
  90. package/spec/v2/facets/auth.schema.json +24 -0
  91. package/spec/v2/facets/mcp.schema.json +4 -0
  92. package/spec/v2/facets/oauth.schema.json +70 -0
  93. package/spec/v2/facets/webhooks.schema.json +23 -2
  94. package/spec/v2/id-field-bindings.json +2 -0
  95. package/spec/v2/interop-map.json +639 -0
  96. package/spec/v2/interop-map.schema.json +458 -0
  97. package/spec/v2/migrations.json +7 -0
  98. package/spec/v2/migrations.schema.json +58 -0
  99. package/spec/v2/path-manifest.json +6 -1
  100. package/spec/v2/release.json +3 -3
  101. package/spec/v2/surface-baseline.json +7981 -0
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- v1 advertised a `supportedTransports` list that could only honestly say `rest`, carried two legacy embedded-protocol profiles with dated sunsets, and let an unauthenticated peer steer version negotiation with no floor, no refresh obligation, and no audit record. This document is the v2 contract for the two embedded protocols (A2A and MCP): how a host advertises them, how a version is negotiated, and what every negotiation leaves behind. Capability shapes are in capabilities.md; the peer identity is the Subject of identity.md.
8
+ The v2 contract for the two embedded protocols (A2A and MCP): how a host advertises them, how a version is negotiated, and what every negotiation leaves behind. Capability shapes are in capabilities.md; the peer identity is the Subject of identity.md.
9
9
 
10
10
  ## REST is the wire
11
11
 
@@ -49,18 +49,48 @@ The event is content-free: `peer` MUST be a digest of the peer origin, never the
49
49
 
50
50
  **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`.
51
51
 
52
+ ## The operation mappings (RFC 0208)
53
+
54
+ `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; and 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.
55
+
56
+ **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.
57
+
58
+ **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.
59
+
60
+ ## MCP tasks and cancellation (RFC 0198)
61
+
62
+ 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:
63
+
64
+ - MUST answer a `tools/call` that declared the extension with `CreateTaskResult` whenever the run is not terminal when the host answers, never with `InputRequiredResult`;
65
+ - 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;
66
+ - MUST NOT append to a run's log to answer `tasks/get`.
67
+
68
+ **Cancellation.** Until the host has sent its whole response to a request that starts or continues a run, the run belongs to that request: 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.
69
+
52
70
  ## The MCP round ceiling
53
71
 
54
- `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 v1 `requestState` requirements carry over unchanged.
72
+ `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.
55
73
 
56
74
  ## The durable-task projection
57
75
 
58
- `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 NOT emit it: v2 has no `auth` interrupt kind. Adding one is an additive v2.x RFC, not a host extension.
76
+ `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.
77
+
78
+ ## Per-agent cards
79
+
80
+ 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.
81
+
82
+ `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.
83
+
84
+ **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**.
59
85
 
60
86
  ## gRPC
61
87
 
62
88
  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 — an unwitnessable family is not advertisable — and `api/v2/openapi.yaml` and the AsyncAPI document are the only canonical API descriptions. The extension re-enters core only by a v2.x additive RFC that generates the proto from `spec/v2/declaration.json` and lands a suite client.
63
89
 
90
+ ## Trace context (RFC 0207)
91
+
92
+ 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 it 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.
93
+
64
94
  ## Threat model
65
95
 
66
- `SECURITY/threat-model-interop.md` is the threat model for this document: downgrade, card/runtime drift, cross-tenant lookup through a peer, artifact leakage across the boundary, the anonymous end-user actor, and negotiation replay. Its invariants are the two silent-downgrade rows plus `interop-negotiation-authenticated`, `interop-minimum-version-enforced`, and `interop-peer-no-authority-escalation` in `SECURITY/invariants.yaml`. 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.
96
+ `SECURITY/threat-model-interop.md` is the threat model for this document; its invariants are rows of `SECURITY/invariants.yaml`. 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.
@@ -21,6 +21,9 @@
21
21
  | `conversation.exchange` | `conversationId`, `prompt` | The resume value MUST validate against `outcomeSchema` when supplied |
22
22
  | `conversation.close` | `conversationId` | Gated as above |
23
23
  | `low-confidence` | `agentId`, `threshold`, `observed` | 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) |
24
+ | `credential` | `provider`, `scopes`, `reason`, `connectUrl` | Gated on `oauth.credentialInterrupt` (oauth.md); snapshot status `waiting-input`; the resume value is `{ outcome }` and carries no credential |
25
+
26
+ An interrupt of any kind MUST NOT solicit credential material; a credential is acquired through `credential`.
24
27
 
25
28
  `key` is the deterministic re-entry key: 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.
26
29
 
@@ -38,7 +41,7 @@ Every kind uses two registered types (events.md): `interrupt.requested`, whose p
38
41
 
39
42
  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). Exactly one of two concurrent resolves MUST succeed; the other MUST receive `409 interrupt_already_resolved`.
40
43
 
41
- **Callback delivery (RFC 0196).** `createRun.callbackUrl` names where a host that advertises `interrupt.callbackDelivery: true` delivers notice of an interrupt, so its holder can resolve it through the token surface. The payload, timing and signing are host-defined in this revision. A host advertising the facet MUST refuse at `createRun`, with `400 validation_error` and `details.field: "callbackUrl"`, a URL the `webhooks.md` §SSRF registration guard would refuse, MUST re-validate every resolved address at delivery, and 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.
44
+ **Callback delivery (RFC 0196).** `createRun.callbackUrl` names where a host that advertises `interrupt.callbackDelivery: true` delivers notice of an interrupt, so its holder can resolve it through the token surface. The payload, timing and signing are host-defined in this revision. 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, and 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.
42
45
 
43
46
  | Status | Code | Condition |
44
47
  | --- | --- | --- |
@@ -50,7 +53,7 @@ A host MUST expose the run-scoped surface and SHOULD expose the signed-token sur
50
53
 
51
54
  ## Tokens
52
55
 
53
- The token grammar is `ow2.<alg>.<kid>.<payload>.<mac>`, defined in identity.md; `alg` MUST be one the host advertises in `interrupt.tokenAlgs[]` (`hs256` at the cut) and `kid` MUST select a secret the host holds, otherwise `401 interrupt_token_invalid`. A v1 two-segment token remains resolvable under `kid: legacy` until its `expiresAt`.
56
+ The token grammar, the `interrupt.tokenAlgs[]` / `kid` check and the v1 drain are identity.md §4 (`401 interrupt_token_invalid`).
54
57
 
55
58
  | Rule | Requirement |
56
59
  | --- | --- |
@@ -89,4 +92,4 @@ enforce the same eligibility at the store, or MUST NOT expose the record to
89
92
  that principal for write: a rule enforced per route is only as complete as the
90
93
  census of writers (RFC 0187 §C.1).
91
94
 
92
- Refs are opaque to the engine; the host resolves them against its own identity model. 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). A host that does not advertise a ref kind MUST ignore that field. A relaxation of any obligation here is an operator setting recorded in the certification bundle, never a discovery field (security-defaults.md).
95
+ Refs are opaque to the engine; the host resolves them against its own identity model. 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). A host that does not advertise a ref kind MUST ignore that field.
@@ -0,0 +1,20 @@
1
+ # Node-pack runtimes
2
+
3
+ > **Status: Stable · RFC 0008.**
4
+ > **Normative home:** `nodePackRuntimes`.
5
+
6
+ ## Why this exists
7
+
8
+ v2 carried this family only by pointing at `spec/v1/node-packs.md`. This is its v2 contract, and it 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`.
9
+
10
+ ## Languages
11
+
12
+ `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`.
13
+
14
+ ## WASM
15
+
16
+ A host that loads `wasm` packs MUST advertise `nodePackRuntimes.wasm` with at least one `abiVersions[]` entry (RFC 0008), 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.
17
+
18
+ ## The MCP registry record
19
+
20
+ RFC 0203 (`Active`): 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`, and 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. `mcpServer` under any other language, or an `entry` that differs from `remotes[0].url`, makes the manifest invalid (`pack_validation_failed`). The record is by value; this contract defines no registry lookup. 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`.
@@ -0,0 +1,32 @@
1
+ # OAuth
2
+
3
+ > **Status: Stable · RFC 0046, RFC 0047.**
4
+ > **Normative home:** `oauth`, `credentials`.
5
+
6
+ ## Why this exists
7
+
8
+ A connector node needs a token a user granted to a third party. The host obtains it, stores it, refreshes it and hands it to the node's sandbox, so a pack names a provider and scopes and never touches the grant. Here the host is an OAuth client; identity.md covers the host as a protected resource. The client rules, the MCP-reach paragraph and the credential interrupt below are RFC 0199 (`Active`).
9
+
10
+ ## Credentials
11
+
12
+ A host advertising `credentials` MUST resolve a `{ ref, scope }` reference (`schemas/v2/credential-reference.schema.json`) at node execution and inject the material into the node sandbox only; it 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 and the old fails `credential_not_found` after it. `credentials.encryptionAtRest` is a claim about storage; it gates nothing.
13
+
14
+ ## Token lifecycle
15
+
16
+ A host advertising `oauth` MUST perform only the grants in `oauth.grants` and MUST refuse to register a node whose `auth.provider` or scope is not in `oauth.providers` (`oauth_provider_unsupported`, `oauth_scope_unsupported`). It drives the redirect and callback host-side; the code, redirect URI, `state` and PKCE verifier MUST NOT enter a run-visible surface. Tokens persist as a `credentials` entry at scope `user` or `workspace` and are refreshed host-side. On terminal refresh failure the host MUST emit `connector.auth-expired` and fail the node with `connector_auth_expired`, unless it advertises `oauth.credentialInterrupt`.
17
+
18
+ ## The authorization-code client
19
+
20
+ On every `authorization_code` grant the host MUST:
21
+
22
+ 1. send PKCE with `S256` and never `plain`, omitting PKCE only for a provider advertised with `pkce: "unsupported"`;
23
+ 2. send a fresh, unguessable `state` bound to the initiating Subject, and refuse a callback whose `state` is absent, unknown, reused or expired, making no token request for it;
24
+ 3. complete the callback only for the initiating Subject (invariant `oauth-same-user-binding`);
25
+ 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);
26
+ 5. use one fixed, registered redirect URI per provider.
27
+
28
+ 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, and 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`, as MUST a grant for such a provider whose manifest declares no `issuer`.
29
+
30
+ ## The credential interrupt
31
+
32
+ A host advertising `oauth.credentialInterrupt` MUST suspend the node with a `credential` interrupt (interrupt.md) instead of failing it when no credential resolves for the Subject or refresh failed terminally. `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, and `declined` fails the node with `connector_auth_declined`.
@@ -8,15 +8,15 @@
8
8
 
9
9
  ## Reading order
10
10
 
11
- 1. `overview.md` — axioms, §0, claim vocabulary, `ext/` rule
11
+ 1. `overview.md` — axioms, §0, §0a, claim vocabulary, `ext/` rule
12
12
  2. `versioning.md` — major negotiation, `OpenWOP-Version`, the 18 axes, release identity
13
13
  3. `capabilities.md` — one well-known resource, record type, closed root, derived profiles
14
14
  4. `identity.md` — Subject, lanes, `SubjectLink`, id grammars, resume tokens
15
15
  5. `runs.md` — create / get / cancel / fork, `configurable`, snapshot, owner
16
16
  6. `events.md`, `errors.md`, `headers.md` — event `oneOf`, payload and error registries, `OpenWOP-*`
17
- 7. `streams.md`, `interrupt.md`, `idempotency.md`, `replay.md` — run-side surfaces
17
+ 7. `interrupt.md`, `idempotency.md`, `replay.md`, `conversation.md` — run-side surfaces
18
18
  8. `persistence.md` — era key, v1 reader rule, pinned runs
19
- 9. `security-defaults.md`, `webhooks.md`, `interop.md` — obligations of a surface, signatures, A2A / MCP
19
+ 9. `security-defaults.md`, `webhooks.md`, `interop.md`, `host-services.md` — obligations of a surface, signatures, A2A / MCP, host services
20
20
  10. `packs.md`, `connection-packs.md`, `form-content-packs.md`, `workflow-chain-packs.md` — pack identity, engines ceiling
21
21
  11. `conformance.md` — requirement ids, witness classes, bundle v3, seams profile
22
22
 
@@ -31,7 +31,20 @@
31
31
 
32
32
  ## §0 Closed-enum growth rule (RFC 0171 §A.5)
33
33
 
34
- 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; removing or renaming one is a major.
34
+ 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, and removing one is a major except under §0a.
35
+
36
+ ## §0a Retiring a v2 surface (RFC 0197)
37
+
38
+ 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.
39
+
40
+ 1. Its replacement shipped in an earlier 2.x minor.
41
+ 2. A `v2-minor` row in `spec/v1/deprecations.json` named the removal minor at least two minors and 30 days earlier.
42
+ 3. No committed v2 host bundle and no published registry manifest carries it.
43
+ 4. It is an optional family, facet, enum member or envelope kind, so its absence is already a 2.0 state.
44
+ 5. Its family is `experimental` in `spec/v2/declaration.json`.
45
+ 6. No independent host is in the INTEROP-MATRIX v2 table.
46
+
47
+ Readers MUST keep accepting a retired shape on replay, fork and poll; only emission narrows.
35
48
 
36
49
  ## v1 end-of-support (RFC 0174 §B.4)
37
50
 
@@ -51,8 +64,8 @@ Normative for any public conformance statement:
51
64
 
52
65
  ## What is `ext/` (RFC 0174 §E.2; RFC 0169 §B.3)
53
66
 
54
- `spec/v2/core/` is under 25,000 words (`scripts/check-core-budget.mjs`). 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`).
67
+ `spec/v2/core/` stays within the word budget `scripts/check-core-budget.mjs` enforces (RFC 0190). 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`).
55
68
 
56
69
  ## What a MUST means (RFC 0168 §B.1; Axiom 1)
57
70
 
58
- 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 MUST mint a normative observation path before the cut or is demoted to SHOULD (RFC 0168 §B.3; see `conformance.md`).
71
+ 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` §"Witness class".
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- Every one of the 282 pack versions published under v1 either pins `<2.0.0` or declares no ceiling at all, four peer-dependency grammars were signed into the registry, and two signing conventions shared one word while signing different bytes. This document is the v2 contract for pack manifests, the registry tree, peer-dependency identifiers, and signing. The per-kind rules live in connection-packs.md, form-content-packs.md, and workflow-chain-packs.md; the capability vocabulary a pack requires is capabilities.md.
8
+ The v2 contract for pack manifests, the registry tree, peer-dependency identifiers, and signing. The per-kind rules live in connection-packs.md, form-content-packs.md, and workflow-chain-packs.md; the capability vocabulary a pack requires is capabilities.md.
9
9
 
10
10
  ## The engine range
11
11
 
@@ -20,10 +20,8 @@ client MUST treat an absent record as "this host installs no packs" rather than
20
20
  as an unknown.
21
21
 
22
22
  `testMode` is DEPRECATED and MUST NOT be relied on by a client. It advertises
23
- the v1 `/v1/packs-test/*` mirror, a conformance seam — and `conformance.md`
24
- §"The seams profile" places seams in the `openwop-conformance-seams-v2` profile
25
- and the `/conformance/seams/` path space, not in the capability namespace. It
26
- remains advertisable through the overlap because hosts already publish it; it is
23
+ the v1 `/v1/packs-test/*` mirror, a conformance seam (`conformance.md`
24
+ §"The seams profile"), and remains advertisable through the overlap because hosts already publish it; it is
27
25
  removed at 3.0. A host mounting a test catalog SHOULD advertise the seams
28
26
  profile instead, and MUST NOT treat `testMode` as a second way to claim one.
29
27
 
@@ -58,7 +56,7 @@ The 13 manifest schemas carry `$id` under `https://openwop.dev/spec/v2/`; the v1
58
56
  | `security-advisory` | registry | closed |
59
57
  | `prompt-ref` | leaf | none |
60
58
 
61
- Every pack-authored document MUST admit `patternProperties` `^(openwop-|x-|vendor\.)`. The `openwop-` prefix is the v2 spelling of the v1 `x-openwop-*` annotation keys, renamed so annotation keys and wire headers stop sharing a token shape. 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).
59
+ 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).
62
60
 
63
61
  ## Signing
64
62
 
@@ -5,7 +5,7 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- The v2 cut renames event types that are persisted, indexed, and unique-keyed in production stores, and fork and replay read those rows verbatim. This document states 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.
8
+ 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.
9
9
 
10
10
  ## The codemap is data
11
11
 
@@ -25,30 +25,14 @@ The v2 cut renames event types that are persisted, indexed, and unique-keyed in
25
25
  Discovery MUST advertise the value the host writes for new runs and nothing else; a host MUST hold one constant for this axis.
26
26
 
27
27
  **Absent stays era `2` forever; it is never backfilled.** A host MUST NOT rewrite
28
- historical rows to add an explicit `2`, and a reader MUST NOT require one. The
29
- trichotomy is sound only because a v2 host stamps `3` on *every* run it creates:
30
- if any creation path is left unstamped after the cut, the runs it makes are
31
- indistinguishable from pre-cut runs and every reader will translate them as era
32
- `2` — a silent wrong read, not an error. So a host with more than one creation
33
- path MUST begin stamping `3` on **all** of them in the same change; staging that
34
- across deploys is the failure this rule exists to prevent.
35
-
36
- **Collapsing to one constant is a precondition for advertising, not a
37
- consequence.** A host whose creation paths disagree — one writing `2`, another
38
- writing nothing — has no single value to advertise, and whatever it publishes is
39
- false for some of its own runs. Unify the writers first, then advertise. This is
40
- the same class of constraint as the writer rule below and is ordered the same
41
- way: the store is made coherent before the wire describes it.
28
+ historical rows to add an explicit `2`, and a reader MUST NOT require one. A host with more than one creation
29
+ path MUST begin stamping `3` on **all** of them in the same change: an unstamped
30
+ path's runs read as era `2`, a silent wrong read.
42
31
 
43
32
  **The snapshot field is required on the wire, and MAY be synthesized.**
44
- `schemas/v2/run-snapshot.schema.json` requires `eventLogSchemaVersion`, but an
45
- era-`2` run predates the key and has nothing stored. The snapshot is a read
46
- projection, so the host MUST supply `2` from the absent-⇒-`2` rule rather than
47
- fail the read; a missing *stored* era is not a read error. The consequence is
48
- worth stating plainly: on the wire this field is never absent, so it cannot
49
- falsify a host's era handling on its own. What falsifies that is the vocabulary
50
- of the events themselves, which is why the reader and writer rules below carry
51
- the obligation and this field only reports it.
33
+ For an era-`2` run with nothing stored, the host MUST supply `2` from the
34
+ absent-⇒-`2` rule rather than fail the read. The field therefore cannot falsify
35
+ era handling on its own; the reader and writer rules below carry the obligation.
52
36
 
53
37
  ## The `eventLog` family
54
38
 
@@ -88,35 +72,24 @@ A writer that emits a property a closed def cannot seat (RFC 0185 §B) MUST
88
72
  mark the row with what it could not seat, so the refusal names the writer
89
73
  instead of surfacing as an unexplained read failure (RFC 0187 §D.1).
90
74
 
91
- This binds every writer for as long as an era-`2` run stays open, which on a
92
- host with human-approval interrupts can be days. Draining era-`2` runs before
93
- serving v2 is not the path — see §"Runs pinned to v1" — so the writer rule is
94
- what makes an in-flight run safe across the cut. Its witness is
95
- `v2-era-2-append-vocabulary`.
75
+ This binds every writer for as long as an era-`2` run stays open (§"Runs pinned
76
+ to v1"); its witness is `v2-era-2-append-vocabulary`.
96
77
 
97
78
  ### The v1 wire of an era-`3` log
98
79
 
99
- The reader rule above is written for a v2 reader of an era-`2` log. Through the
100
- overlap a host serves BOTH majors (`versioning.md` §5) and v1 operations keep
101
- their `/v1/…` path keys unchanged (§1.2), so the mirror case is forced and the
102
- corpus owed it a rule: a run created today is era `3` and its log is stored in
103
- v2 vocabulary, yet the same log must still be readable on `/v1/…` exactly as it
104
- was before the cut.
80
+ Through the overlap (`versioning.md` §5) an era-`3` log, stored in v2
81
+ vocabulary, must still read on `/v1/…` exactly as before the cut.
105
82
 
106
83
  A host serving both majors MUST therefore map an era-`3` log's `type` back to
107
84
  its v1 spelling on the v1 read path, through the **same codemap row, inverted**.
108
- This is well defined and not a private mapping: `spec/v2/event-codemap.json` is
109
- a bijection — 118 rows, 118 distinct `v1` names, 118 distinct `v2` names, no
110
- many-to-one fold — so the inverse of a row is exact. A host MUST verify that
111
- property at load rather than assume it; if a future row folds two v1 names onto
112
- one v2 name, the inverse stops being a function and the host MUST refuse to
113
- serve the v1 representation rather than guess which spelling to emit.
85
+ The inverse is exact only while `spec/v2/event-codemap.json` is a bijection; a
86
+ host MUST verify that at load and, if a row folds two v1 names onto one v2 name,
87
+ MUST refuse to serve the v1 representation rather than guess a spelling.
114
88
 
115
89
  A type with NO codemap row — v2-only vocabulary, anything RFC 0185/0186 seated
116
90
  — has no v1 spelling to invert to. A host MUST emit it unchanged on the v1 read
117
91
  path, MUST NOT drop the row, and MUST NOT refuse the read for it (RFC 0187
118
- §B.1): a v1 consumer already tolerates an unknown `type`, and the alternatives
119
- lose data or make one new row cost an otherwise readable run.
92
+ §B.1).
120
93
 
121
94
  ### The seat
122
95
 
@@ -124,10 +97,6 @@ The adapter MUST sit at the storage boundary every reader passes through — the
124
97
 
125
98
  The seat is a **claims-check** (conformance.md §Witness class): discharged by that disclosure and by audit, never by the wire. The rule binds **every** reader, including the ones the suite has no name for; `run-event.schema.json` records why three passing legs do not discharge it.
126
99
 
127
- ### Forking a v1 run
128
-
129
- A fork of an era-`2` run MUST produce a prefix byte-equivalent to the translated parent, and its `run.started` MUST carry the legacy Subject where the parent had none (RFC 0176 §A.5; replay.md, identity.md).
130
-
131
100
  ## Runs pinned to v1
132
101
 
133
102
  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.
@@ -136,7 +105,7 @@ A non-terminal run a v2 host inherits carries `version.pinned` events naming cha
136
105
  | --- | --- |
137
106
  | 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. |
138
107
  | 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. |
139
- | Suspended on an interrupt at the cut | The run continues under the row above; its outstanding token is resolvable under `kid: legacy` until `expiresAt` (RFC 0170 §E.1), and the run reads through the adapter. |
108
+ | Suspended on an interrupt at the cut | The run continues under the row above; its token drains per §"Everything else a v1 host persisted". |
140
109
 
141
110
  "Drain" is retired as the only path. 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` (RFC 0176 §B.3). Discovery's `minClientVersion` rule is RFC 0172 row `C5.8`.
142
111
 
@@ -145,8 +114,8 @@ A non-terminal run a v2 host inherits carries `version.pinned` events naming cha
145
114
  | Artifact | Requirement |
146
115
  | --- | --- |
147
116
  | 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). |
148
- | Webhook deliveries | A host advertising both majors MUST dual-emit the `X-openwop-*` and `OpenWOP-*` header families through the overlap; a v2 receiver MUST accept a v1-signed delivery (`X-openwop-*`, scheme `v1`) verifying the same bytes; per-subscription secrets are unchanged; deliveries queued before the cut are drained under their own retry policy with the payload they were serialized with (webhooks.md). |
149
- | Interrupt resume tokens | Drained: a token that is **not** `ow2.`-prefixed is a v1 token and resolves under `kid: legacy` until `expiresAt`; new tokens carry the `ow2.` prefix (identity.md). The rule is written over the prefix and never over a segment count: the v1 `{token}` path parameter carries no `pattern`, so a conforming v1 token may be a single opaque row key, and a count-based rule leaves such a host with no drain rule at all. |
117
+ | Webhook deliveries | Dual-emitted, and queued deliveries drained, per webhooks.md §"Dual emission through the overlap". |
118
+ | Interrupt resume tokens | Drained: a token that is **not** `ow2.`-prefixed is a v1 token and resolves under `kid: legacy` until `expiresAt`; new tokens carry the `ow2.` prefix (identity.md). The rule is written over the prefix, never a segment count. |
150
119
  | 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). |
151
120
  | Owner stamps | A run without a Subject MUST be legacy-stamped at first v2 read and MUST NOT be rewritten later (RFC 0170 §A.3). A host's stored owner fields are projected to the Subject; the projection is the host's to name. |
152
121
  | Audit log | Never upgraded (RFC 0170). |
@@ -170,7 +139,7 @@ Template — one row per store:
170
139
  | --- | --- | --- |
171
140
  | events | v1 vocabulary; `UNIQUE (runId, sequence)` | `translated` |
172
141
  | runs | no `eventLogSchemaVersion`; owner fields | `legacy-stamped` |
173
- | interrupts | two-segment tokens | `drained` |
142
+ | interrupts | un-prefixed (v1) tokens | `drained` |
174
143
  | webhook subscriptions and queued deliveries | subscriptions; serialized deliveries | `unchanged`; `drained` |
175
144
  | idempotency, invocation, outbox, correlation tables | keyed records | `unchanged` |
176
145
  | audit log | audit facts | `never-upgraded` |
@@ -181,7 +150,7 @@ The reference hosts' dispositions are recorded in RFC 0176; a host MUST NOT deci
181
150
 
182
151
  ## Durable acceptance and recovery
183
152
 
184
- RFC 0158 §A–§D, restated because its v1 homes (`idempotency.md`, `storage-adapters.md`) retire with v1. Nothing here is new.
153
+ RFC 0158 §A–§D.
185
154
 
186
155
  | Clause | Requirement |
187
156
  | --- | --- |
@@ -193,8 +162,4 @@ RFC 0158 §A–§D, restated because its v1 homes (`idempotency.md`, `storage-ad
193
162
 
194
163
  A host MAY claim a qualification rung (`durable-single-instance`, `durable-multi-instance`, `multi-region-qualified`; cumulative) only with the evidence named for it, and MUST NOT claim one from tests in which no process was terminated (§D.9). A rung is evidence, not a capability: discovery carries none, and the rung and its bounds are published in the certification bundle (conformance.md §Bundle v3).
195
164
 
196
- ## The corpus-tag pin
197
-
198
- A consumer that vendors any file from `schemas/`, `api/`, or `spec/` MUST pin to a published `openwop-conformance/vX.Y.Z` tag, MUST record the tag, and MUST refuse a sync from any other ref. A v1.x consumer MUST NOT vendor `schemas/v2/` (RFC 0176 §E.1). The `corpus-tag-pinned` check verifies that each consumer's recorded tag resolves (conformance.md).
199
-
200
165
  See also: overview.md, events.md, replay.md, identity.md, webhooks.md.
@@ -5,24 +5,18 @@
5
5
 
6
6
  ## Why this exists
7
7
 
8
- The event log makes any past state of a run reconstructible by folding events up to a sequence. `POST /runs/{runId}:fork` turns that into a wire surface: a replay proves that 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.
8
+ `POST /runs/{runId}:fork` makes any past state of a run re-executable: a replay proves that 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.
9
9
 
10
10
  ## The surface
11
11
 
12
12
  A host that advertises `replay` (capabilities.md) 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 `fork | branch | rerun`; `effectSeamsManifest` is the constant `/host/effect-seams`. There is no `sideEffectSuppression` field: suppression is the only conforming replay behavior (RFC 0173 §B, row `C6.2`) and `none` is not a value.
13
13
 
14
- The request body is `{ mode, fromSeq?, runOptionsOverlay? }`, `mode ∈ replay | branch`. Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are re-executed.
14
+ The request body, `fromSeq` defaults and `201` response are runs.md §Fork. Events with `sequence < fromSeq` are fixed history; events `>= fromSeq` are re-executed.
15
15
 
16
16
  | Rule | Requirement |
17
17
  | --- | --- |
18
- | `fromSeq` for `replay` | MAY be omitted; omission means `0` (full re-execution). |
19
- | `fromSeq` for `branch` | MUST be supplied. |
20
- | `runOptionsOverlay` | MUST be omitted or empty for `replay`; MAY be supplied for `branch`. |
21
18
  | `fromSeq` out of range | `400`; a sequence absent from the source log is `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 (RFC 0194 §C; binds only where a compensation tail follows the terminal event). |
22
19
  | Source run not visible to the caller | `404`. |
23
- | Response | `201` `{ runId, sourceRunId, fromSeq, mode, status, eventsUrl }`; the fork is a new run with its own log. |
24
-
25
- The fork's `owner.tenant` and `owner.subject` MUST be copied verbatim from the source run (RFC 0165 §B.4; identity.md).
26
20
 
27
21
  ## Modes
28
22
 
@@ -34,7 +28,7 @@ The fork's `owner.tenant` and `owner.subject` MUST be copied verbatim from the s
34
28
 
35
29
  The replay contract is observable-output-sequence determinism, not bit-equivalent execution (RFC 0041 §C):
36
30
 
37
- 1. The events at indices `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo per-region clock fields (RFC 0036 §E) and ULID time-component entropy when ULIDs are minted fresh. The range is half-open, matching §The surface: `sequence < fromSeq` is fixed history, and the event AT `fromSeq` is re-executed — so it is governed by §Divergence, not by this clause.
31
+ 1. The events at indices `[0, fromSeq)` MUST be byte-equivalent between source and replay, modulo per-region clock fields (RFC 0036 §E) and ULID time-component entropy when ULIDs are minted fresh. The event at `fromSeq` is governed by §Divergence.
38
32
  2. `variables`, `channels`, and `status` of the run snapshot at each index in that range MUST be byte-equivalent.
39
33
  3. The bytes on the wire of underlying tool and LLM calls MAY differ, provided the observable state at each index is byte-equivalent.
40
34
 
@@ -82,9 +76,9 @@ Pure nodes and LLM calls served from the invocation log MUST re-execute live; ot
82
76
 
83
77
  A host advertising `replay` MUST publish `schemas/v2/effect-seam-manifest.schema.json`-shaped data at `GET /host/effect-seams` (RFC 0173 §C.1): `{ 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).
84
78
 
85
- `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 that 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 — and a row using it MUST carry `note` naming that mechanism, so an unnameable seam is still auditable and none can hide behind a vague label.
79
+ `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 that 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 — and a row using it MUST carry `note` naming that mechanism.
86
80
 
87
- **Completeness outranks driveability.** Every outbound effect path the node runtime can reach MUST be listed, including one the suite has no way to drive: the suite's receiver speaks HTTP, so an `smtp` or `other` row is typically unreachable by it. Such a row is still MUST-list, and the scenario records it `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: an undriveable row that is listed is a known gap in the evidence, while a mislabelled or missing one is a false statement about the host. A seam omitted from the manifest is invisible to the suite: the manifest is a self-declaration whose false negatives are found by audit — negative-existence, not a witness.
81
+ **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`), which the scenario records `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. The manifest is a self-declaration whose false negatives are found by audit — negative-existence, not a witness.
88
82
 
89
83
  ## Replay-from-event-log internals
90
84
 
@@ -37,7 +37,7 @@ Every operation accepts `OpenWOP-Version` (overview.md); every mutating operatio
37
37
 
38
38
  ## Create
39
39
 
40
- The `createRun` body is closed at the composition (`unevaluatedProperties: false`): `workflowId` (REQUIRED unless `mode: eval`), `inputs`, `residency`, `tenantId`, `scopeId`, `callbackUrl` (interrupt.md §Callback delivery: a host that does not advertise `interrupt.callbackDelivery: true` SHOULD refuse it with `400 validation_error`, `details.field: "callbackUrl"`; one that does MUST hold it to the `webhooks.md` §SSRF guard at create time and at delivery), `mode`, `evalSuiteRef`, `agentId`, and the `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`.
40
+ The `createRun` body is closed at the composition (`unevaluatedProperties: false`): `workflowId` (REQUIRED unless `mode: eval`), `inputs`, `residency`, `tenantId`, `scopeId`, `callbackUrl` (interrupt.md §Callback delivery; a refusal is `400 validation_error`, `details.field: "callbackUrl"`), `mode`, `evalSuiteRef`, `agentId`, and the `RunOptions` fields `configurable`, `tags`, `metadata`. A body without `RunOptions` MUST be accepted as if it were `{}`.
41
41
 
42
42
  | Header | Rule |
43
43
  | --- | --- |
@@ -45,7 +45,7 @@ The `createRun` body is closed at the composition (`unevaluatedProperties: false
45
45
  | `OpenWOP-Dedup: enforce` | The host MUST reject a duplicate `(tenantId, scopeId)` with `409 run_already_active` and `Retry-After`. |
46
46
  | `OpenWOP-Force-Engine-Version` | Test keys only; the seams profile. A host MUST reject it on a production credential with `403`. |
47
47
 
48
- The `201` response is `{ runId, status, eventsUrl, statusUrl? }`, `status` 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 that names a different host (a backing service behind the origin) 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 id the `201` returned, percent-encoded per identity.md §5 (a front door that decodes `%2F` before routing makes every tenant-bound id unreachable). `mode: eval` (with `evalSuiteRef` and `agentId` REQUIRED) 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. A `residency.region` the host does not advertise MUST be rejected with `422 residency_unavailable` and no run created. 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`.
48
+ The `201` response is `{ runId, status, eventsUrl, statusUrl? }`, `status` 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 that names a different host (a backing service behind the origin) 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 id the `201` returned, percent-encoded per identity.md §5 (a front door that decodes `%2F` before routing makes every tenant-bound id unreachable). `mode: eval` (with `evalSuiteRef` and `agentId` REQUIRED) 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. A host advertising `dataResidency` MUST reject a `residency.region` outside `dataResidency.regions` with `422 residency_unavailable` and create no run (§"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`.
49
49
 
50
50
  `run.started` (events.md) MUST echo the run's `owner` block exactly as `RunSnapshot.owner` carries it (RFC 0170 §A.1); `transport` records `rest`, `mcp`, `a2a` or `ui`.
51
51
 
@@ -75,7 +75,7 @@ An unknown root key, an unknown key inside a section, or a dotted key (`ai.provi
75
75
  | --- | --- |
76
76
  | `owner` | `{ tenant, workspace?, subject }`, closed, `subject` REQUIRED (`schemas/v2/subject.schema.json`). `principal` and `principalKind` do not exist. A run created before the host emitted subjects reads with the legacy subject rule (identity.md), stamped at first read and never rewritten. |
77
77
  | `status` | `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 §0. |
78
- | `eventLogSchemaVersion` | The era key, integer ≥ 2. A v2 host MUST stamp `3` on every run it creates; a v1-era run reads as `2` (events.md). |
78
+ | `eventLogSchemaVersion` | The era key, integer ≥ 2 (persistence.md §"The era key"). |
79
79
  | `engineVersion` | Integer. |
80
80
  | `compensationStatus` | `none`, `pending`, `running`, `completed`, `partial`, `failed`, `manual`. A host that does not advertise `compensation` MUST omit it; a host that does MUST include it on every snapshot, `none` when never requested. |
81
81
  | `currentNodeId` | Set while suspended; names the node holding the interrupt. |
@@ -94,13 +94,13 @@ The `200` SHOULD carry a strong `ETag` derived from the latest persisted `sequen
94
94
 
95
95
  `cancelRun` accepts `{ reason? }` and answers `200 { runId, status }` with `status` `cancelling` or `cancelled`; the cascade MAY be asynchronous and the run emits `run.cancelled` when it completes. A cancel on a run that is already terminal (`completed`, `failed`, `cancelled`) MUST be refused `409 run_terminal`; a `200` whose `status` echoes the terminal state is outside this grammar. Cancelling a parent MUST NOT silently abandon an active compensation (security-defaults.md). `run.cancelled.parentRunId` with `reason: parent-cancelled` records a cascade from a parent.
96
96
 
97
- A non-terminal run a v2 host inherits from v1 whose `version.pinned` change ids the host still implements MUST continue under the era-2 reader; the pin is never rewritten. When any pinned change id is no longer implemented the host MUST cancel the run with `run.cancelled { reason: 'v1_pin_unsupported', cancelledBy: 'v2-cutover' }` and MUST NOT follow code it no longer has (RFC 0176 §B.1). A run suspended on an interrupt at the cut continues, its token resolvable under `kid: legacy` until `expiresAt` (interrupt.md).
97
+ A non-terminal run inherited from v1 continues, or is cancelled `v1_pin_unsupported`, per persistence.md §"Runs pinned to v1" (RFC 0176 §B.1).
98
98
 
99
99
  `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, MUST return `200 { results[] }` in request order even when every id failed, and 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` — `id_tenant_mismatch` (or `not_found` where existence is not leaked) when the id's tenant segment is not the caller's (identity.md §5 applies inside an entry exactly as on a path), `run_forbidden` for a run in the caller's tenant the caller may not cancel, `run_terminal` for a run already terminal. `ok: true` carries `status` `cancelling` or `cancelled`; `ok: false` carries the error envelope (errors.md).
100
100
 
101
101
  ## Pause and resume
102
102
 
103
- `pauseRun` accepts `{ reason?, drainPolicy? }` with `drainPolicy` `immediate` (snapshot between events) or `drain-current-node` (default; the executing node reaches a terminal first) and answers `202 { runId, status: 'paused', pausedAt? }`; the transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word. With `immediate`, the attempt that was executing is cut between events: 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 — a `node.failed` here would make a replay fold a failure the source never had (replay.md). The record of the interruption is `run.paused` itself; its payload MAY carry `interruptedNodeId` and `interruptedAttempt` so a `debug` consumer can see which attempt was cut. A run already paused, terminal, or otherwise unpausable MUST receive `409`: `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the status that refused it. `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, emits `run.resumed`, and MUST return `409` when the run is not paused — `run_terminal` or `run_state_conflict` by the same rule. Pause is operator-driven and distinct from cancel (terminal) and from an interrupt (`waiting-*`); only `resumeRun` or a cancel exits `paused`. A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state.
103
+ `pauseRun` accepts `{ reason?, drainPolicy? }` with `drainPolicy` `immediate` (snapshot between events) or `drain-current-node` (default; the executing node reaches a terminal first) and answers `202 { runId, status: 'paused', pausedAt? }`; the transition emits `run.paused`, whose payload echoes the request's `drainPolicy` word. With `immediate`, the attempt that was executing is cut between events: 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. The record of the interruption is `run.paused` itself; its payload MAY carry `interruptedNodeId` and `interruptedAttempt` so a `debug` consumer can see which attempt was cut. A run already paused, terminal, or otherwise unpausable MUST receive `409`: `run_terminal` when the run is terminal, else `run_state_conflict` with `details.runStatus` naming the status that refused it. `resumeRun` accepts `{ reason? }`, answers `202 { runId, status: 'running', resumedAt? }`, emits `run.resumed`, and MUST return `409` when the run is not paused — `run_terminal` or `run_state_conflict` by the same rule. Only `resumeRun` or a cancel exits `paused`. A replay MUST fold `run.paused` and `run.resumed` as no-ops for projected state.
104
104
 
105
105
  ## Fork
106
106
 
@@ -112,7 +112,7 @@ A non-terminal run a v2 host inherits from v1 whose `version.pinned` change ids
112
112
 
113
113
  ## Annotations, artifacts, eval summary
114
114
 
115
- `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. `getArtifact` returns the artifact as an implementation-defined JSON object. `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, `404` when the run is not an eval run; the summary MUST be content-free of task output, rubric prose and credentials.
115
+ `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. `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, and a `url` Part in it MUST NOT resolve beyond the caller's `artifacts:read` authorization. `getEvalSummary` returns `schemas/v2/eval-summary.schema.json` for a terminal eval run, `409` while it is running, `404` when the run is not an eval run; the summary MUST be content-free of task output, rubric prose and credentials.
116
116
 
117
117
  ## Conversation and residency capabilities
118
118
 
@@ -121,8 +121,9 @@ workflow whose `nodes[].typeId` references `core.conversationGate` MUST be refus
121
121
  does not advertise `conversationPrimitive`, at registration or at run creation, with `422
122
122
  capability_required` naming the family in `details.requiredCapability`.
123
123
 
124
+ A conversation turn MAY carry `parts`, a non-empty array of A2A `Part` objects (`schemas/v2/part.schema.json`); its presence 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.
125
+
124
126
  A host advertising `dataResidency` MUST honor-or-reject: accept a `residency` constraint naming a
125
127
  region in `dataResidency.regions`, refuse one it does not advertise with `residency_unavailable`,
126
- and MUST NOT silently accept-and-ignore. Advertising `regions` while accepting an unadvertised
127
- region is a hollow advertisement. A host that does not advertise `dataResidency` MAY ignore or
128
+ and MUST NOT silently accept-and-ignore. A host that does not advertise `dataResidency` MAY ignore or
128
129
  reject a `residency` constraint but MUST NOT claim to honor it.