@openwop/openwop-conformance 1.62.0 → 1.65.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.
@@ -4,7 +4,13 @@
4
4
  "title": "PromptPackManifest",
5
5
  "description": "Manifest for a published OpenWOP prompt pack — `pack.json` at the pack root with `kind: \"prompt\"`. Peer to `node-pack-manifest.schema.json` (RFC 0003) and `workflow-chain-pack-manifest.schema.json` (RFC 0013); disjoint from both via the `kind` discriminator. See `spec/v1/prompts.md` §\"Discovery & distribution\" for the canonical contract and RFC 0028 §B for the rationale.\n\nPrompt packs distribute curated PromptTemplate collections via the same signed-tarball + Ed25519 + SRI pipeline that already serves node and workflow-chain packs. When a host installs a prompt pack and advertises `capabilities.prompts.packsSupported: true`, the templates appear in `GET /v1/prompts` with `meta.source: \"pack\"` + `meta.packName` + `meta.packVersion`.",
6
6
  "type": "object",
7
- "required": ["name", "version", "kind", "engines", "prompts"],
7
+ "required": [
8
+ "name",
9
+ "version",
10
+ "kind",
11
+ "engines",
12
+ "prompts"
13
+ ],
8
14
  "additionalProperties": false,
9
15
  "properties": {
10
16
  "name": {
@@ -24,19 +30,38 @@
24
30
  "const": "prompt",
25
31
  "description": "Pack kind discriminator. MUST be the literal string `\"prompt\"` for this schema. Manifests carrying `kind: \"node\"` validate against `node-pack-manifest.schema.json`; `kind: \"workflow-chain\"` against `workflow-chain-pack-manifest.schema.json`."
26
32
  },
27
- "description": { "type": "string", "maxLength": 1024 },
28
- "author": { "type": "string" },
29
- "license": { "type": "string", "description": "SPDX license identifier (e.g., `Apache-2.0`)." },
30
- "homepage": { "type": "string", "format": "uri" },
31
- "repository": { "type": "string", "format": "uri" },
33
+ "description": {
34
+ "type": "string",
35
+ "maxLength": 1024
36
+ },
37
+ "author": {
38
+ "type": "string"
39
+ },
40
+ "license": {
41
+ "type": "string",
42
+ "description": "SPDX license identifier (e.g., `Apache-2.0`)."
43
+ },
44
+ "homepage": {
45
+ "type": "string",
46
+ "format": "uri"
47
+ },
48
+ "repository": {
49
+ "type": "string",
50
+ "format": "uri"
51
+ },
32
52
  "keywords": {
33
53
  "type": "array",
34
- "items": { "type": "string", "maxLength": 64 },
54
+ "items": {
55
+ "type": "string",
56
+ "maxLength": 64
57
+ },
35
58
  "maxItems": 50
36
59
  },
37
60
  "engines": {
38
61
  "type": "object",
39
- "required": ["openwop"],
62
+ "required": [
63
+ "openwop"
64
+ ],
40
65
  "properties": {
41
66
  "openwop": {
42
67
  "type": "string",
@@ -48,13 +73,17 @@
48
73
  },
49
74
  "dependencies": {
50
75
  "type": "object",
51
- "additionalProperties": { "type": "string" },
76
+ "additionalProperties": {
77
+ "type": "string"
78
+ },
52
79
  "description": "Other packs whose templates this pack's templates reference (e.g., via cross-pack `PromptRef.libraryId` lookups, or via a future `extends:` inheritance syntax). Map of pack name → SemVer range. Install-time validation MUST resolve every entry before accepting the install; an unresolvable entry rejects with `prompt_pack_dependency_unresolvable`. The *semantics* of cross-pack composition (template inheritance, transitive `dependencies` closure) are out of scope for RFC 0028 and defer to a follow-up RFC; this RFC ships the field + the install-time resolve MUST only. Shape mirrors `workflow-chain-pack-manifest.schema.json#/properties/dependencies` (RFC 0013)."
53
80
  },
54
81
  "prompts": {
55
82
  "type": "array",
56
83
  "minItems": 1,
57
- "items": { "$ref": "./prompt-template.schema.json" },
84
+ "items": {
85
+ "$ref": "./prompt-template.schema.json"
86
+ },
58
87
  "description": "PromptTemplate entries per RFC 0027 §A. Each MUST have a unique `(templateId, version)` pair within the pack. Hosts installing this pack expose the templates at `GET /v1/prompts` with `meta.source: \"pack\"` + `meta.packName: <this pack's name>` + `meta.packVersion: <this pack's version>` populated automatically (RFC 0028 §C)."
59
88
  },
60
89
  "signing": {
@@ -72,9 +101,17 @@
72
101
  },
73
102
  "method": {
74
103
  "type": "string",
75
- "enum": ["manual", "sigstore"]
104
+ "enum": [
105
+ "manual",
106
+ "sigstore"
107
+ ]
76
108
  }
77
109
  }
78
110
  }
111
+ },
112
+ "patternProperties": {
113
+ "^(x-|vendor\\.)": {
114
+ "description": "Vendor / host extension escape hatch (RFC 0138). A property matching `^(x-|vendor\\.)` is a HOST EXTENSION: its semantics are unspecified by this protocol, and a consumer that does not recognize one MUST ignore it rather than reject the pack. Resolves the contradiction in `host-extensions.md` §\"Vendor-prefixed namespaces\", which requires a client receiving an unknown vendor-prefixed field to treat it as opaque — a field that `additionalProperties: false` previously made impossible to carry. SECURITY: an extension value is PACK-AUTHORED, therefore untrusted; \"ignore\" means ignore. See `node-packs.md` §\"Vendor extensions on pack manifests\" and the `pack-manifest-extension-opaque` invariant."
115
+ }
79
116
  }
