@openwop/spec-artifacts 2.42.6 → 2.42.8
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.
- package/CORPUS-STAMP.json +70 -67
- package/api/seams-v2.yaml +238 -322
- package/api/v2/asyncapi.yaml +83 -113
- package/api/v2/openapi.yaml +408 -724
- package/package.json +1 -1
- package/schemas/audit-verify-result.schema.json +2 -2
- package/schemas/v2/audit-verify-result.schema.json +2 -2
- package/schemas/v2/capabilities.schema.json +4 -7
- package/schemas/v2/error-envelope.schema.json +144 -69
- package/spec/v1/alias-detectors.json +6 -0
- package/spec/v1/core-standard-manifest.json +2 -2
- package/spec/v1/deprecations.json +22 -0
- package/spec/v1/gaps.json +188 -1
- package/spec/v1/migrations.json +18 -0
- package/spec/v2/README.md +35 -37
- package/spec/v2/core/capabilities.md +186 -119
- package/spec/v2/core/conformance.md +78 -27
- package/spec/v2/core/connection-packs.md +8 -3
- package/spec/v2/core/conversation.md +10 -4
- package/spec/v2/core/errors.md +30 -9
- package/spec/v2/core/events.md +116 -80
- package/spec/v2/core/form-content-packs.md +11 -13
- package/spec/v2/core/headers.md +23 -14
- package/spec/v2/core/host-services.md +22 -5
- package/spec/v2/core/i18n.md +64 -0
- package/spec/v2/core/idempotency.md +48 -36
- package/spec/v2/core/identity.md +110 -42
- package/spec/v2/core/interop.md +82 -21
- package/spec/v2/core/interrupt.md +79 -40
- package/spec/v2/core/node-pack-runtimes.md +18 -4
- package/spec/v2/core/oauth.md +40 -6
- package/spec/v2/core/overview.md +61 -33
- package/spec/v2/core/packs.md +53 -28
- package/spec/v2/core/persistence.md +61 -80
- package/spec/v2/core/portability.md +44 -0
- package/spec/v2/core/replay.md +56 -42
- package/spec/v2/core/runs.md +198 -60
- package/spec/v2/core/security-defaults.md +57 -33
- package/spec/v2/core/tool-catalog.md +29 -6
- package/spec/v2/core/versioning.md +115 -50
- package/spec/v2/core/webhooks.md +61 -20
- package/spec/v2/core/workflow-chain-packs.md +16 -14
- package/spec/v2/corrections.json +17 -1
- package/spec/v2/corrections.schema.json +1 -1
- package/spec/v2/declaration.json +26 -9
- package/spec/v2/declaration.schema.json +10 -0
- package/spec/v2/errors.json +121 -4
- package/spec/v2/errors.schema.json +38 -0
- package/spec/v2/ext/README.md +36 -23
- package/spec/v2/ext/a2uiSurface/README.md +75 -89
- package/spec/v2/ext/brand/README.md +15 -15
- package/spec/v2/ext/canvas/README.md +15 -15
- package/spec/v2/ext/chat/README.md +15 -15
- package/spec/v2/ext/coordination/README.md +15 -15
- package/spec/v2/ext/dataIntegration/README.md +17 -20
- package/spec/v2/ext/entities/README.md +15 -15
- package/spec/v2/ext/grpc-transport/README.md +6 -8
- package/spec/v2/ext/kanban/README.md +15 -15
- package/spec/v2/ext/knowledge/README.md +15 -15
- package/spec/v2/ext/launchStudio/README.md +16 -16
- package/spec/v2/ext/messaging/README.md +15 -15
- package/spec/v2/ext/portability/README.md +3 -17
- package/spec/v2/ext/provider-idempotency/README.md +7 -10
- package/spec/v2/ext/restTransport/README.md +30 -19
- package/spec/v2/ext/sandbox-runtime-notes/README.md +7 -9
- package/spec/v2/ext/webResearch/README.md +16 -16
- package/spec/v2/profiles.json +6 -2
- package/spec/v2/release.json +2 -2
- package/spec/v2/surface-baseline.json +544 -2
|
@@ -1,77 +1,121 @@
|
|
|
1
1
|
# Versioning and Release
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
3
|
+
> **Status: Stable.**
|
|
4
4
|
|
|
5
5
|
## Why this exists
|
|
6
6
|
|
|
7
7
|
How a v2 host selects a major, what each version axis means, and what a release is.
|
|
8
8
|
|
|
9
|
-
## 1. Major negotiation
|
|
9
|
+
## 1. Major negotiation
|
|
10
10
|
|
|
11
11
|
### 1.1 Advertisement
|
|
12
12
|
|
|
13
|
-
A v2 host MUST advertise
|
|
13
|
+
A v2 host MUST advertise two root fields, both REQUIRED in `schemas/v2/capabilities.schema.json` ([capabilities.md](capabilities.md)):
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
- `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>"]`.
|
|
16
|
+
- `preferredVersion` — MUST be a member of `protocolVersions[]`. Through the overlap it MUST name a 1.x member, because a header-less request is a v1 client's ([capabilities.md](capabilities.md) §1; §1.3). On a host serving a single major, it MUST equal `protocolVersion`.
|
|
17
|
+
|
|
18
|
+
A host that drops v1 advertises a `2.x` `preferredVersion`; its header-less representation becomes the closed v2 root.
|
|
19
|
+
|
|
20
|
+
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`. The suite's `--target-major` defaults from it.
|
|
16
21
|
|
|
17
22
|
### 1.2 Paths
|
|
18
23
|
|
|
19
|
-
v1 operations keep their `/v1/…` path keys unchanged through the overlap.
|
|
24
|
+
- v1 operations keep their `/v1/…` path keys unchanged through the overlap.
|
|
25
|
+
- 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.
|
|
26
|
+
- An unversioned path is the v2 surface; v1's rule that it answers `400` does not apply.
|
|
27
|
+
|
|
28
|
+
Advertising a major is a claim about the whole **path space**, not about `/.well-known/openwop` alone (§1.3 selects that resource's representation):
|
|
20
29
|
|
|
21
|
-
A host that advertises a major in `protocolVersions[]` MUST reach, under that major, every operation
|
|
30
|
+
- 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.
|
|
31
|
+
- 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.
|
|
32
|
+
- Seam and proprietary paths are not manifest operations and need no per-major twin.
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
`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.
|
|
24
35
|
|
|
25
|
-
|
|
36
|
+
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](conformance.md).
|
|
26
37
|
|
|
27
38
|
### 1.3 The request header
|
|
28
39
|
|
|
29
|
-
A request on an unversioned path MAY carry `OpenWOP-Version: <major>` or `OpenWOP-Version: <major>.<minor
|
|
40
|
+
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).
|
|
30
41
|
|
|
31
42
|
| Condition | Host behavior |
|
|
32
43
|
| --- | --- |
|
|
33
44
|
| Header names a major in `protocolVersions[]` | MUST serve that major |
|
|
34
|
-
| Header names a major not in `protocolVersions[]` | MUST answer `406` `protocol_version_unsupported
|
|
35
|
-
| Header absent on
|
|
45
|
+
| Header names a major not in `protocolVersions[]` | MUST answer `406` `protocol_version_unsupported`, with `details.protocolVersions[]` echoing the list |
|
|
46
|
+
| Header absent on `/.well-known/openwop` | MUST serve `preferredVersion`'s major |
|
|
47
|
+
| Header absent on any other unversioned path | MUST serve major 2: the path is the v2 surface (§1.2) |
|
|
36
48
|
| `/v1/…` path with `OpenWOP-Version` other than `1` | MUST answer `400` `protocol_version_mismatch` |
|
|
37
49
|
|
|
38
|
-
|
|
50
|
+
`/.well-known/openwop` is the one resource both majors serve at the same unversioned path, so it is the only place a header-less request can come from a client of the earlier major.
|
|
51
|
+
|
|
52
|
+
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](errors.md)).
|
|
39
53
|
|
|
40
54
|
### 1.4 The response header
|
|
41
55
|
|
|
42
|
-
Every protocol response MUST carry `OpenWOP-Version: <major>.<minor>` naming the contract that produced it. Reporting a version other than the one used is a silent downgrade and non-conformant
|
|
56
|
+
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`).
|
|
57
|
+
|
|
58
|
+
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.
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
`spec/v2/path-manifest.json` (or its `/v1/` twin through the overlap). A shell,
|
|
46
|
-
hosting fallback, conformance seam, or proprietary route has no protocol
|
|
47
|
-
version to name.
|
|
60
|
+
On a manifest-named path:
|
|
48
61
|
|
|
49
|
-
|
|
62
|
+
- A non-protocol response MUST NOT carry `OpenWOP-Version` and MUST NOT be `application/json`.
|
|
63
|
+
- 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`).
|
|
50
64
|
|
|
51
|
-
|
|
65
|
+
A vendor path (§5) is not a shared name and is unconstrained.
|
|
52
66
|
|
|
53
|
-
|
|
54
|
-
|
|
67
|
+
#### Content negotiation on a shared name
|
|
68
|
+
|
|
69
|
+
A host MAY serve a protocol operation and a page under one unversioned name, selecting on `Accept`, iff:
|
|
70
|
+
|
|
71
|
+
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.
|
|
72
|
+
2. The page obeys the rules above.
|
|
55
73
|
3. The response carries `Vary: Accept, OpenWOP-Version`.
|
|
56
74
|
|
|
57
75
|
Otherwise the page MUST move off the shared name.
|
|
58
76
|
|
|
59
77
|
### 1.5 Client precedence and `minClientVersion`
|
|
60
78
|
|
|
61
|
-
When both majors are advertised, a v2 client MUST select the highest major it implements that the host lists
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
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.
|
|
82
|
+
|
|
83
|
+
- A client SHOULD send it on every request under major 2.
|
|
84
|
+
- 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.
|
|
85
|
+
- A host MUST NOT refuse a request because it omits the header or sends a malformed value.
|
|
86
|
+
- A value outside the grammar MUST be treated as absent, and MUST NOT produce a `400`.
|
|
87
|
+
- The value is the client's claim. A host MUST NOT use it as an authentication or authorization input.
|
|
88
|
+
|
|
89
|
+
`minClientVersion` (axis 15) is optional. When a host advertises it:
|
|
62
90
|
|
|
63
|
-
|
|
91
|
+
- It MUST use the axis-1 grammar.
|
|
92
|
+
- 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.
|
|
93
|
+
- 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](errors.md)).
|
|
94
|
+
- The discovery document is not exempt: a client refused there learns the floor from `details.minClientVersion`.
|
|
64
95
|
|
|
65
|
-
|
|
96
|
+
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`.
|
|
97
|
+
|
|
98
|
+
This header rule binds requests served under major 2. A request served under major 1 follows that major's frozen text.
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
## 2. The 18 version axes
|
|
103
|
+
|
|
104
|
+
Dispositions:
|
|
105
|
+
|
|
106
|
+
- `unify` — one type and grammar, with a codemod.
|
|
107
|
+
- `first-class` — own schema-enforced grammar and negotiation rule.
|
|
108
|
+
- `retire` — absorbed into the capability record's `{status, since, until?}`.
|
|
109
|
+
- `delete` — removed, with a register row.
|
|
66
110
|
|
|
67
111
|
| # | Axis | Disposition | v2 grammar | Owner |
|
|
68
112
|
| --- | --- | --- | --- | --- |
|
|
69
|
-
| 1 | `protocolVersion` | first-class;
|
|
113
|
+
| 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 |
|
|
70
114
|
| 2 | `protocolVersions[]` + `preferredVersion` | first-class, negotiation input | as #1 | this document |
|
|
71
115
|
| 3 | `engineVersion` | unify: integer everywhere; codemod `openwop.codemod.engine-version-unify` | `integer, minimum 0` | this document |
|
|
72
116
|
| 4 | `eventLogSchemaVersion` | first-class, the era key | integer; v2 writes `3` | `persistence.md` |
|
|
73
117
|
| 5 | per-event `schemaVersion` | first-class; §0 growth rule | integer | `events.md` |
|
|
74
|
-
| 6 | `schemaVersions` map | first-class; the map moves into the record's `kinds` seat
|
|
118
|
+
| 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` |
|
|
75
119
|
| 7 | `version.pinned` | first-class; the v1-pinned-run disposition | integer min/max | `persistence.md` |
|
|
76
120
|
| 8 | `contractProvenance` | delete | — | `capabilities.md` |
|
|
77
121
|
| 9 | `minimumSuiteVersion` | retire into `spec/v2/declaration.json` | semver | `capabilities.md` |
|
|
@@ -80,51 +124,72 @@ When both majors are advertised, a v2 client MUST select the highest major it im
|
|
|
80
124
|
| 12 | MCP `revisions[]` / `preferredVersion` | first-class facet of `mcp` | date | `interop.md` |
|
|
81
125
|
| 13 | `multiAgent.executionModel.version` | first-class | integer with a schema `maximum` the suite reads | `events.md` |
|
|
82
126
|
| 14 | OpenAPI / AsyncAPI `info.version` | generated from the corpus tag | semver | this document |
|
|
83
|
-
| 15 | `minClientVersion` | first-class
|
|
127
|
+
| 15 | `minClientVersion` | first-class (§1.5) | as #1 | this document |
|
|
84
128
|
| 16 | channel `schemaVersion` / `compatibleWith` | first-class | integer / range | `events.md` |
|
|
85
129
|
| 17 | webhook signature scheme | retire into `deprecations.json` | — | `webhooks.md` |
|
|
86
130
|
| 18 | pack `engines.openwop` + `registryVersion` | first-class with the absent-ceiling rule | semver range / semver | `packs.md` |
|
|
87
131
|
|
|
88
|
-
One grammar covers protocol, envelope-kind
|
|
132
|
+
- One grammar covers protocol, envelope-kind and pack axes wherever a version is `<major>.<minor>` (#1, #2, #11, #15).
|
|
133
|
+
- `typeId@<semver>` is a pack axis (`packs.md`); the `2` in `typeId@2.0.0` never means `OpenWOP-Version: 2`.
|
|
134
|
+
- `docs/PROTOCOL-STATUS.md` carries one row per axis.
|
|
89
135
|
|
|
90
136
|
### 2.1 `engineVersion` (axis 3)
|
|
91
137
|
|
|
92
|
-
`engineVersion` MUST be an integer (`minimum 0`) at the discovery root and on every per-event carrier.
|
|
138
|
+
- `engineVersion` MUST be an integer (`minimum 0`) at the discovery root and on every per-event carrier.
|
|
139
|
+
- 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.
|
|
140
|
+
- The codemod `openwop.codemod.engine-version-unify` MUST refuse any value not matching `^(0|[1-9][0-9]*)$`.
|
|
93
141
|
|
|
94
|
-
### 2.2 `eventLogSchemaVersion` (axis 4
|
|
142
|
+
### 2.2 `eventLogSchemaVersion` (axis 4)
|
|
95
143
|
|
|
96
144
|
`eventLogSchemaVersion` is the era key; the schema floor is `minimum 2`. Its stamping, absent-⇒-`2` and discovery rules are `persistence.md` §"The era key"; the reader contract is `persistence.md`.
|
|
97
145
|
|
|
98
|
-
## 3. Where v2 lives
|
|
146
|
+
## 3. Where v2 lives
|
|
147
|
+
|
|
148
|
+
- `spec/v2/core/` and `spec/v2/ext/<key>/` hold the prose.
|
|
149
|
+
- `schemas/v2/` holds every v2 schema, with `$id` under `https://openwop.dev/spec/v2/`; the site publishes them at `/spec/v2/`.
|
|
150
|
+
- 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.
|
|
151
|
+
- AsyncAPI `servers.production.pathname` is empty; every channel address carries its own path, as OpenAPI path keys do.
|
|
152
|
+
|
|
153
|
+
## 4. One release identity
|
|
154
|
+
|
|
155
|
+
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.
|
|
156
|
+
|
|
157
|
+
- 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.
|
|
158
|
+
- The published tarball digest MUST equal the tree's as a release precondition.
|
|
159
|
+
- The identity and advertised-versions checks keep their three-outcome discipline ([conformance.md](conformance.md)).
|
|
160
|
+
- 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.
|
|
161
|
+
- A v1.x consumer MUST NOT vendor `schemas/v2/`.
|
|
162
|
+
|
|
163
|
+
## 5. The overlap
|
|
99
164
|
|
|
100
|
-
|
|
165
|
+
Through the overlap a host:
|
|
101
166
|
|
|
102
|
-
|
|
167
|
+
- MUST advertise both majors (§1.1);
|
|
168
|
+
- MUST emit `OpenWOP-Version` on every response (§1.4);
|
|
169
|
+
- MUST serve `/.well-known/openwop` as one resource whose representation the request header selects ([capabilities.md](capabilities.md)).
|
|
103
170
|
|
|
104
|
-
The
|
|
171
|
+
The dual-stack scenario creates one run through `/v1/runs` with no header and reads it through `/runs` with `OpenWOP-Version: 2`; the response headers name the contract used.
|
|
105
172
|
|
|
106
|
-
A
|
|
173
|
+
A run minted under major 1 and read under major 2 MUST use the tenant-bound projection `<tenantId>/<v1-id>` ([identity.md](identity.md) §5). A host MUST NOT return a bare v1 id in a major-2 response.
|
|
107
174
|
|
|
108
|
-
|
|
175
|
+
### Retirement
|
|
109
176
|
|
|
110
|
-
|
|
177
|
+
The overlap ends at v1 end-of-support ([overview.md](overview.md)): `protocolVersions[]` drops the `1.<n>` member, and every alias carrying the `v1-end-of-support` trigger is removed.
|
|
111
178
|
|
|
112
|
-
**
|
|
113
|
-
|
|
114
|
-
bare v1 id in a major-2 response.
|
|
179
|
+
- **Retirement is atomic.** §1.1 admits no state in which both majors are advertised and `2.x` is preferred, so dropping v1 retires the whole `/v1` path space at once.
|
|
180
|
+
- **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.
|
|
115
181
|
|
|
116
|
-
|
|
182
|
+
### Host-proprietary paths: `/host/<org>/…`
|
|
117
183
|
|
|
118
|
-
|
|
184
|
+
Every vendor namespace — capability records ([capabilities.md](capabilities.md) §3.2), error codes, event types, pack properties — is keyed to an org registered in `spec/v2/declaration.json`; paths join that pattern.
|
|
119
185
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
colliding route or apply §1.4 content negotiation.
|
|
186
|
+
- 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.
|
|
187
|
+
- An org MUST NOT be named after a manifest segment under `/host/` (`reservedOrgs`).
|
|
188
|
+
- A host SHOULD advertise the mount under `extensions.<org>.<name>`.
|
|
189
|
+
- A `/v1/host/<org>/…` twin MAY ride the overlap; it retires atomically with `/v1`.
|
|
125
190
|
|
|
126
|
-
|
|
191
|
+
## 6. Migration rows
|
|
127
192
|
|
|
128
|
-
|
|
193
|
+
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).
|
|
129
194
|
|
|
130
|
-
|
|
195
|
+
*Sources: RFC 0167, RFC 0168, RFC 0172, RFC 0176, RFC 0179, RFC 0181, RFC 0193, RFC 0219.*
|
package/spec/v2/core/webhooks.md
CHANGED
|
@@ -1,33 +1,36 @@
|
|
|
1
1
|
# Webhooks
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
3
|
+
> **Status: Stable.**
|
|
4
4
|
> **Normative home:** `webhooks`.
|
|
5
5
|
|
|
6
6
|
## Why this exists
|
|
7
7
|
|
|
8
|
-
A client registers a URL and an event filter once; the host POSTs matching events, signed, as they happen.
|
|
8
|
+
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.
|
|
9
9
|
|
|
10
10
|
## Surfaces
|
|
11
11
|
|
|
12
|
-
A host that advertises `webhooks` (capabilities.md) serves `registerWebhook` (`POST /webhooks`) and `unregisterWebhook` (`DELETE /webhooks/{webhookId}`) from `api/v2/openapi.yaml`. The facet (`spec/v2/facets/webhooks.schema.json`) is `{ signatureAlgorithms[] }`, which MUST list `"v1"
|
|
12
|
+
A host that advertises `webhooks` ([capabilities.md](capabilities.md)) serves `registerWebhook` (`POST /webhooks`) and `unregisterWebhook` (`DELETE /webhooks/{webhookId}`) from `api/v2/openapi.yaml`. The facet (`spec/v2/facets/webhooks.schema.json`) is `{ signatureAlgorithms[] }`, which MUST list `"v1"`.
|
|
13
13
|
|
|
14
14
|
| Operation | Request | Response |
|
|
15
15
|
| --- | --- | --- |
|
|
16
|
-
| `registerWebhook` | `{ url, events[], secret?, tags?, signatureAlgorithms? }
|
|
16
|
+
| `registerWebhook` | `{ url, events[], secret?, tags?, signatureAlgorithms? }` | `201 { webhookId, secret? }` |
|
|
17
17
|
| `unregisterWebhook` | path `webhookId` | `204`; `404` when unknown; `403` when the caller is outside the subscription's tenant |
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
On `registerWebhook`, `url` MUST be `https://` and `events[]` MUST be non-empty v2 event type names ([events.md](events.md)).
|
|
20
|
+
|
|
21
|
+
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.
|
|
22
|
+
|
|
23
|
+
A `204` from `unregisterWebhook` ends the subscription's deliveries, including retries already scheduled (§Durability).
|
|
20
24
|
|
|
21
25
|
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.
|
|
22
26
|
|
|
23
27
|
## Delivery
|
|
24
28
|
|
|
25
|
-
The delivery envelope is generated from the event's payload definition (events.md §Payloads). The body is `{ runId, workspaceId?, event }
|
|
29
|
+
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.
|
|
26
30
|
|
|
27
|
-
The
|
|
28
|
-
`identity.md
|
|
29
|
-
|
|
30
|
-
exists at delivery time.
|
|
31
|
+
- The body MUST validate against `schemas/v2/webhook-delivery.schema.json`.
|
|
32
|
+
- `workspaceId` is present exactly when `RunSnapshot.owner.workspace` is ([identity.md §1](identity.md)); a host MUST NOT substitute its tenant id for an absent workspace.
|
|
33
|
+
- The envelope's `runId` MUST use the tenant-bound form of [identity.md §5](identity.md), matching the nested event and every response representation, even though no versioned request exists at delivery time.
|
|
31
34
|
|
|
32
35
|
### Headers
|
|
33
36
|
|
|
@@ -43,15 +46,21 @@ A host MUST send all five on every delivery. The signed bytes are `{timestamp}.{
|
|
|
43
46
|
|
|
44
47
|
### Verification
|
|
45
48
|
|
|
46
|
-
A subscriber MUST verify before acting:
|
|
49
|
+
A subscriber MUST verify before acting:
|
|
50
|
+
|
|
51
|
+
1. Reject a timestamp more than ±5 minutes from its clock.
|
|
52
|
+
2. Compute `HMAC-SHA256({timestamp}.{rawBody}, secret)`.
|
|
53
|
+
3. Compare in constant time.
|
|
54
|
+
|
|
55
|
+
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.
|
|
47
56
|
|
|
48
57
|
### Standard Webhooks
|
|
49
58
|
|
|
50
|
-
`standard-webhooks-1` names the symmetric scheme of Standard Webhooks 1.0.0
|
|
59
|
+
`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`.
|
|
51
60
|
|
|
52
61
|
**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.
|
|
53
62
|
|
|
54
|
-
**Endpoint verification.** Before answering `201`,
|
|
63
|
+
**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.
|
|
55
64
|
|
|
56
65
|
**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.
|
|
57
66
|
|
|
@@ -59,28 +68,60 @@ A subscriber MUST verify before acting: reject a timestamp more than ±5 minutes
|
|
|
59
68
|
|
|
60
69
|
### Dual emission through the overlap
|
|
61
70
|
|
|
62
|
-
A host advertising both majors MUST send, on every delivery, the `X-openwop-*` family alongside the `OpenWOP-*` family with identical values
|
|
71
|
+
- 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.
|
|
72
|
+
- A v2 receiver MUST accept a delivery carrying only the `X-openwop-*` family under scheme `v1`, verifying the same bytes.
|
|
73
|
+
- 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](persistence.md)).
|
|
74
|
+
- The `X-openwop-*` family is removed on its register date.
|
|
63
75
|
|
|
64
76
|
## Durability
|
|
65
77
|
|
|
66
|
-
Durable delivery is an obligation of the `webhooks` surface (
|
|
78
|
+
Durable delivery is an obligation of the `webhooks` surface ([security-defaults.md](security-defaults.md)). A host MUST:
|
|
67
79
|
|
|
68
80
|
- retry a failed attempt per its advertised `retryPolicy` (`maxAttempts`, `backoff ∈ none | fixed | exponential`) with backoff between attempts;
|
|
69
|
-
- route a delivery whose retries are exhausted to the dead-letter sink
|
|
81
|
+
- route a delivery whose retries are exhausted to the dead-letter sink (§Dead letters), rather than drop it;
|
|
70
82
|
- deliver each matching event at least once; a receiver MAY observe the same event more than once;
|
|
71
|
-
- 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)
|
|
72
|
-
- sustain that while at least **8** subscriptions have attempts outstanding that their receivers have not answered
|
|
73
|
-
- after `unregisterWebhook` answers `204`, not start any further attempt for that subscription, including attempts already scheduled for retry (invariant `webhook-unregister-stops-delivery
|
|
83
|
+
- 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);
|
|
84
|
+
- sustain that while at least **8** subscriptions have attempts outstanding that their receivers have not answered;
|
|
85
|
+
- after `unregisterWebhook` answers `204`, not start any further attempt for that subscription, including attempts already scheduled for retry (invariant `webhook-unregister-stops-delivery`);
|
|
74
86
|
- dead-letter a `payload_unprojectable` delivery (events.md §Era-2) on the first attempt, never retry it.
|
|
75
87
|
|
|
76
88
|
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.
|
|
77
89
|
|
|
90
|
+
### Delivery isolation
|
|
91
|
+
|
|
92
|
+
One subscription's slow or dead receiver MUST NOT delay another subscription's deliveries (invariant `webhook-delivery-isolation`). The rule is about when an attempt starts, not how soon after an event it must start, and it names no mechanism: a lane per subscription, a concurrent pool or asynchronous I/O all meet it; a sequential loop over a batch does not.
|
|
93
|
+
|
|
94
|
+
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.
|
|
95
|
+
|
|
96
|
+
### Unregister
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
### Dead letters
|
|
101
|
+
|
|
102
|
+
This sink is the delivery sink; the `deadLetter` family is the run sink and is a different thing.
|
|
103
|
+
|
|
104
|
+
- A host advertising `webhooks.deadLetter` MUST serve `GET /webhooks/{webhookId}/dead-letters`.
|
|
105
|
+
- A record MUST NOT carry the delivered body, the delivery headers, or the subscription secret — a dead-letter read names a delivery, it does not replay one.
|
|
106
|
+
- 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.
|
|
107
|
+
- `expiresAt` bounds a record's retention only while its subscription exists.
|
|
108
|
+
|
|
78
109
|
## Replay
|
|
79
110
|
|
|
80
111
|
A host MUST NOT deliver events a `replay` fork re-emits as fixed history; replay-ness is read from the run, never from the event type (replay.md). A `branch` fork's events are new facts and are delivered.
|
|
81
112
|
|
|
82
113
|
## Egress
|
|
83
114
|
|
|
84
|
-
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`.
|
|
115
|
+
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`.
|
|
116
|
+
|
|
117
|
+
Address classification:
|
|
118
|
+
|
|
119
|
+
- An IPv4-mapped IPv6 address (`::ffff:0:0/96`) MUST be judged by the IPv4 address it embeds, whatever its spelling.
|
|
120
|
+
- 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.
|
|
121
|
+
- A host SHOULD refuse every destination the IANA special-purpose address registries mark not globally reachable.
|
|
122
|
+
|
|
123
|
+
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").
|
|
85
124
|
|
|
86
125
|
See also: events.md, replay.md, persistence.md, security-defaults.md.
|
|
126
|
+
|
|
127
|
+
*Sources: RFC 0053, RFC 0165, RFC 0171, RFC 0173, RFC 0176, RFC 0188, RFC 0196, RFC 0201, RFC 0215, RFC 0217.*
|
|
@@ -1,37 +1,39 @@
|
|
|
1
1
|
# Workflow Chain Packs
|
|
2
2
|
|
|
3
|
-
> **Status: Stable
|
|
3
|
+
> **Status: Stable.**
|
|
4
4
|
> **Normative home:** `workflowChainPacks`.
|
|
5
5
|
|
|
6
6
|
## Why this exists
|
|
7
7
|
|
|
8
|
-
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.
|
|
8
|
+
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](packs.md).
|
|
9
9
|
|
|
10
10
|
## Exact pins
|
|
11
11
|
|
|
12
|
-
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.
|
|
12
|
+
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.
|
|
13
13
|
|
|
14
14
|
## Co-registered children
|
|
15
15
|
|
|
16
|
-
When a parent chain expands a `subChainRef`, the child is registered as its own workflow
|
|
16
|
+
When a parent chain expands a `subChainRef`, the child is registered as its own workflow. A host MUST:
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| Deletion | delete the child only when its last parent is deleted |
|
|
23
|
-
| Ownership record | persist the resolved child version in the parent's ownership record, so a re-instantiation or `:fork` reproduces the same child |
|
|
18
|
+
- **Identity** — register the child under a deterministic id, so two parents composing the same child share one registration.
|
|
19
|
+
- **Reference count** — count each parent that references the child; deleting a parent decrements the count.
|
|
20
|
+
- **Deletion** — delete the child only when its last parent is deleted.
|
|
21
|
+
- **Ownership record** — persist the resolved child version in the parent's ownership record, so a re-instantiation or `:fork` reproduces the same child.
|
|
24
22
|
|
|
25
|
-
The ownership record is a persistence.md store.
|
|
23
|
+
The ownership record is a [persistence.md](persistence.md) store.
|
|
26
24
|
|
|
27
25
|
## Parameter substitution
|
|
28
26
|
|
|
29
|
-
`{{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.
|
|
27
|
+
`{{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.
|
|
30
28
|
|
|
31
29
|
## Composition depth
|
|
32
30
|
|
|
33
|
-
A host MUST bound sub-chain nesting by `workflowChainPacks.subChains.maxDepth` (capabilities.md), 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.
|
|
31
|
+
A host MUST bound sub-chain nesting by `workflowChainPacks.subChains.maxDepth` ([capabilities.md](capabilities.md)), 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.
|
|
34
32
|
|
|
35
33
|
## Edge conditions
|
|
36
34
|
|
|
37
|
-
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`.
|
|
35
|
+
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`.
|
|
36
|
+
|
|
37
|
+
A host MUST carry both fields through expansion verbatim and MUST honor them on expanded edges as on authored ones. [form-content-packs.md](form-content-packs.md) reuses this operator set for field visibility.
|
|
38
|
+
|
|
39
|
+
*Sources: RFC 0133, RFC 0177.*
|
package/spec/v2/corrections.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "./corrections.schema.json",
|
|
3
3
|
"$comment": "RFC 0197 §A.4 — v2 Class-3 corrections. Seeded with P3 only. The 2.8.2 removal of `workflowChainPacks.deferredParameters` and `.hostExpansionSeam` (P1) is deliberately NOT seeded: a committed bundle carried `deferredParameters` and then failed `capabilities-root-closed`, so the row would fail R3 — which is the point. P1 is recorded as the motivating defect in COMPATIBILITY.md §3a, not laundered into this register.",
|
|
4
4
|
"version": 1,
|
|
5
|
-
"updated": "2026-09-
|
|
5
|
+
"updated": "2026-09-27",
|
|
6
6
|
"rows": [
|
|
7
7
|
{
|
|
8
8
|
"id": "openwop.correction.v2.1",
|
|
@@ -27,6 +27,22 @@
|
|
|
27
27
|
"manifestsScanned": 190,
|
|
28
28
|
"note": "The prior shape is a manifest shape, not a discovery shape, so no bundle could carry it and the bundle leg is 0 by construction. The registry leg was measured at the correction: 0 of 190 published v2 pack.json documents validated against the schemas BEFORE the change and 185 of 190 after, so no published document carried the `{ publicKeyRef, signatureRef, method }` block the schemas described. `spec/v2/core/packs.md` §Signing, RFC 0177 §C.3 and migration row openwop.migration.C10.1 had already replaced it with `{ keyId, scheme }`; the schemas were the outlier against the prose, the Accepted RFC and the registry at once. `signing` stays OPTIONAL on a bare manifest, so no previously-valid unsigned document is invalidated. FragmentNode.config was closed by the v1→v2 seed against its own description, failing 73 of 81 published chain packs; reopening it narrows nothing."
|
|
29
29
|
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "openwop.correction.v2.2",
|
|
33
|
+
"date": "2026-09-27",
|
|
34
|
+
"class": "w3c-class-3",
|
|
35
|
+
"rfc": "0172",
|
|
36
|
+
"pointers": [
|
|
37
|
+
"schemas/v2/capabilities.schema.json#/minClientVersion"
|
|
38
|
+
],
|
|
39
|
+
"compatibilityEntry": "the v2 `minClientVersion` grammar",
|
|
40
|
+
"priorShapeCensus": {
|
|
41
|
+
"bundles": 0,
|
|
42
|
+
"manifests": 0,
|
|
43
|
+
"manifestsScanned": 156,
|
|
44
|
+
"note": "The prior shape is any string; the corrected shape is the axis-1 `<major>.<minor>` grammar that spec/v2/core/versioning.md §1.5 and the v2-min-client-version scenario already required of an advertised value. Bundle leg: all 8 committed evidence/v2-host-bundles/*.json advertise minClientVersion, and every value conforms (seven \"1.0\", MyndHyve \"0.1\"), so 0 carry a value the pattern rejects. The one \"0.1.0\" in the corpus is a major-1 witness under evidence/rfc-witnesses/, not a v2 bundle. Registry leg: minClientVersion is a discovery field, not a manifest field; 0 of 156 pack.json documents on openwop-registry origin/main (c15d3d6) carry it. The schema added a pattern only, so no property, type or required entry moved and check-v2-surface-monotone sees no change."
|
|
45
|
+
}
|
|
30
46
|
}
|
|
31
47
|
]
|
|
32
48
|
}
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"description": "Every schema pointer the correction moved, as `<repo-relative file>#<INSTANCE pointer>` \u2014 the document-shaped path scripts/generate-v2-surface-baseline.mjs prints (`#/signing`, `#/chains/[]/dag/nodes/[]/config`), never the schema-shaped one (`#/properties/signing`), because a `$defs` name is not a surface. scripts/check-v2-surface-monotone.mjs matches a baseline removal against this list, so a pointer left out is a removal the monotone gate refuses.",
|
|
31
31
|
"items": { "type": "string", "pattern": "^[A-Za-z0-9_./-]+#(/[^#|]*)?$" }
|
|
32
32
|
},
|
|
33
|
-
"compatibilityEntry": { "type": "string", "minLength": 1, "description": "A literal substring of the COMPATIBILITY.md §3 \"correction on record\" entry. The gate fails when it drifts — a register row whose prose entry has been reworded away is an unrecorded shape change." },
|
|
33
|
+
"compatibilityEntry": { "type": "string", "minLength": 1, "description": "A literal substring of the COMPATIBILITY.md §3 \"correction on record\" entry, which is kept in docs/COMPATIBILITY-CORRECTIONS-LOG.md. The gate fails when it drifts — a register row whose prose entry has been reworded away is an unrecorded shape change." },
|
|
34
34
|
"priorShapeCensus": {
|
|
35
35
|
"type": "object",
|
|
36
36
|
"additionalProperties": false,
|