@openwop/spec-artifacts 2.42.7 → 2.42.9

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.
@@ -135,82 +135,157 @@
135
135
  "minLength": 1
136
136
  },
137
137
  "details": {
138
- "oneOf": [
139
- {
140
- "type": "object",
141
- "required": [
142
- "supported"
143
- ],
144
- "properties": {
145
- "supported": {
146
- "type": "array",
147
- "minItems": 1,
148
- "items": {
149
- "type": "string",
150
- "enum": [
151
- "values",
152
- "updates",
153
- "messages",
154
- "debug"
155
- ]
156
- },
157
- "description": "Each individual mode the host serves; combinations are not listed (events.md §The events channel)."
158
- }
159
- },
160
- "additionalProperties": true,
161
- "x-openwop-error": "unsupported_stream_mode"
162
- },
163
- {
164
- "type": "object",
165
- "required": [
166
- "runStatus"
167
- ],
168
- "properties": {
169
- "runStatus": {
170
- "type": "string",
171
- "description": "The run status that refused the transition (runs.md §Pause and resume)."
172
- }
173
- },
174
- "additionalProperties": true,
175
- "x-openwop-error": "run_state_conflict"
138
+ "type": "object",
139
+ "additionalProperties": true,
140
+ "description": "Error-specific contextual data. A code that registers a details schema in errors.json constrains it through the allOf below, selected by `error`; every other code accepts any object."
141
+ }
142
+ },
143
+ "allOf": [
144
+ {
145
+ "if": {
146
+ "properties": {
147
+ "error": {
148
+ "const": "unsupported_stream_mode"
149
+ }
176
150
  },
177
- {
178
- "type": "object",
179
- "additionalProperties": false,
180
- "required": [
181
- "type",
182
- "keys"
183
- ],
184
- "properties": {
185
- "type": {
186
- "type": "string",
187
- "description": "the event type whose payload could not be projected"
151
+ "required": [
152
+ "error"
153
+ ]
154
+ },
155
+ "then": {
156
+ "properties": {
157
+ "details": {
158
+ "type": "object",
159
+ "required": [
160
+ "supported"
161
+ ],
162
+ "properties": {
163
+ "supported": {
164
+ "type": "array",
165
+ "minItems": 1,
166
+ "items": {
167
+ "type": "string",
168
+ "enum": [
169
+ "values",
170
+ "updates",
171
+ "messages",
172
+ "debug"
173
+ ]
174
+ },
175
+ "description": "Each individual mode the host serves; combinations are not listed (events.md §The events channel)."
176
+ }
188
177
  },
189
- "keys": {
190
- "type": "array",
191
- "items": {
192
- "type": "string"
193
- },
194
- "minItems": 1,
195
- "description": "the property names the host could neither carry nor seat"
178
+ "additionalProperties": true,
179
+ "x-openwop-error": "unsupported_stream_mode"
180
+ }
181
+ }
182
+ }
183
+ },
184
+ {
185
+ "if": {
186
+ "properties": {
187
+ "error": {
188
+ "const": "run_state_conflict"
189
+ }
190
+ },
191
+ "required": [
192
+ "error"
193
+ ]
194
+ },
195
+ "then": {
196
+ "properties": {
197
+ "details": {
198
+ "type": "object",
199
+ "required": [
200
+ "runStatus"
201
+ ],
202
+ "properties": {
203
+ "runStatus": {
204
+ "type": "string",
205
+ "description": "The run status that refused the transition (runs.md §Pause and resume)."
206
+ }
196
207
  },
197
- "eventId": {
198
- "$ref": "ids.schema.json#/$defs/eventId"
208
+ "additionalProperties": true,
209
+ "x-openwop-error": "run_state_conflict"
210
+ }
211
+ }
212
+ }
213
+ },
214
+ {
215
+ "if": {
216
+ "properties": {
217
+ "error": {
218
+ "const": "client_version_unsupported"
219
+ }
220
+ },
221
+ "required": [
222
+ "error"
223
+ ]
224
+ },
225
+ "then": {
226
+ "properties": {
227
+ "details": {
228
+ "type": "object",
229
+ "properties": {
230
+ "minClientVersion": {
231
+ "type": "string",
232
+ "pattern": "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)$",
233
+ "description": "OPTIONAL. The host's advertised minClientVersion, so a client refused at discovery learns the floor (versioning.md §1.5)."
234
+ }
199
235
  },
200
- "sequence": {
201
- "type": "integer",
202
- "minimum": 0
203
- }
204
- },
205
- "x-openwop-error": "payload_unprojectable"
236
+ "additionalProperties": true,
237
+ "x-openwop-error": "client_version_unsupported"
238
+ }
239
+ }
240
+ }
241
+ },
242
+ {
243
+ "if": {
244
+ "properties": {
245
+ "error": {
246
+ "const": "payload_unprojectable"
247
+ }
206
248
  },
207
- {
208
- "type": "object",
209
- "additionalProperties": true
249
+ "required": [
250
+ "error"
251
+ ]
252
+ },
253
+ "then": {
254
+ "properties": {
255
+ "details": {
256
+ "type": "object",
257
+ "additionalProperties": false,
258
+ "required": [
259
+ "type",
260
+ "keys"
261
+ ],
262
+ "properties": {
263
+ "type": {
264
+ "type": "string",
265
+ "description": "the event type whose payload could not be projected"
266
+ },
267
+ "keys": {
268
+ "type": "array",
269
+ "items": {
270
+ "type": "string"
271
+ },
272
+ "minItems": 1,
273
+ "description": "the property names the host could neither carry nor seat"
274
+ },
275
+ "eventId": {
276
+ "$ref": "ids.schema.json#/$defs/eventId"
277
+ },
278
+ "sequence": {
279
+ "type": "integer",
280
+ "minimum": 0
281
+ }
282
+ },
283
+ "x-openwop-error": "payload_unprojectable"
284
+ }
210
285
  }
211
- ]
286
+ }
212
287
  }
213
- },
288
+ ],
214
289
  "x-openwop-http-status": {
215
290
  "connection_provider_unresolved": 400,
216
291
  "connector_action_unresolved": 400,
@@ -308,6 +308,12 @@
308
308
  "file": "spec/v2/declaration.json",
309
309
  "token": "\"deltaTransport\"",
310
310
  "deprecation": "openwop.deprecation.v2-a2ui-delta-transport-facet"
311
+ },
312
+ {
313
+ "alias": "optional `locale` on InterruptPayload.data and the resume payload",
314
+ "file": "spec/v1/i18n.md",
315
+ "token": "## `locale` field on `InterruptPayload`",
316
+ "deprecation": "openwop.deprecation.interrupt-locale-field"
311
317
  }
312
318
  ]
313
319
  }
@@ -4,7 +4,7 @@
4
4
  "provenance": {
5
5
  "suite": {
6
6
  "package": "@openwop/openwop-conformance",
7
- "version": "2.42.7"
7
+ "version": "2.42.9"
8
8
  },
9
9
  "note": "Derived from the corpus at generation time. Regenerate with --write; verify with --check."
10
10
  },
@@ -421,5 +421,5 @@
421
421
  "$id": "https://openwop.dev/spec/v1/workspace-file.schema.json"
422
422
  }
423
423
  ],