80
117
  }
@@ -199,5 +199,10 @@
199
199
  "description": "Optional URL with deprecation / yank rationale + remediation."
200
200
  }
201
201
  },
202
+ "patternProperties": {
203
+ "^(x-|vendor\\.)": {
204
+ "description": "Vendor / host extension escape hatch (RFC 0138). A property matching `^(x-|vendor\\.)` is a HOST EXTENSION: its semantics are unspecified by this protocol, and a consumer that does not recognize one MUST ignore it rather than reject the pack. Resolves the contradiction in `host-extensions.md` \u00a7\"Vendor-prefixed namespaces\", which requires a client receiving an unknown vendor-prefixed field to treat it as opaque \u2014 a field that `additionalProperties: false` previously made impossible to carry. SECURITY: an extension value is PACK-AUTHORED, therefore untrusted; \"ignore\" means ignore. This is the PUBLICATION contract: without the hatch here a pack carrying a root-level extension would validate against its source manifest and then be rejected at registry `PUT` \u2014 the same split-brain RFC 0138 exists to close. Per-item entry objects are already carried loosely (RFC 0107 G1), so this covers the root. See `node-packs.md` \u00a7\"Vendor extensions on pack manifests\" and the `pack-manifest-extension-opaque` invariant."
205
+ }
206
+ },
202
207
  "additionalProperties": false
203
208
  }
@@ -4,7 +4,13 @@
4
4
  "title": "WorkflowChainPackManifest",
5
5
  "description": "Manifest for a published OpenWOP workflow-chain pack — `pack.json` at the pack root with `kind: \"workflow-chain\"`. Distinct from node-pack-manifest.schema.json. See workflow-chain-packs.md for the canonical contract and RFC 0013 for the rationale. Chain packs are workflow-edit-time abstractions: a host editor expands each declared chain inline into the parent workflow at author time, so the dispatching runtime sees only concrete `core.*` (or published-vendor) typeIds.",
6
6
  "type": "object",
7
- "required": ["name", "version", "kind", "engines", "chains"],
7
+ "required": [
8
+ "name",
9
+ "version",
10
+ "kind",
11
+ "engines",
12
+ "chains"
13
+ ],
8
14
  "properties": {
9
15
  "name": {
10
16
  "type": "string",
@@ -23,19 +29,38 @@
23
29
  "const": "workflow-chain",
24
30
  "description": "Pack kind discriminator. MUST be the literal string `\"workflow-chain\"` for this schema. Manifests carrying `kind: \"node\"` (or omitting `kind`) validate against `node-pack-manifest.schema.json` instead."
25
31
  },
26
- "description": { "type": "string", "maxLength": 1024 },
27
- "author": { "type": "string" },
28
- "license": { "type": "string", "description": "SPDX license identifier (e.g., `Apache-2.0`)." },
29
- "homepage": { "type": "string", "format": "uri" },
30
- "repository": { "type": "string", "format": "uri" },
32
+ "description": {
33
+ "type": "string",
34
+ "maxLength": 1024
35
+ },
36
+ "author": {
37
+ "type": "string"
38
+ },
39
+ "license": {
40
+ "type": "string",
41
+ "description": "SPDX license identifier (e.g., `Apache-2.0`)."
42
+ },
43
+ "homepage": {
44
+ "type": "string",
45
+ "format": "uri"
46
+ },
47
+ "repository": {
48
+ "type": "string",
49
+ "format": "uri"
50
+ },
31
51
  "keywords": {
32
52
  "type": "array",
33
- "items": { "type": "string", "maxLength": 64 },
53
+ "items": {
54
+ "type": "string",
55
+ "maxLength": 64
56
+ },
34
57
  "maxItems": 50
35
58
  },
36
59
  "engines": {
37
60
  "type": "object",
38
- "required": ["openwop"],
61
+ "required": [
62
+ "openwop"
63
+ ],
39
64
  "properties": {
40
65
  "openwop": {
41
66
  "type": "string",
@@ -47,22 +72,35 @@
47
72
  },
48
73
  "dependencies": {
49
74
  "type": "object",
50
- "additionalProperties": { "type": "string" },
75
+ "additionalProperties": {
76
+ "type": "string"
77
+ },
51
78
  "description": "Other node packs whose typeIds this pack's chains reference. Map of pack name → semver range. The host editor uses this map at expansion time to verify referenced typeIds resolve."
52
79
  },
53
80
  "chains": {
54
81
  "type": "array",
55
82
  "minItems": 1,
56
- "items": { "$ref": "#/$defs/WorkflowChain" },
83
+ "items": {
84
+ "$ref": "#/$defs/WorkflowChain"
85
+ },
57
86
  "description": "Chains the pack contributes. Each MUST have a unique `chainId` within the pack."
58
87
  },
59
- "signing": { "$ref": "#/$defs/Signing" }
88
+ "signing": {
89
+ "$ref": "#/$defs/Signing"
90
+ }
60
91
  },
61
92
  "additionalProperties": false,
62
93
  "$defs": {
63
94
  "WorkflowChain": {
64
95
  "type": "object",
65
- "required": ["chainId", "version", "label", "description", "parameters", "dag"],
96
+ "required": [
97
+ "chainId",
98
+ "version",
99
+ "label",
100
+ "description",
101
+ "parameters",
102
+ "dag"
103
+ ],
66
104
  "description": "A single workflow-chain entry — a pre-configured DAG fragment + parameter schema that the host editor expands inline at author time. See workflow-chain-packs.md §Chain entry shape.",
67
105
  "properties": {
68
106
  "chainId": {
@@ -96,37 +134,57 @@
96
134
  "additionalProperties": true,
97
135
  "$comment": "Open by design — this field IS a JSON Schema document, so it must accept any of the 30+ JSON Schema 2020-12 keywords (`type`, `properties`, `required`, `oneOf`, `allOf`, etc.). Strict closure would require importing the JSON Schema meta-schema."
98
136
  },
99
- "dag": { "$ref": "#/$defs/WorkflowDefinitionFragment" },
137
+ "dag": {
138
+ "$ref": "#/$defs/WorkflowDefinitionFragment"
139
+ },
100
140
  "outputs": {
101
141
  "type": "object",
102
- "additionalProperties": { "$ref": "#/$defs/ChainOutput" },
142
+ "additionalProperties": {
143
+ "$ref": "#/$defs/ChainOutput"
144
+ },
103
145
  "description": "Declared outputs the chain surfaces to the parent workflow. Keys are output names; values declare type + description."
104
146
  },
105
147
  "capabilities": {
106
148
  "type": "array",
107
149
  "items": {
108
150
  "type": "string",
109
- "enum": ["streamable", "cacheable", "side-effectful", "mcp-exportable"]
151
+ "enum": [
152
+ "streamable",
153
+ "cacheable",
154
+ "side-effectful",
155
+ "mcp-exportable"
156
+ ]
110
157
  },
111
158
  "uniqueItems": true,
112
159
  "description": "Capability traits to propagate to every expanded node. Hosts MUST copy this array into each expanded `WorkflowNode.capabilities` so existing capability gates apply uniformly."
113
160
  },
114
161
  "subChains": {
115
162
  "type": "array",
116
- "items": { "$ref": "#/$defs/SubChainRef" },
163
+ "items": {
164
+ "$ref": "#/$defs/SubChainRef"
165
+ },
117
166
  "description": "RFC 0133 §1.1. OPTIONAL. Child chains this chain composes at RUN TIME. Each entry names a chain (a sibling `chainId` in this pack, or an externally published chain) that a node references via `config.subChainRef`. Unlike RFC 0013's author-time inline splice, a referenced sub-chain is co-instantiated as its own registered workflow and dispatched by the parent as a child run (both parent and child stay owned + builder-editable). A host without runtime child dispatch MUST refuse instantiation with `sub_chain_unsupported` (422) rather than flatten. Requires `capabilities.workflowChainPacks.subChains.supported: true`. See `workflow-chain-packs.md` §\"Sub-chain composition (RFC 0133)\"."
118
167
  },
119
168
  "producedVariables": {
120
169
  "type": "array",
121
- "items": { "$ref": "#/$defs/ProducedVariable" },
170
+ "items": {
171
+ "$ref": "#/$defs/ProducedVariable"
172
+ },
122
173
  "description": "RFC 0133 §2.2. OPTIONAL. Run-scoped values a node writes to the executor's variable bag during the run and downstream nodes read by name via a `{ type:\"variable\", variableName }` input binding — where the value is NOT an author-time `parameter` and rides no typed output port. Prefer edges (a typed `sourceOutput`→`targetInput` hand-off) wherever a node exposes a value on a port; `producedVariables` is only for values written to the run bag with no typed port. On expansion the host emits these into `WorkflowDefinition.variables[]` as run-scoped entries (name + type, no value). Any `{ type:\"variable\" }` read of an UNDECLARED name (not here and not a materialized parameter) is a `variable_undeclared` manifest error. See `workflow-chain-packs.md` §\"Produced (run-scoped) variables (RFC 0133)\"."
123
174
  }
124
175
  },
125
- "additionalProperties": false
176
+ "additionalProperties": false,
177
+ "patternProperties": {
178
+ "^(x-|vendor\\.)": {
179
+ "description": "Vendor / host extension escape hatch (RFC 0138). A property matching `^(x-|vendor\\.)` is a HOST EXTENSION: its semantics are unspecified by this protocol, and a consumer that does not recognize one MUST ignore it rather than reject the pack. Resolves the contradiction in `host-extensions.md` §\"Vendor-prefixed namespaces\", which requires a client receiving an unknown vendor-prefixed field to treat it as opaque — a field that `additionalProperties: false` previously made impossible to carry. SECURITY: an extension value is PACK-AUTHORED, therefore untrusted; \"ignore\" means ignore. See `node-packs.md` §\"Vendor extensions on pack manifests\" and the `pack-manifest-extension-opaque` invariant."
180
+ }
181
+ }
126
182
  },
127
183
  "SubChainRef": {
128
184
  "type": "object",
129
- "required": ["ref"],
185
+ "required": [
186
+ "ref"
187
+ ],
130
188
  "description": "RFC 0133 §1.1. A reference to a child chain the parent composes. `ref` is EITHER a string naming a SIBLING `chainId` in this pack's `chains[]`, OR an object naming an externally published chain (resolved + signature-verified like any pack dependency). A sibling `ref` that matches no `chainId`, or an external `ref` that fails resolution/verification, is `sub_chain_unresolved`. A chain that transitively composes itself is `sub_chain_cycle`.",
131
189
  "properties": {
132
190
  "ref": {
@@ -140,11 +198,24 @@
140
198
  },
141
199
  {
142
200
  "type": "object",
143
- "required": ["packName", "chainId", "version"],
201
+ "required": [
202
+ "packName",
203
+ "chainId",
204
+ "version"
205
+ ],
144
206
  "description": "An externally published chain, resolved + signature-verified like any pack dependency.",
145
207
  "properties": {
146
- "packName": { "type": "string", "minLength": 1, "maxLength": 256 },
147
- "chainId": { "type": "string", "pattern": "^[a-z][a-zA-Z0-9._-]*$", "minLength": 1, "maxLength": 256 },
208
+ "packName": {
209
+ "type": "string",
210
+ "minLength": 1,
211
+ "maxLength": 256
212
+ },
213
+ "chainId": {
214
+ "type": "string",
215
+ "pattern": "^[a-z][a-zA-Z0-9._-]*$",
216
+ "minLength": 1,
217
+ "maxLength": 256
218
+ },
148
219
  "version": {
149
220
  "type": "string",
150
221
  "description": "Semver range for the external chain. Co-registration SHOULD pin the resolved version into the parent's ownership record for reproducibility (RFC 0133 §Unresolved #1, resolved: pin)."
@@ -159,7 +230,11 @@
159
230
  },
160
231
  "ProducedVariable": {
161
232
  "type": "object",
162
- "required": ["name", "producedBy", "type"],
233
+ "required": [
234
+ "name",
235
+ "producedBy",
236
+ "type"
237
+ ],
163
238
  "description": "RFC 0133 §2.2. A run-scoped variable a chain declares: written by one node and read by others via a `{ type:\"variable\", variableName }` input binding. Run-scoped — carries NO author-time value (that is what `parameters` are for); the manifest distinguishes the two.",
164
239
  "properties": {
165
240
  "name": {
@@ -174,7 +249,13 @@
174
249
  },
175
250
  "type": {
176
251
  "type": "string",
177
- "enum": ["string", "number", "boolean", "object", "array"],
252
+ "enum": [
253
+ "string",
254
+ "number",
255
+ "boolean",
256
+ "object",
257
+ "array"
258
+ ],
178
259
  "description": "JSON-Schema type token for validation/inspection."
179
260
  },
180
261
  "description": {
@@ -186,7 +267,10 @@
186
267
  },
187
268
  "ChainOutput": {
188
269
  "type": "object",
189
- "required": ["type", "description"],
270
+ "required": [
271
+ "type",
272
+ "description"
273
+ ],
190
274
  "properties": {
191
275
  "type": {
192
276
  "type": "string",
@@ -201,18 +285,24 @@
201
285
  },
202
286
  "WorkflowDefinitionFragment": {
203
287
  "type": "object",
204
- "required": ["nodes"],
288
+ "required": [
289
+ "nodes"
290
+ ],
205
291
  "description": "Subset of workflow-definition.schema.json. `id`/`name`/`version`/`triggers`/`settings`/`metadata` MUST be omitted (host generates per-expansion); `variables` is replaced by the chain's top-level `parameters`. See workflow-chain-packs.md §WorkflowDefinitionFragment.",
206
292
  "properties": {
207
293
  "nodes": {
208
294
  "type": "array",
209
295
  "minItems": 1,
210
- "items": { "$ref": "#/$defs/FragmentNode" },
296
+ "items": {
297
+ "$ref": "#/$defs/FragmentNode"
298
+ },
211
299
  "description": "Nodes in the fragment. Every node's `typeId` MUST reference a published node-pack typeId or a reserved `core.*` typeId."
212
300
  },
213
301
  "edges": {
214
302
  "type": "array",
215
- "items": { "$ref": "#/$defs/FragmentEdge" },
303
+ "items": {
304
+ "$ref": "#/$defs/FragmentEdge"
305
+ },
216
306
  "description": "Edges between fragment nodes. Required when `nodes.length > 1`."
217
307
  }
218
308
  },
@@ -221,7 +311,10 @@
221
311
  "FragmentNode": {
222
312
  "type": "object",
223
313
  "description": "Mirror of `workflow-definition.schema.json#/$defs/WorkflowNode` with relaxed `required[]` (chain authors MAY omit `name`/`position`/`config`/`inputs` for trivial pass-through nodes). Maintenance note: when fields are added to `WorkflowNode` in `workflow-definition.schema.json`, mirror the addition here so chain packs can express the same shapes. Drift here means chain-pack authors can't use new node features.",
224
- "required": ["id", "typeId"],
314
+ "required": [
315
+ "id",
316
+ "typeId"
317
+ ],
225
318
  "properties": {
226
319
  "id": {
227
320
  "type": "string",
@@ -234,12 +327,18 @@
234
327
  "minLength": 1,
235
328
  "maxLength": 256
236
329
  },
237
- "name": { "type": "string" },
330
+ "name": {
331
+ "type": "string"
332
+ },
238
333
  "position": {
239
334
  "type": "object",
240
335
  "properties": {
241
- "x": { "type": "number" },
242
- "y": { "type": "number" }
336
+ "x": {
337
+ "type": "number"
338
+ },
339
+ "y": {
340
+ "type": "number"
341
+ }
243
342
  },
244
343
  "additionalProperties": false
245
344
  },
@@ -247,7 +346,11 @@
247
346
  "type": "object",
248
347
  "description": "Node config — host-validated against the referenced typeId's config schema. String fields MAY contain `{{params.<name>}}` placeholders that the host MUST substitute at expansion time. RFC 0133: a `core.subWorkflow` / `core.dispatch` (child-run) node MAY carry `subChainRef: string` — a declared `subChains[].ref` (sibling `chainId` or external ref) the host rewrites to the minted child `workflowId` at co-expansion time (§1.3 step 3). A concrete `config.workflowId` inside a fragment remains INVALID — a chain MUST NOT pin a host-specific workflow id (the `not` guard below enforces it).",
249
348
  "additionalProperties": true,
250
- "not": { "required": ["workflowId"] },
349
+ "not": {
350
+ "required": [
351
+ "workflowId"
352
+ ]
353
+ },
251
354
  "properties": {
252
355
  "subChainRef": {
253
356
  "type": "string",
@@ -268,7 +371,10 @@
268
371
  },
269
372
  "FragmentEdge": {
270
373
  "type": "object",
271
- "required": ["from", "to"],
374
+ "required": [
375
+ "from",
376
+ "to"
377
+ ],
272
378
  "properties": {
273
379
  "from": {
274
380
  "type": "string",
@@ -284,7 +390,13 @@
284
390
  },
285
391
  "triggerRule": {
286
392
  "type": "string",
287
- "enum": ["all_success", "any_success", "all_complete", "none_failed", "any_failed"],
393
+ "enum": [
394
+ "all_success",
395
+ "any_success",
396
+ "all_complete",
397
+ "none_failed",
398
+ "any_failed"
399
+ ],
288
400
  "default": "all_success",
289
401
  "description": "Optional fan-in / error-routing rule — the SAME shape and enum as a top-level workflow edge's `triggerRule` (workflow-definition.schema.json §WorkflowEdge). Governs how the target node fires given its incoming edges: `all_success` (default — every incoming edge's source succeeded), `any_success`, `all_complete` (fire when all sources have finished regardless of success — best-effort completion), `none_failed`, `any_failed`. Additive (RFC 0125): omitting it is identical to `all_success`, the implicit prior behavior. Mirrors the RFC 0013 2026-07-03 `condition` amendment's WorkflowEdge→FragmentEdge move. Expansion MUST carry this value onto the resulting `WorkflowEdge` so the scheduler honors it (see `workflow-chain-packs.md` §\"Expansion semantics\")."
290
402
  }
@@ -297,12 +409,28 @@
297
409
  "properties": {
298
410
  "type": {
299
411
  "type": "string",
300
- "enum": ["expression", "equals", "notEquals", "contains", "regex", "truthy", "falsy"],
412
+ "enum": [
413
+ "expression",
414
+ "equals",
415
+ "notEquals",
416
+ "contains",
417
+ "regex",
418
+ "truthy",
419
+ "falsy"
420
+ ],
301
421
  "description": "RFC 0134: `truthy`/`falsy` test the resolved value at `left` for truthiness and take NO `right` operand; `left` is required for all operators except `expression`."
302
422
  },
303
- "left": { "type": "string", "description": "Left operand path (e.g., 'status', 'output.approved'). Required for every operator except `expression`." },
304
- "right": { "description": "Right operand value (any JSON value). Meaningless for `truthy`/`falsy` (RFC 0134)." },
305
- "expression": { "type": "string", "description": "Used when type='expression'." }
423
+ "left": {
424
+ "type": "string",
425
+ "description": "Left operand path (e.g., 'status', 'output.approved'). Required for every operator except `expression`."
426
+ },
427
+ "right": {
428
+ "description": "Right operand value (any JSON value). Meaningless for `truthy`/`falsy` (RFC 0134)."
429
+ },
430
+ "expression": {
431
+ "type": "string",
432
+ "description": "Used when type='expression'."
433
+ }
306
434
  },
307
435
  "additionalProperties": false
308
436
  },
@@ -320,10 +448,18 @@
320
448
  },
321
449
  "method": {
322
450
  "type": "string",
323
- "enum": ["manual", "sigstore"]
451
+ "enum": [
452
+ "manual",
453
+ "sigstore"
454
+ ]
324
455
  }
325
456
  },
326
457
  "additionalProperties": false
327
458
  }
459
+ },
460
+ "patternProperties": {
461
+ "^(x-|vendor\\.)": {
462
+ "description": "Vendor / host extension escape hatch (RFC 0138). A property matching `^(x-|vendor\\.)` is a HOST EXTENSION: its semantics are unspecified by this protocol, and a consumer that does not recognize one MUST ignore it rather than reject the pack. Resolves the contradiction in `host-extensions.md` §\"Vendor-prefixed namespaces\", which requires a client receiving an unknown vendor-prefixed field to treat it as opaque — a field that `additionalProperties: false` previously made impossible to carry. SECURITY: an extension value is PACK-AUTHORED, therefore untrusted; \"ignore\" means ignore. See `node-packs.md` §\"Vendor extensions on pack manifests\" and the `pack-manifest-extension-opaque` invariant."
463
+ }
328
464
  }
329
465
  }
@@ -115,3 +115,55 @@ export function sampleArtifactTypePack() {
115
115
  };
116
116
  return { artifactTypeId, manifest, schema };
117
117
  }
118
+
119
+ /**
120
+ * Recursively strips `^(x-|vendor\.)` properties from a value, so two
121
+ * registration projections can be compared for equality *modulo* the
122
+ * extensions themselves (RFC 0139 §"The differential-install contract").
123
+ *
124
+ * A host that echoes the submitted manifest back inside its projection would
125
+ * otherwise always differ — trivially, and for a reason that proves nothing.
126
+ * Stripping is recursive because the echo can be nested at any depth.
127
+ */
128
+ export function stripExtensions(value: unknown): unknown {
129
+ if (Array.isArray(value)) return value.map(stripExtensions);
130
+ if (value !== null && typeof value === 'object') {
131
+ const out: Record<string, unknown> = {};
132
+ for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
133
+ if (/^(x-|vendor\.)/.test(k)) continue;
134
+ out[k] = stripExtensions(v);
135
+ }
136
+ return out;
137
+ }
138
+ return value;
139
+ }
140
+
141
+ /** Deep-clones a manifest and hangs `extras` off its first `artifactTypes[]` entry AND its root. */
142
+ export function withExtensions(manifest: unknown, extras: Record<string, unknown>): unknown {
143
+ const m = JSON.parse(JSON.stringify(manifest)) as Record<string, unknown>;
144
+ Object.assign(m, extras);
145
+ const types = m['artifactTypes'];
146
+ if (Array.isArray(types) && types.length > 0 && types[0] && typeof types[0] === 'object') {
147
+ Object.assign(types[0] as Record<string, unknown>, extras);
148
+ }
149
+ return m;
150
+ }
151
+
152
+ /**
153
+ * Stable stringify for projection comparison — key order must not decide the
154
+ * verdict, or leg 3 would red on a host that serialises its map differently
155
+ * between two calls and we would report an opacity violation that isn't one.
156
+ */
157
+ export function canonicalJson(value: unknown): string {
158
+ const walk = (v: unknown): unknown => {
159
+ if (Array.isArray(v)) return v.map(walk);
160
+ if (v !== null && typeof v === 'object') {
161
+ const o = v as Record<string, unknown>;
162
+ const out: Record<string, unknown> = {};
163
+ for (const k of Object.keys(o).sort()) out[k] = walk(o[k]);
164
+ return out;
165
+ }
166
+ return v;
167
+ };
168
+ return JSON.stringify(walk(value));
169
+ }
@@ -156,3 +156,20 @@ export function experimentalGate(
156
156
  }
157
157
  return behaviorGate(profileName, advertised);
158
158
  }
159
+
160
+ /**
161
+ * `behaviorGate` for a value that is `null` when a host-sample seam is absent.
162
+ *
163
+ * Combines the gate with a TypeScript type predicate, because
164
+ * `behaviorGate(P, x !== null)` gates correctly but does NOT narrow `x` — and
165
+ * the alternative, sprinkling `!` at every use site, silently discards the
166
+ * null-safety that made the check worth writing.
167
+ *
168
+ * Semantics are exactly `behaviorGate`'s: an absent seam skips in default mode
169
+ * and FAILS under `OPENWOP_REQUIRE_BEHAVIOR=true`, because a host that
170
+ * advertises a capability and serves no seam has made a claim the suite cannot
171
+ * check. (RFC 0139 §"The G14 flip".)
172
+ */
173
+ export function behaviorGatePresent<T>(profileName: string, value: T | null | undefined): value is T {
174
+ return behaviorGate(profileName, value !== null && value !== undefined) && value !== null && value !== undefined;
175
+ }