424
- "digest": "2da5e56166f26214299fff10d28db4e2c2d0c5e42f81aebcc20d66dc9a28c3c0"
424
+ "digest": "fb4145362ce7b57ba7e06e742acac2800106bb12e863f6eb6d2dc9dd2f50cb6c"
425
425
  }
@@ -1002,6 +1002,28 @@
1002
1002
  "token": "deltaTransport"
1003
1003
  }
1004
1004
  ]
1005
+ },
1006
+ {
1007
+ "id": "openwop.deprecation.interrupt-locale-field",
1008
+ "status": "deprecated",
1009
+ "surface": "optional `locale` field on `InterruptPayload.data` and on the interrupt resume payload",
1010
+ "kind": "schema-field",
1011
+ "deprecatedIn": "RFC 0171 §Migration table row `openwop.migration.C4.17` (v2 closes interrupt data and resume bodies)",
1012
+ "authority": "RFCS/0171-v2-wire-envelope.md §Migration table, row openwop.migration.C4.17",
1013
+ "removeIn": "2.0",
1014
+ "replacement": "none; the locale a host rendered in is request-scoped (`Content-Language`, spec/v2/core/i18n.md)",
1015
+ "codemod": "openwop.codemod.interrupt-locale-drop",
1016
+ "sources": [
1017
+ {
1018
+ "file": "RFCS/0171-v2-wire-envelope.md",
1019
+ "token": "openwop.migration.C4.17"
1020
+ },
1021
+ {
1022
+ "file": "spec/v1/i18n.md",
1023
+ "token": "`locale` field on `InterruptPayload`"
1024
+ }
1025
+ ],
1026
+ "removalTrigger": "v2.0-cut"
1005
1027
  }
1006
1028
  ]
1007
1029
  }
package/spec/v1/gaps.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "./gaps.schema.json",
3
3
  "version": 1,
4
- "updated": "2026-09-27",
4
+ "updated": "2026-09-28",
5
5
  "witnessClasses": [
6
6
  "witnessable-unaided",
7
7
  "witnessable-gated",
@@ -12923,8 +12923,8 @@
12923
12923
  "surface": "Two of the three certified hosts read a malformed value as `0.0` and refuse it (`openwop-examples` v2 reference host `router.ts`, openwop-app `protocolVersion.ts`). Under §C.2 both fail the planned `malformed-not-refused` leg.",
12924
12924
  "witness": "unclassified",
12925
12925
  "requirementId": null,
12926
- "disposition": "transferred",
12927
- "target": "rfc-0219",
12926
+ "disposition": "closed",
12927
+ "target": null,
12928
12928
  "sources": [
12929
12929
  {
12930
12930
  "file": "RFCS/registers/0219-client-version-header.gaps.md",
@@ -12940,8 +12940,8 @@
12940
12940
  "surface": "Whether the value names the corpus release the client implements (this RFC) or a product version (v1's `openwop-sdk/<v>`).",
12941
12941
  "witness": "unclassified",
12942
12942
  "requirementId": null,
12943
- "disposition": "transferred",
12944
- "target": "rfc-0219",
12943
+ "disposition": "closed",
12944
+ "target": null,
12945
12945
  "sources": [
12946
12946
  {
12947
12947
  "file": "RFCS/registers/0219-client-version-header.gaps.md",
@@ -12974,8 +12974,8 @@
12974
12974
  "surface": "`client_version_unsupported` has `details: null`. Two hosts send `details.minClientVersion`, MyndHyve sends none.",
12975
12975
  "witness": "unclassified",
12976
12976
  "requirementId": null,
12977
- "disposition": "transferred",
12978
- "target": "spec/v2/core/errors.md",
12977
+ "disposition": "closed",
12978
+ "target": null,
12979
12979
  "sources": [
12980
12980
  {
12981
12981
  "file": "RFCS/registers/0219-client-version-header.gaps.md",
@@ -12991,8 +12991,8 @@
12991
12991
  "surface": "The header's meaning on a `/v1/…` path during the overlap. The reference host applies the floor there; openwop-app does not.",
12992
12992
  "witness": "unclassified",
12993
12993
  "requirementId": null,
12994
- "disposition": "transferred",
12995
- "target": "rfc-0219",
12994
+ "disposition": "closed",
12995
+ "target": null,
12996
12996
  "sources": [
12997
12997
  {
12998
12998
  "file": "RFCS/registers/0219-client-version-header.gaps.md",
@@ -13068,6 +13068,23 @@
13068
13068
  }
13069
13069
  ]
13070
13070
  },
13071
+ {
13072
+ "id": "openwop.gap.0221.1",
13073
+ "rfc": "0221",
13074
+ "local": "G1",
13075
+ "section": "§Implementation notes",
13076
+ "surface": "MyndHyve's behaviour for a secret-less registration is unmeasured.",
13077
+ "witness": "unclassified",
13078
+ "requirementId": null,
13079
+ "disposition": "externally-gated",
13080
+ "target": "myndhyve-next-cut",
13081
+ "sources": [
13082
+ {
13083
+ "file": "RFCS/registers/0221-generated-webhook-secret-returned-once.gaps.md",
13084
+ "token": "G1"
13085
+ }
13086
+ ]
13087
+ },
13071
13088
  {
13072
13089
  "id": "openwop.gap.spec.a2a-integration.g1",
13073
13090
  "rfc": "0152",
@@ -1038,6 +1038,24 @@
1038
1038
  "deprecationId": null,
1039
1039
  "note": "Envelopes are transient; a persisted envelope.* event is read through the adapter."
1040
1040
  },
1041
+ {
1042
+ "id": "openwop.migration.C4.17",
1043
+ "child": "C4",
1044
+ "kind": "remove",
1045
+ "v1": {
1046
+ "surface": "optional `locale` on `InterruptPayload.data` and on the resume payload (i18n.md §\"`locale` field on `InterruptPayload`\")",
1047
+ "path": "spec/v1/i18n.md"
1048
+ },
1049
+ "v2": {
1050
+ "surface": "none — interrupt data and resume bodies are closed; the rendered locale is request-scoped (`Content-Language`, spec/v2/core/i18n.md)"
1051
+ },
1052
+ "codemod": "openwop.codemod.interrupt-locale-drop",
1053
+ "persistedData": "translated",
1054
+ "requirementIds": [],
1055
+ "gapIds": [],
1056
+ "deprecationId": "openwop.deprecation.interrupt-locale-field",
1057
+ "note": "Recorded 2026-09-27: the v2 schema closure (C4.9) left the v1 field no place, and it is dropped rather than re-added. A v1 `interrupt.requested` event carrying `data.locale` is read through the C.9 adapter like any other extra key."
1058
+ },
1041
1059
  {
1042
1060
  "id": "openwop.migration.C6.1",
1043
1061
  "child": "C6",
package/spec/v2/README.md CHANGED
@@ -30,9 +30,9 @@ when v1 ends.
30
30
 
31
31
  | Area | Documents |
32
32
  | --- | --- |
33
- | Foundation | [`overview`](./core/overview.md), [`versioning`](./core/versioning.md), [`headers`](./core/headers.md), [`identity`](./core/identity.md), [`oauth`](./core/oauth.md), [`capabilities`](./core/capabilities.md) |
33
+ | Foundation | [`overview`](./core/overview.md), [`versioning`](./core/versioning.md), [`headers`](./core/headers.md), [`identity`](./core/identity.md), [`oauth`](./core/oauth.md), [`capabilities`](./core/capabilities.md), [`i18n`](./core/i18n.md) |
34
34
  | Execution | [`runs`](./core/runs.md), [`events`](./core/events.md), [`interrupt`](./core/interrupt.md), [`conversation`](./core/conversation.md), [`persistence`](./core/persistence.md), [`idempotency`](./core/idempotency.md), [`replay`](./core/replay.md) |
35
- | Integration | [`webhooks`](./core/webhooks.md), [`interop`](./core/interop.md), [`host services`](./core/host-services.md), [`tool catalog`](./core/tool-catalog.md), [`packs`](./core/packs.md), [`node-pack runtimes`](./core/node-pack-runtimes.md), [`connection packs`](./core/connection-packs.md), [`form-content packs`](./core/form-content-packs.md), [`workflow-chain packs`](./core/workflow-chain-packs.md) |
35
+ | Integration | [`webhooks`](./core/webhooks.md), [`interop`](./core/interop.md), [`host services`](./core/host-services.md), [`tool catalog`](./core/tool-catalog.md), [`portability`](./core/portability.md), [`packs`](./core/packs.md), [`node-pack runtimes`](./core/node-pack-runtimes.md), [`connection packs`](./core/connection-packs.md), [`form-content packs`](./core/form-content-packs.md), [`workflow-chain packs`](./core/workflow-chain-packs.md) |
36
36
  | Reliability | [`errors`](./core/errors.md), [`security defaults`](./core/security-defaults.md), [`conformance`](./core/conformance.md) |
37
37
 
38
38
  ## Source-of-truth map
@@ -19,7 +19,7 @@ Every error a v2 host returns is a row in one registry. A client routes on `erro
19
19
  Every error response body MUST be `{ error, message, details? }` and nothing else (`additionalProperties: false`).
20
20
 
21
21
  - `error` is the registered code or a vendor code. `message` is a non-empty string.
22
- - `details` is an object shaped by the row's `details` schema. A row whose `details` is `null` accepts any object. When a row registers a schema, the generated envelope becomes a `oneOf` discriminated on `error`.
22
+ - `details` is an object shaped by the row's `details` schema. A row whose `details` is `null` accepts any object. When a row registers a schema, the generated envelope applies it to `details` only when `error` names that code.
23
23
  - Contextual data (conflict refs, trace ids, validation paths) MUST live under `details`, never at a new top level.
24
24
  - When present, `details.correlationId` MUST be a non-empty string.
25
25
 
@@ -26,7 +26,7 @@ A form instantiated from a pack advertised through `forms.contentPacks` MUST be
26
26
 
27
27
  ## Localized strings
28
28
 
29
- `label`, `title`, and `description` are localized strings. A host MUST select the rendered language by the locale-selection and fallback rules of `spec/v1/i18n.md`, the `i18n` family's normative text in `spec/v2/declaration.json`, and MUST treat every rendered string as untrusted: escaped for the target surface, never interpreted as markup, script, or a template directive.
29
+ `label`, `title`, and `description` are localized strings. A host MUST select the rendered language by the locale-selection and fallback rules of [i18n.md](i18n.md), and MUST treat every rendered string as untrusted: escaped for the target surface, never interpreted as markup, script, or a template directive.
30
30
 
31
31
  ## Validation
32
32
 
@@ -16,9 +16,10 @@ Standard headers keep their standard names: `Idempotency-Key`, `ETag`, `If-None-
16
16
  | `Idempotency-Key` | 15 | Per-mutation idempotency token (`idempotency.md` Layer 1). The server caches `(tenantId, endpoint, key)` → response for ≥24h. A duplicate request returns the cached response with `OpenWOP-Idempotent-Replay: true`. |
17
17
  | `If-None-Match` | 2 | Conditional request on the discovery document (capabilities.md §1) and the run snapshot (runs.md §Snapshot). A value matching the `ETag` the host sent MUST yield `304 Not Modified` with no body. The 304 carries `OpenWOP-Version` like every response (versioning.md §1.4). |
18
18
  | `Last-Event-ID` | 1 | Resume from sequence after this ID. |
19
+ | `OpenWOP-Client-Version` | 55 | The protocol version the client implements (versioning.md §1.5). Compared with `minClientVersion` on major.minor. A malformed value is treated as absent and MUST NOT produce a 400. Never selects a contract. |
19
20
  | `OpenWOP-Dedup` | 1 | When set, the host's cross-host claim system rejects a duplicate `(tenantId, scopeId)` pair with `409 Conflict`. |
20
21
  | `OpenWOP-Force-Engine-Version` | 1 | Test keys only. The server emits this run's events as if it ran the given engine version, which must be within `Capabilities.testing.forceEngineVersionRange`. Servers MUST reject it on production API keys with `403 force_engine_version_forbidden`. |
21
- | `OpenWOP-Version` | 55 | Selects one of the host's listed major.minor versions. Absent, the host uses its `preferredVersion`; an unlisted value is 406 protocol_version_unsupported. |
22
+ | `OpenWOP-Version` | 55 | Selects one of the host's listed major.minor versions. Absent, `/.well-known/openwop` uses `preferredVersion` and every other unversioned path is v2; an unlisted value is 406 protocol_version_unsupported. |
22
23
 
23
24
  ## Response headers
24
25
 
@@ -0,0 +1,64 @@
1
+ # Internationalization
2
+
3
+ > **Status: Stable.**
4
+ > **Normative home:** `i18n`.
5
+
6
+ ## Why this exists
7
+
8
+ A host renders some text for humans: interrupt prompts, error messages, extension UI strings. This document states how a client asks for a locale and how a host answers. Machine-readable identifiers stay ASCII, dates and times stay ISO 8601, and number, currency and text-direction formatting are out of scope.
9
+
10
+ ## Language tags
11
+
12
+ Every locale identifier — `Accept-Language`, `Content-Language` and every `locale` field — is a [BCP 47](https://www.rfc-editor.org/rfc/rfc5646) tag.
13
+
14
+ - Tags compare case-insensitively (RFC 5646 §2.1.1, RFC 4647 §2).
15
+ - A host SHOULD advertise and emit tags in the case RFC 5646 §2.1.1 recommends: lowercase language, titlecase script, uppercase region (`zh-Hant-TW`, `es-419`).
16
+
17
+ ## `Accept-Language`
18
+
19
+ A client MAY send `Accept-Language` ([RFC 9110 §12.5.4](https://www.rfc-editor.org/rfc/rfc9110#section-12.5.4)) on any request. A host MAY honor it to localize human-facing text in the response.
20
+
21
+ A host:
22
+
23
+ - MUST parse the header without failing the request. A malformed value MUST NOT cause `400`; the host uses its default locale and proceeds.
24
+ - MUST NOT reject a request because it does not support the requested locale.
25
+ - MUST honor q-values: the highest-q locale it supports wins, and ties go to request order.
26
+ - MUST NOT infer a locale from the request body to override the header, which is authoritative.
27
+ - SHOULD set `Content-Language` to the canonical tag of the locale used when it localized the response. When it localized nothing, it SHOULD omit `Content-Language` or set it to its default locale.
28
+ - MAY cache localized text. When localization is expensive, caching by `(localeTag, sourceText)` is RECOMMENDED.
29
+ - MAY return default-locale text at once and emit `node.completed` when the localized version is ready. This is discouraged for an interrupt prompt, which a human needs localized now.
30
+
31
+ ## Fallback
32
+
33
+ When a host cannot localize to the requested locale, it:
34
+
35
+ 1. walks the list in q-value order and uses the first locale it supports;
36
+ 2. otherwise tries the language family: asked for `ja-JP` and holding only `ja`, it uses `ja` and sets `Content-Language: ja`;
37
+ 3. otherwise uses its default locale, which it SHOULD advertise as `i18n.defaultLocale`;
38
+ 4. sets `Content-Language` to the locale it actually used, never another.
39
+
40
+ ## Error envelopes
41
+
42
+ A host MAY localize an error `message`, and MAY then add `details.locale`.
43
+
44
+ - The `error` code MUST be the same registered identifier in every locale ([errors.md](errors.md)); it is never translated.
45
+ - `details` keys are schema field names and are not localized.
46
+ - A human-facing `details` value MAY be localized, and SHOULD carry a `locale` sibling on the same object.
47
+
48
+ ## The `i18n` record
49
+
50
+ A host that negotiates locale SHOULD advertise `i18n`. A host advertising it honors `Accept-Language` on every protected route; a host that omits it serves one locale.
51
+
52
+ - **`defaultLocale`** — the locale returned when no `Accept-Language` entry matches `supportedLocales`; `"en"` when omitted.
53
+ - **`supportedLocales`** — the locales the host can return for human-facing text. It MUST contain `defaultLocale`, and its order carries no meaning.
54
+ - A host that translates by machine SHOULD list only the locales it has validated end to end.
55
+
56
+ ## Replay and fork
57
+
58
+ Locale is chosen at request time. `Content-Language` is request-scoped and not recorded in the event log, so a replay re-projects the recorded text and does not re-render it. A fork localizes by its own request's `Accept-Language`, not the parent run's.
59
+
60
+ ## Clients
61
+
62
+ A client that wants localized content MUST check discovery for `i18n` before sending `Accept-Language`. A host that does not advertise it ignores the header and returns its default locale.
63
+
64
+ *Sources: RFC 0206.*
@@ -179,7 +179,7 @@ Every id field in every v2 schema and every `api/v2/openapi.yaml` parameter and
179
179
  - A host MUST reject a tenant-bound id whose tenant segment is not the caller's with `403` `id_tenant_mismatch`.
180
180
  - A host-minted opaque segment MUST match `^[A-Za-z0-9._~-]{16,128}$`.
181
181
  - Ids in documents and bodies are bound, always. A client MAY bind at its request seam.
182
- - Handle grammars (`memoryRef`, workspace `path`/`etag`, the plugin version token) and their `resolvability` class are specified where each handle is used. An importer MUST re-mint every `host`-scoped handle ([capabilities.md § portability](capabilities.md)).
182
+ - Handle grammars (`memoryRef`, workspace `path`/`etag`, the plugin version token) and their `resolvability` class are specified where each handle is used. An importer MUST re-mint every `host`-scoped handle ([portability.md](portability.md)).
183
183
 
184
184
  ### Wire form
185
185
 
@@ -16,7 +16,7 @@
16
16
  6. [`events.md`](events.md), [`errors.md`](errors.md), [`headers.md`](headers.md) — event `oneOf`, payload and error registries, `OpenWOP-*` headers
17
17
  7. [`interrupt.md`](interrupt.md), [`idempotency.md`](idempotency.md), [`replay.md`](replay.md), [`conversation.md`](conversation.md) — run-side surfaces
18
18
  8. [`persistence.md`](persistence.md) — era key, v1 reader rule, pinned runs
19
- 9. [`security-defaults.md`](security-defaults.md), [`webhooks.md`](webhooks.md), [`interop.md`](interop.md), [`host-services.md`](host-services.md), [`tool-catalog.md`](tool-catalog.md) — surface obligations, signatures, A2A and MCP, host services
19
+ 9. [`security-defaults.md`](security-defaults.md), [`webhooks.md`](webhooks.md), [`interop.md`](interop.md), [`host-services.md`](host-services.md), [`tool-catalog.md`](tool-catalog.md), [`i18n.md`](i18n.md), [`portability.md`](portability.md) — surface obligations, signatures, A2A and MCP, host services, locale, estate export
20
20
  10. [`packs.md`](packs.md), [`node-pack-runtimes.md`](node-pack-runtimes.md), [`connection-packs.md`](connection-packs.md), [`form-content-packs.md`](form-content-packs.md), [`workflow-chain-packs.md`](workflow-chain-packs.md) — pack identity, engines ceiling
21
21
  11. [`conformance.md`](conformance.md) — requirement ids, witness classes, bundle v3, seams profile
22
22
 
@@ -0,0 +1,44 @@
1
+ # Portability
2
+
3
+ > **Status: Stable.**
4
+ > **Normative home:** `portability`.
5
+
6
+ ## Why this exists
7
+
8
+ A tenant's reusable estate moves between hosts as one export bundle, and lands on the destination under the importing caller's identity. This document states the bundle and the rules an importer follows. An adapter that turns another platform's export into a bundle is host tooling.
9
+
10
+ ## The export bundle
11
+
12
+ An `ExportBundle` (`schemas/v2/export-bundle.schema.json`) carries `bundleVersion: "2"`, a `source` (its origin and an informational `originPrincipal`) and `items[]`. Each item has:
13
+
14
+ - a `kind`: `agent`, `pack`, `prompt-template`, `connection-ref`, `schedule`, `roster` or `org-chart`;
15
+ - a bundle-local `ref` and optional `dependsOn` edges;
16
+ - a `payload` shaped by that kind's own schema.
17
+
18
+ ## The `portability` record
19
+
20
+ - **`export`** — the host emits a bundle for the caller's tenant or workspace, optionally limited to some `kinds`.
21
+ - **`import`** — the host applies a bundle.
22
+ - **`kinds`** — the item kinds the host exports and imports.
23
+ - **`dryRun`** — import offers a plan preview that writes nothing. A host advertising `import` MUST advertise `dryRun: true`.
24
+
25
+ ## Import rules
26
+
27
+ - **No credential material.** A bundle MUST NOT contain credential values: a `connection-ref` item carries only references and provider ids. The importer MUST report unbound references in `secretsToRebind` and MUST NOT invent or transfer secret material. A host MUST reject with `422` an imported bundle whose payload carries a literal credential value.
28
+ - **Dry run.** When `import` is advertised, import MUST offer a dry run. A dry run MUST NOT write, and MUST return the plan it would execute: creates, updates, skips, conflicts and unbound credential references.
29
+ - **Idempotent.** Import MUST be idempotent: re-applying a bundle resolves each item to `skipped` or `updated`, never to a duplicate create.
30
+ - **Ordered.** Items MUST be applied in `dependsOn` topological order. A cycle is a `422`.
31
+ - **Re-owned.** Every imported entity MUST be re-owned to the caller's Subject at the destination ([identity.md](identity.md) §1). `source.originPrincipal` is informational and MUST NOT grant any access. Host-scoped handles are re-minted as [identity.md](identity.md) §5 requires.
32
+ - **Executable behavior.** An imported agent or pack that is executable behavior SHOULD pass through the host's install policy ([RFC 0043](https://github.com/openwop/openwop/blob/main/RFCS/0043-registry-and-extension-policy.md)), and MAY be staged as a proposal ([RFC 0096](https://github.com/openwop/openwop/blob/main/RFCS/0096-reviewable-learning-skill-proposal-lifecycle.md)) instead of being activated.
33
+
34
+ Applying an import returns an outcome per item — `created`, `updated`, `skipped` or `failed` — plus `secretsToRebind`.
35
+
36
+ ## The `import.applied` event
37
+
38
+ An applied import emits `import.applied` (`schemas/v2/run-event-payloads.schema.json`). A host MUST NOT emit it unless it advertises `portability`. It carries counts and references only, never item payloads or secret values.
39
+
40
+ ## Routes
41
+
42
+ No protocol path is defined for export or import: a host serves them on routes of its own. A host-private migration from an anonymous sandbox into a signed-in tenant fits this contract, because its response is a subset of the import outcome.
43
+
44
+ *Sources: RFC 0043, RFC 0096, RFC 0098.*
@@ -16,7 +16,7 @@ Every id `$ref`s `schemas/v2/ids.schema.json` ([identity.md](identity.md)). A `r
16
16
 
17
17
  ## Surface
18
18
 
19
- Every operation accepts `OpenWOP-Version` ([overview.md](overview.md)) and every response carries it. Every mutating operation accepts `Idempotency-Key` ([idempotency.md](idempotency.md)). Scopes are the `auth.md` vocabulary.
19
+ Every operation accepts `OpenWOP-Version` ([overview.md](overview.md)) and every response carries it. Every mutating operation accepts `Idempotency-Key` ([idempotency.md](idempotency.md)). Scopes are the vocabulary listed in `api/v2/openapi.yaml`'s security schemes, matched as [identity.md](identity.md) §2.1 describes.
20
20
 
21
21
  | Operation | Method and path | Scope | Gate |
22
22
  | --- | --- | --- | --- |
@@ -43,9 +43,12 @@ A request on an unversioned path MAY carry `OpenWOP-Version: <major>` or `OpenWO
43
43
  | --- | --- |
44
44
  | Header names a major in `protocolVersions[]` | MUST serve that major |
45
45
  | Header names a major not in `protocolVersions[]` | MUST answer `406` `protocol_version_unsupported`, with `details.protocolVersions[]` echoing the list |
46
- | Header absent on an unversioned path | MUST serve `preferredVersion`'s major |
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) |
47
48
  | `/v1/…` path with `OpenWOP-Version` other than `1` | MUST answer `400` `protocol_version_mismatch` |
48
49
 
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
+
49
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)).
50
53
 
51
54
  ### 1.4 The response header
@@ -75,10 +78,26 @@ Otherwise the page MUST move off the shared name.
75
78
 
76
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.
77
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
+
78
89
  `minClientVersion` (axis 15) is optional. When a host advertises it:
79
90
 
80
91
  - It MUST use the axis-1 grammar.
81
- - A host MAY refuse a client below it. A refusal MUST be `426` `client_version_unsupported`.
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`.
95
+
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.
82
101
 
83
102
  ## 2. The 18 version axes
84
103
 
@@ -173,4 +192,4 @@ Every vendor namespace — capability records ([capabilities.md](capabilities.md
173
192
 
174
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).
175
194
 
176
- *Sources: RFC 0167, RFC 0168, RFC 0172, RFC 0176, RFC 0179, RFC 0181, RFC 0193.*
195
+ *Sources: RFC 0167, RFC 0168, RFC 0172, RFC 0176, RFC 0179, RFC 0181, RFC 0193, RFC 0219.*