@openwop/openwop-conformance 1.51.0 → 1.53.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.
@@ -0,0 +1,129 @@
1
+ {
2
+ "name": "vendor.openwop.workflow-chain-sample",
3
+ "version": "1.0.0",
4
+ "kind": "workflow-chain",
5
+ "description": "Reference workflow-chain pack proving the RFC 0013 manifest format works end-to-end across schema validation, registry indexing, and the reference expansion library. Two chains: a 1-node 'summarize text' chain showing literal parameter substitution, and a 2-node 'fetch-and-summarize' chain showing multi-node DAG composition with edge wiring and capability propagation. In-tree only (NOT published to packs.openwop.dev); the canonical proof that the chain-pack contract is implementable with real-world-shaped content.",
6
+ "author": "openwop project",
7
+ "license": "Apache-2.0",
8
+ "homepage": "https://openwop.dev/spec/v1/workflow-chain-packs.md",
9
+ "repository": "https://github.com/openwop/openwop",
10
+ "engines": {
11
+ "openwop": ">=1.1 <2.0.0"
12
+ },
13
+ "chains": [
14
+ {
15
+ "chainId": "vendor.openwop.workflow-chain-sample.summarize-text",
16
+ "version": "1.0.0",
17
+ "label": "Summarize Text",
18
+ "description": "Drag-tile that expands to a single core.ai.callPrompt with the summarization system prompt + author-supplied length / tone parameters substituted in. Mirrors the 'Generate PRD' positive example from workflow-chain-packs.md §Examples — the canonical 1-node chain shape.",
19
+ "parameters": {
20
+ "type": "object",
21
+ "required": ["sourceText"],
22
+ "properties": {
23
+ "sourceText": {
24
+ "type": "string",
25
+ "description": "The text to summarize. Surfaced via {{params.sourceText}} placeholder in the system prompt."
26
+ },
27
+ "targetLength": {
28
+ "type": "string",
29
+ "enum": ["one-sentence", "one-paragraph", "executive-summary"],
30
+ "default": "one-paragraph",
31
+ "description": "How long the summary should be."
32
+ },
33
+ "tone": {
34
+ "type": "string",
35
+ "default": "neutral",
36
+ "description": "Tone for the summary (e.g., 'casual', 'technical', 'executive')."
37
+ }
38
+ }
39
+ },
40
+ "dag": {
41
+ "nodes": [
42
+ {
43
+ "id": "summarize-call",
44
+ "typeId": "core.ai.callPrompt",
45
+ "name": "Summarize",
46
+ "position": { "x": 0, "y": 0 },
47
+ "config": {
48
+ "systemPrompt": "You are an expert summarizer. Produce a {{params.targetLength}} summary in a {{params.tone}} tone. Avoid lists unless the source uses them. Avoid hedging.\n\nSource text:\n{{params.sourceText}}",
49
+ "envelopeType": "summary.v1",
50
+ "provider": "anthropic"
51
+ },
52
+ "inputs": {}
53
+ }
54
+ ],
55
+ "edges": []
56
+ },
57
+ "outputs": {
58
+ "summaryText": {
59
+ "type": "string",
60
+ "description": "The summarized text from the AI call's envelope payload."
61
+ }
62
+ },
63
+ "capabilities": ["cacheable"]
64
+ },
65
+ {
66
+ "chainId": "vendor.openwop.workflow-chain-sample.fetch-and-summarize",
67
+ "version": "1.0.0",
68
+ "label": "Fetch &amp; Summarize URL",
69
+ "description": "Drag-tile that expands to a two-node DAG: core.openwop.http.get fetches the URL, then core.ai.callPrompt summarizes the response body. Demonstrates multi-node chain composition with edge wiring, parameter substitution across multiple nodes, and capability propagation (side-effectful from the HTTP fetch propagates to both expanded nodes per spec §Capability propagation).",
70
+ "parameters": {
71
+ "type": "object",
72
+ "required": ["url"],
73
+ "properties": {
74
+ "url": {
75
+ "type": "string",
76
+ "format": "uri",
77
+ "description": "The URL to fetch. Surfaced via {{params.url}} in the HTTP node config."
78
+ },
79
+ "targetLength": {
80
+ "type": "string",
81
+ "enum": ["one-sentence", "one-paragraph", "executive-summary"],
82
+ "default": "one-paragraph"
83
+ }
84
+ }
85
+ },
86
+ "dag": {
87
+ "nodes": [
88
+ {
89
+ "id": "fetch",
90
+ "typeId": "core.openwop.http.get",
91
+ "name": "Fetch URL",
92
+ "position": { "x": 0, "y": 0 },
93
+ "config": {
94
+ "url": "{{params.url}}",
95
+ "timeoutMs": 30000
96
+ },
97
+ "inputs": {}
98
+ },
99
+ {
100
+ "id": "summarize",
101
+ "typeId": "core.ai.callPrompt",
102
+ "name": "Summarize",
103
+ "position": { "x": 280, "y": 0 },
104
+ "config": {
105
+ "systemPrompt": "Produce a {{params.targetLength}} summary of the fetched page content. Strip navigation, ads, and boilerplate. Focus on the article body.",
106
+ "envelopeType": "summary.v1",
107
+ "provider": "anthropic"
108
+ },
109
+ "inputs": {}
110
+ }
111
+ ],
112
+ "edges": [
113
+ { "from": "fetch.body", "to": "summarize.sourceText" }
114
+ ]
115
+ },
116
+ "outputs": {
117
+ "summaryText": {
118
+ "type": "string",
119
+ "description": "The summarized page content."
120
+ },
121
+ "sourceUrl": {
122
+ "type": "string",
123
+ "description": "Echo of the input URL for downstream reference."
124
+ }
125
+ },
126
+ "capabilities": ["side-effectful"]
127
+ }
128
+ ]
129
+ }
@@ -0,0 +1,17 @@
1
+ {
2
+ "source": "change",
3
+ "subscriptionId": "sub_13",
4
+ "deliveryId": "dlv_a7b8",
5
+ "dedupKey": "contacts:lsn:0/1C4F9D0",
6
+ "receivedAt": "2026-07-06T12:01:00Z",
7
+ "verified": true,
8
+ "contentTrust": "untrusted",
9
+ "permittedPurposes": ["analytics"],
10
+ "change": {
11
+ "op": "update",
12
+ "table": "contacts",
13
+ "changelogId": "0/1C4F9D0",
14
+ "before": { "id": 77, "tier": "free" },
15
+ "after": { "id": 77, "tier": "pro" }
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "source": "stream",
3
+ "subscriptionId": "sub_12",
4
+ "deliveryId": "dlv_e5f6",
5
+ "dedupKey": "events:3:88412",
6
+ "receivedAt": "2026-07-06T12:00:00Z",
7
+ "verified": true,
8
+ "contentTrust": "untrusted",
9
+ "stream": {
10
+ "topic": "events",
11
+ "partition": 3,
12
+ "offset": "88412",
13
+ "key": "user_77",
14
+ "message": { "type": "page_view", "path": "/pricing" }
15
+ }
16
+ }
package/fixtures.md CHANGED
@@ -482,11 +482,12 @@ Each JSON is a valid `WorkflowDefinition` per `../schemas/workflow-definition.sc
482
482
 
483
483
  ## Pack-manifest fixtures
484
484
 
485
- The `fixtures/pack-manifests/` sub-directory holds canonical pack manifests used as schema-level proof points (validated server-free against `../schemas/node-pack-manifest.schema.json`). They are NOT seeded into a server — they exist to assert the canonical schema accepts each documented pack-name scope.
485
+ The `fixtures/pack-manifests/` sub-directory holds canonical pack manifests used as schema-level proof points (validated server-free against `../schemas/node-pack-manifest.schema.json`, or `../schemas/workflow-chain-pack-manifest.schema.json` for `kind: "workflow-chain"`). Most are NOT seeded into a server — they assert the canonical schema accepts each documented pack-name scope. The `workflow-chain-sample.pack` fixture is the exception: it is the **bundled, host-syncable** `vendor.openwop.workflow-chain-sample` pack that `workflow-chain-host-expansion.test.ts` loads (and that a live host serving the RFC 0013 `/v1/host/sample/workflow-chain:expand` seam vendors) — so the published pack is the single contract source for that live-host witness.
486
486
 
487
- | Fixture | `name` | Purpose |
488
- | ---------------------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
489
- | `pack-private-example` | `private.example-host.example-tools` | Asserts the v1.0 pack-name pattern accepts the `private.<host>.*` scope reserved for host-internal registries. |
487
+ | Fixture | `name` | Purpose |
488
+ | --------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
489
+ | `pack-private-example` | `private.example-host.example-tools` | Asserts the v1.0 pack-name pattern accepts the `private.<host>.*` scope reserved for host-internal registries. |
490
+ | `workflow-chain-sample.pack` | `vendor.openwop.workflow-chain-sample` | RFC 0013 `kind: "workflow-chain"` reference pack (2 chains: 1-node `summarize-text` + 2-node `fetch-and-summarize`). **Bundled + host-syncable** (2026-07-05 erratum A-lite): `workflow-chain-host-expansion.test.ts` loads it and derives the expected expansion from the `expandChain()` reference library, so a serving host's `/v1/host/sample/workflow-chain:expand` output is checked against the spec algorithm for the identical published pack — no hardcoded expansion. |
490
491
 
491
492
  Pack-manifest fixtures are exercised by the server-free `fixtures-valid.test.ts` scenarios — adding one runs the schema validator against it automatically.
492
493
 
@@ -512,6 +513,8 @@ The `fixtures/trigger-events/` sub-directory holds canonical external-event inge
512
513
  | Fixture | Schema | Purpose |
513
514
  | -------------------------------------------- | --------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
514
515
  | `trigger-event-email` | `trigger-event.schema.json` | Canonical positive `source:"email"` TriggerEvent — `verified`, `contentTrust:"untrusted"`, an `AttachmentRef` with a host-internal `ref`. Drives the §F.1 one-of, AttachmentRef, and contentTrust shape assertions. |
516
+ | `trigger-event-stream` | `trigger-event.schema.json` | Canonical positive `source:"stream"` TriggerEvent (RFC 0127) — broker coordinates (`topic`/`partition`/`offset`/`key`) + `message` body, dedup key composed from the coordinates. Drives the stream-side schema assertions in `trigger-stream-cdc-sources.test.ts`. |
517
+ | `trigger-event-change` | `trigger-event.schema.json` | Canonical positive `source:"change"` TriggerEvent (RFC 0127) — REQUIRED `op:"update"` + `table`/`changelogId`/`before`/`after` row images, plus a `permittedPurposes` label (RFC 0128). Drives the op-REQUIRED negative + the label shape assertions in `trigger-stream-cdc-sources.test.ts` / `purpose-propagation.test.ts`. |
515
518
  | `trigger-subscription-registration-email` | `trigger-subscription-registration.schema.json` | Canonical positive registration (`source:"email"`, `workflowId`, `verification.mode:"required"`). Drives the registration shape assertions. |
516
519
 
517
520
  Negative cases (cross-source sub-objects, raw attachment URLs, `Authorization` headers) are inline test data in `trigger-ingestion.test.ts` per suite convention — a deliberately-invalid fixture file would fail the automatic sweep.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.51.0",
3
+ "version": "1.53.0",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -470,6 +470,22 @@
470
470
  }
471
471
  }
472
472
  },
473
+ "purposePropagation": {
474
+ "type": "object",
475
+ "additionalProperties": false,
476
+ "description": "RFC 0128 (`Active`). Purpose-propagation — the host reads and RE-EMITS `permittedPurposes` labels (opaque purpose strings riding the A2A `metadata.openwop.permittedPurposes` extension and the `TriggerEvent.permittedPurposes` field) on onward OpenWOP-envelope hops of the same data, narrowing at most and never widening; a `[]` label is honored as no-onward-use. This family advertises label PROPAGATION only — it does NOT claim the host enforces purpose limits on its own internal use (that is declared intent under the receiver's local governance, RFC 0128 §4, deliberately not conformance-gated). A host MUST NOT advertise `supported: true` until RFC 0128 reaches `Accepted` and propagation is honored (truthful-advertisement, `OPENWOP_REQUIRE_BEHAVIOR=true`). Absent block ⇒ labels pass through unread as unknown metadata (pre-RFC behavior).",
477
+ "required": ["supported"],
478
+ "properties": {
479
+ "supported": {
480
+ "type": "boolean",
481
+ "description": "RFC 0128. When `true`, the host preserves and re-emits received `permittedPurposes` labels per RFC 0128 §3 (re-emit on OpenWOP-envelope onward hops; MAY narrow; MUST NOT widen; derived outputs never carry a purpose absent from any contributing labelled input; `[]` ⇒ no onward hop)."
482
+ },
483
+ "propagatesOnward": {
484
+ "type": "boolean",
485
+ "description": "RFC 0128. When `true`, the host's onward OpenWOP-envelope hops (A2A forwards, trigger/sync events to OpenWOP peers) re-emit the label. Distinct from `supported` only for hosts that read labels but have no onward-hop surface at all; a host WITH onward hops advertising `supported: true` MUST also propagate."
486
+ }
487
+ }
488
+ },
473
489
  "credentials": {
474
490
  "type": "object",
475
491
  "description": "RFC 0046 (`Draft`). Portable credential resolution + lifecycle contract — sibling to `secrets`, first-class store-at-rest + workspace sharing + two-key-overlap rotation. A pack references a credential by `{ ref, scope }` (see `credential-reference.schema.json`); the host resolves it into the node sandbox ONLY — never into inputs, persisted variables, channels, any run.* event payload, the debug bundle, or replay state (SECURITY invariant `credential-payload-redaction`). Supersedes the informal BYOK annex; the `secrets` advertisement stays valid.",
@@ -1699,13 +1715,13 @@
1699
1715
  "backoff": { "type": "string", "enum": ["none", "fixed", "exponential"], "description": "Backoff strategy between attempts." }
1700
1716
  }
1701
1717
  },
1702
- "sources": { "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": ["webhook", "schedule", "queue", "email", "form"] }, "description": "Which trigger sources bridge uniformly. A source listed here MUST have a registerable `TriggerSubscription` driven through the four-state machine AND emit the two `trigger.*` events for that source — the list MUST NOT over-claim a source the host has as a feature but does not wire as a durable trigger subscription. A consumer MUST tolerate any subset." },
1718
+ "sources": { "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": ["webhook", "schedule", "queue", "email", "form", "stream", "change"] }, "description": "Which trigger sources bridge uniformly. A source listed here MUST have a registerable `TriggerSubscription` driven through the four-state machine AND emit the two `trigger.*` events for that source — the list MUST NOT over-claim a source the host has as a feature but does not wire as a durable trigger subscription. A consumer MUST tolerate any subset." },
1703
1719
  "ingestion": {
1704
1720
  "type": "object",
1705
1721
  "additionalProperties": false,
1706
- "description": "RFC 0099 §F.3 (additive). External-event ingestion advertisement — which of `sources[]` the host actually ingests from EXTERNALLY-originated events (`webhook`/`email`/`form`), normalizing each to a `TriggerEvent` (`trigger-event.schema.json`) and starting a run. Absent ⇒ the host does NOT externally-ingest (today's behavior — schedule/queue only). A source in `externalSources[]` MUST actually accept an external event, normalize it, and start a run — over-claiming is a dishonest advertisement. A consumer MUST tolerate any subset.",
1722
+ "description": "RFC 0099 §F.3 (additive). External-event ingestion advertisement — which of `sources[]` the host actually ingests from EXTERNALLY-originated events (`webhook`/`email`/`form`/`stream`/`change`), normalizing each to a `TriggerEvent` (`trigger-event.schema.json`) and starting a run. Absent ⇒ the host does NOT externally-ingest (today's behavior — schedule/queue only). A source in `externalSources[]` MUST actually accept an external event, normalize it, and start a run — over-claiming is a dishonest advertisement. A consumer MUST tolerate any subset.",
1707
1723
  "properties": {
1708
- "externalSources": { "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": ["webhook", "email", "form"] }, "description": "Which of `sources[]` are EXTERNALLY ingested per RFC 0099. The honesty gate — each MUST normalize to a `TriggerEvent` and start a run." },
1724
+ "externalSources": { "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": ["webhook", "email", "form", "stream", "change"] }, "description": "Which of `sources[]` are EXTERNALLY ingested per RFC 0099 (`stream`/`change` per RFC 0127). The honesty gate — each MUST normalize to a `TriggerEvent` and start a run." },
1709
1725
  "maxBodyBytes": { "type": "integer", "minimum": 1, "description": "Inbound body cap (webhook body / email / form), reusing the RFC 0076 §B response-cap discipline." },
1710
1726
  "verification": { "type": "array", "uniqueItems": true, "items": { "type": "string", "enum": ["webhook-signature", "email-dmarc", "form-origin"] }, "description": "Which source-authenticity checks the host performs. An advertised check MUST actually be performed (RFC 0099 §F.3 / UQ1)." },
1711
1727
  "registrationEndpoint": { "type": "boolean", "description": "`true` ⇒ the host serves `POST /v1/trigger-subscriptions` (RFC 0099 §F.2) for portable external-event subscription creation." }
@@ -1739,7 +1739,7 @@
1739
1739
  "required": ["subscriptionId", "source", "fromState", "toState"],
1740
1740
  "properties": {
1741
1741
  "subscriptionId": { "type": "string", "minLength": 1, "description": "The TriggerSubscription this state change is for." },
1742
- "source": { "type": "string", "enum": ["webhook", "schedule", "queue", "email", "form"], "description": "The subscription source." },
1742
+ "source": { "type": "string", "enum": ["webhook", "schedule", "queue", "email", "form", "stream", "change"], "description": "The subscription source." },
1743
1743
  "fromState": { "type": "string", "enum": ["active", "paused", "failed", "dead-lettered"], "description": "Prior state." },
1744
1744
  "toState": { "type": "string", "enum": ["active", "paused", "failed", "dead-lettered"], "description": "New state." },
1745
1745
  "reason": { "type": "string", "enum": ["retry-exhausted", "operator-paused", "signature-invalid", "backpressure", "source-removed", "provenance-unevaluable"], "description": "MAY — a machine-stable reason code (a CLOSED enum — a free-form reason is a leak vector that would let a host spill an inbound URL/host/header into it). The load-bearing field is `toState`." }
@@ -2,15 +2,15 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://openwop.dev/spec/v1/trigger-event.schema.json",
4
4
  "title": "TriggerEvent",
5
- "description": "RFC 0099 §F.1. The normalized external-event envelope a host hands a started run as `ctx.triggerData` when an externally-originated event (`webhook`/`email`/`form`) is delivered to an `active` TriggerSubscription (RFC 0083). This is an IN-RUN payload only — it never appears on the durable event log; the `trigger.delivery.attempted` event stays content-free (RFC 0083 §C / SECURITY `trigger-ingestion-content-redaction`). A TriggerEvent carries exactly the per-source sub-object matching its `source`. All content is `untrusted` (SECURITY `threat-model-prompt-injection.md`).",
5
+ "description": "RFC 0099 §F.1. The normalized external-event envelope a host hands a started run as `ctx.triggerData` when an externally-originated event (`webhook`/`email`/`form`/`stream`/`change`) is delivered to an `active` TriggerSubscription (RFC 0083). This is an IN-RUN payload only — it never appears on the durable event log; the `trigger.delivery.attempted` event stays content-free (RFC 0083 §C / SECURITY `trigger-ingestion-content-redaction`). A TriggerEvent carries exactly the per-source sub-object matching its `source`. All content is `untrusted` (SECURITY `threat-model-prompt-injection.md`).",
6
6
  "type": "object",
7
7
  "additionalProperties": false,
8
8
  "required": ["source", "subscriptionId", "deliveryId", "receivedAt"],
9
9
  "properties": {
10
10
  "source": {
11
11
  "type": "string",
12
- "enum": ["webhook", "email", "form"],
13
- "description": "Which external source originated the event. A TriggerEvent MUST carry exactly the per-source sub-object matching this value and MUST NOT carry the others (RFC 0099 §F.1)."
12
+ "enum": ["webhook", "email", "form", "stream", "change"],
13
+ "description": "Which external source originated the event. A TriggerEvent MUST carry exactly the per-source sub-object matching this value and MUST NOT carry the others (RFC 0099 §F.1; `stream`/`change` per RFC 0127)."
14
14
  },
15
15
  "subscriptionId": {
16
16
  "type": "string",
@@ -40,23 +40,38 @@
40
40
  "enum": ["untrusted"],
41
41
  "description": "Always `untrusted`. Inbound external content reaching an LLM node MUST be wrapped per `threat-model-prompt-injection.md` §UNTRUSTED; a TriggerEvent MUST NOT directly advance a HITL approval gate (the `prompt-injection-mcp-no-approval` invariant generalizes)."
42
42
  },
43
+ "permittedPurposes": {
44
+ "type": "array",
45
+ "items": { "type": "string" },
46
+ "description": "RFC 0128 — OPTIONAL declared uses the receiver is permitted to make of the accompanying data. Opaque purpose strings mapped to the host's local vocabulary; carries purpose categories only, never subject identifiers or consent records. Absent ⇒ unlabelled (no constraint asserted); `[]` ⇒ no onward use permitted (NOT the same as absent). A host advertising `purposePropagation` MUST honor the RFC 0128 §3 propagation rules for this label."
47
+ },
43
48
  "webhook": { "$ref": "#/$defs/WebhookEvent" },
44
49
  "email": { "$ref": "#/$defs/EmailEvent" },
45
- "form": { "$ref": "#/$defs/FormEvent" }
50
+ "form": { "$ref": "#/$defs/FormEvent" },
51
+ "stream": { "$ref": "#/$defs/StreamEvent" },
52
+ "change": { "$ref": "#/$defs/ChangeEvent" }
46
53
  },
47
54
  "allOf": [
48
55
  {
49
56
  "description": "RFC 0099 §F.1 — a TriggerEvent MUST carry exactly the per-source sub-object matching its `source` and MUST NOT carry the others.",
50
57
  "if": { "properties": { "source": { "const": "webhook" } } },
51
- "then": { "not": { "anyOf": [{ "required": ["email"] }, { "required": ["form"] }] } }
58
+ "then": { "not": { "anyOf": [{ "required": ["email"] }, { "required": ["form"] }, { "required": ["stream"] }, { "required": ["change"] }] } }
52
59
  },
53
60
  {
54
61
  "if": { "properties": { "source": { "const": "email" } } },
55
- "then": { "not": { "anyOf": [{ "required": ["webhook"] }, { "required": ["form"] }] } }
62
+ "then": { "not": { "anyOf": [{ "required": ["webhook"] }, { "required": ["form"] }, { "required": ["stream"] }, { "required": ["change"] }] } }
56
63
  },
57
64
  {
58
65
  "if": { "properties": { "source": { "const": "form" } } },
59
- "then": { "not": { "anyOf": [{ "required": ["webhook"] }, { "required": ["email"] }] } }
66
+ "then": { "not": { "anyOf": [{ "required": ["webhook"] }, { "required": ["email"] }, { "required": ["stream"] }, { "required": ["change"] }] } }
67
+ },
68
+ {
69
+ "if": { "properties": { "source": { "const": "stream" } } },
70
+ "then": { "not": { "anyOf": [{ "required": ["webhook"] }, { "required": ["email"] }, { "required": ["form"] }, { "required": ["change"] }] } }
71
+ },
72
+ {
73
+ "if": { "properties": { "source": { "const": "change" } } },
74
+ "then": { "not": { "anyOf": [{ "required": ["webhook"] }, { "required": ["email"] }, { "required": ["form"] }, { "required": ["stream"] }] } }
60
75
  }
61
76
  ],
62
77
  "$defs": {
@@ -98,6 +113,31 @@
98
113
  "files": { "type": "array", "items": { "$ref": "#/$defs/AttachmentRef" } }
99
114
  }
100
115
  },
116
+ "StreamEvent": {
117
+ "type": "object",
118
+ "additionalProperties": false,
119
+ "description": "Present iff `source == \"stream\"` (RFC 0127). One message consumed from a streaming broker (Kafka / Kinesis / Pub-Sub); the host is the consumer and each delivered message normalizes to one TriggerEvent. The broker connection is a credential-brokered egress (RFC 0095), never a wire field.",
120
+ "properties": {
121
+ "topic": { "type": "string", "description": "The broker topic/stream/subscription name the message was consumed from." },
122
+ "partition": { "type": "integer", "minimum": 0, "description": "Broker partition/shard, where the broker has one. With `offset`, feeds the RFC 0127 §4 dedup-key guidance `(topic, partition, offset)`." },
123
+ "offset": { "type": "string", "description": "Broker offset / sequence number as an opaque string (brokers disagree on integer width)." },
124
+ "key": { "type": "string", "description": "The message key, where the broker has one." },
125
+ "message": { "description": "The consumed message payload — parsed JSON, or a string for non-JSON. Bounded by `triggerBridge.ingestion.maxBodyBytes`." }
126
+ }
127
+ },
128
+ "ChangeEvent": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "required": ["op"],
132
+ "description": "Present iff `source == \"change\"` (RFC 0127). One change-data-capture record from a warehouse/database changelog. `op` is REQUIRED. The warehouse connection is a credential-brokered egress (RFC 0095), never a wire field.",
133
+ "properties": {
134
+ "op": { "type": "string", "enum": ["insert", "update", "delete"], "description": "The change operation. REQUIRED (RFC 0127 §2 / resolved Q1 — one `change` source with an `op` discriminator, not per-verb sources)." },
135
+ "table": { "type": "string", "description": "The source table/collection/entity name. With `changelogId`, feeds the RFC 0127 §4 dedup-key guidance `(table, changelog-id)`." },
136
+ "changelogId": { "type": "string", "description": "The changelog position/LSN/row-version as an opaque string." },
137
+ "before": { "description": "The pre-image row where the feed provides one (typically `update`/`delete`). Parsed JSON. Bounded by `triggerBridge.ingestion.maxBodyBytes`." },
138
+ "after": { "description": "The post-image row where the feed provides one (typically `insert`/`update`). Parsed JSON. Bounded by `triggerBridge.ingestion.maxBodyBytes`." }
139
+ }
140
+ },
101
141
  "AttachmentRef": {
102
142
  "type": "object",
103
143
  "additionalProperties": false,
@@ -144,6 +184,39 @@
144
184
  "headers": { "X-Event-Type": "issue.created" },
145
185
  "body": { "issue": { "id": 42, "title": "Bug" } }
146
186
  }
187
+ },
188
+ {
189
+ "source": "stream",
190
+ "subscriptionId": "sub_12",
191
+ "deliveryId": "dlv_e5f6",
192
+ "dedupKey": "events:3:88412",
193
+ "receivedAt": "2026-07-06T12:00:00Z",
194
+ "verified": true,
195
+ "contentTrust": "untrusted",
196
+ "stream": {
197
+ "topic": "events",
198
+ "partition": 3,
199
+ "offset": "88412",
200
+ "key": "user_77",
201
+ "message": { "type": "page_view", "path": "/pricing" }
202
+ }
203
+ },
204
+ {
205
+ "source": "change",
206
+ "subscriptionId": "sub_13",
207
+ "deliveryId": "dlv_a7b8",
208
+ "dedupKey": "contacts:lsn:0/1C4F9D0",
209
+ "receivedAt": "2026-07-06T12:01:00Z",
210
+ "verified": true,
211
+ "contentTrust": "untrusted",
212
+ "permittedPurposes": ["analytics"],
213
+ "change": {
214
+ "op": "update",
215
+ "table": "contacts",
216
+ "changelogId": "0/1C4F9D0",
217
+ "before": { "id": 77, "tier": "free" },
218
+ "after": { "id": 77, "tier": "pro" }
219
+ }
147
220
  }
148
221
  ]
149
222
  }
@@ -9,7 +9,7 @@
9
9
  "properties": {
10
10
  "source": {
11
11
  "type": "string",
12
- "enum": ["webhook", "email", "form"],
12
+ "enum": ["webhook", "email", "form", "stream", "change"],
13
13
  "description": "The external source this subscription ingests. MUST appear in the host's `capabilities.triggerBridge.ingestion.externalSources[]`."
14
14
  },
15
15
  "workflowId": {
@@ -8,7 +8,7 @@
8
8
  "required": ["subscriptionId", "source", "state"],
9
9
  "properties": {
10
10
  "subscriptionId": { "type": "string", "minLength": 1, "description": "Stable host-unique id for the subscription. Correlates the §C delivery events + the management surface." },
11
- "source": { "type": "string", "enum": ["webhook", "schedule", "queue", "email", "form"], "description": "Which trigger source backs the subscription. Channels beyond these (Slack/Discord/SMS) bridge as a vendor extension by registering a subscription of the closest source kind (§E)." },
11
+ "source": { "type": "string", "enum": ["webhook", "schedule", "queue", "email", "form", "stream", "change"], "description": "Which trigger source backs the subscription. Channels beyond these (Slack/Discord/SMS) bridge as a vendor extension by registering a subscription of the closest source kind (§E)." },
12
12
  "state": { "type": "string", "enum": ["active", "paused", "failed", "dead-lettered"], "description": "The §B state. `active`: accepting + delivering; `paused`: retained, not delivering (operator-held); `failed`: delivery failing past policy (the webhooks.md circuit-breaker generalized); `dead-lettered`: terminal, deliveries routed to the RFC 0053 sink." },
13
13
  "dedupEnabled": { "type": "boolean", "description": "When true, the host de-duplicates inbound events by `dedupKey` within the retention window (§C-1; the idempotency.md Layer-1 model applied to inbound triggers)." },
14
14
  "retryPolicy": {
@@ -111,7 +111,7 @@
111
111
  "type": "object",
112
112
  "required": ["id", "typeId", "name", "position", "config", "inputs"],
113
113
  "properties": {
114
- "id": { "type": "string", "minLength": 1 },
114
+ "id": { "type": "string", "minLength": 1, "description": "Node identifier, unique within the workflow. Intentionally has NO `maxLength` — RFC 0013 chain expansion mints ids of the form `${chainIdSlug}_${expansionId}_${fragmentId}` where `chainIdSlug` is an unbounded reverse-DNS string, so an expanded node id can exceed any short cap. A host MUST NOT reject or truncate a node id by length (no host-imposed `maxLength` stricter than this schema)." },
115
115
  "typeId": {
116
116
  "type": "string",
117
117
  "description": "Canonical node type ID (e.g., 'core.ai.callPrompt', 'core.chat.approvalGate'). Reserved prefixes: 'core.*' for spec-canonical, 'vendor.<org>.*' for third-party.",
@@ -126,17 +126,33 @@ export class ChainUnresolvableTypeIdError extends Error {
126
126
 
127
127
  const PARAM_PATTERN = /\{\{params\.([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
128
128
  /** A value that is EXACTLY a single `{{params.<name>}}` token (whole-value),
129
- * distinct from a token embedded in a larger string. Whole-value tokens are
130
- * the only non-prompt position deferrable to a variable-sourced PortValue
131
- * (WCP2 raw-typed rule); an embedded non-prompt token has no runtime `{{}}`
132
- * construct and MUST resolve at expansion time. */
129
+ * distinct from a token embedded in a larger string. Under expansion-time
130
+ * substitution a whole-value token resolves to the RAW TYPED parameter value
131
+ * rather than a string coercion (WCP2 raw-typed rule); under deferred mode it
132
+ * is the only non-prompt position deferrable to a variable-sourced PortValue.
133
+ * An embedded non-prompt token has no runtime `{{}}` construct and MUST resolve
134
+ * at expansion time. */
133
135
  const WHOLE_VALUE_PATTERN = /^\{\{params\.([a-zA-Z_][a-zA-Z0-9_]*)\}\}$/;
134
136
 
135
- /** Recursive literal substitution of `{{params.<name>}}` placeholders in
136
- * any string field. Non-string values pass through unchanged; nested
137
- * arrays/objects are walked. */
137
+ /** Recursive substitution of `{{params.<name>}}` placeholders in any string
138
+ * field. Two cases per `workflow-chain-packs.md` §"Parameter substitution":
139
+ * - WHOLE-VALUE: a string that is exactly one `{{params.x}}` token resolves
140
+ * to the RAW typed parameter value (object / array / number / boolean
141
+ * survive as their JSON type instead of being stringified).
142
+ * - EMBEDDED: one or more tokens inside surrounding text do literal string
143
+ * substitution.
144
+ * Non-string values pass through unchanged; nested arrays/objects are walked. */
138
145
  function substitute(value: unknown, params: Record<string, unknown>): unknown {
139
146
  if (typeof value === 'string') {
147
+ const whole = WHOLE_VALUE_PATTERN.exec(value);
148
+ if (whole) {
149
+ // Whole-value token — return the raw typed value so a param typed
150
+ // `object` / `array` / `number` / `boolean` reaches the node config
151
+ // intact. `undefined` (undeclared param) collapses to the empty string,
152
+ // matching the embedded-token convention below.
153
+ const v = params[whole[1] as string];
154
+ return v === undefined ? '' : v;
155
+ }
140
156
  return value.replace(PARAM_PATTERN, (_match, name: string) => {
141
157
  const v = params[name];
142
158
  // Per the spec, parameter values are validated against the chain's
@@ -110,6 +110,12 @@ describe('fixtures: node-pack-manifest schema validity', () => {
110
110
  ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
111
111
  const schema = JSON.parse(readFileSync(PACK_MANIFEST_SCHEMA_PATH, 'utf8'));
112
112
  const validate = ajv.compile(schema);
113
+ // A `pack-manifests/` fixture may be a node pack OR a `kind: "workflow-chain"`
114
+ // pack (RFC 0013) — pick the schema by `kind`, exactly as the registry does.
115
+ const chainSchema = JSON.parse(
116
+ readFileSync(join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json'), 'utf8'),
117
+ );
118
+ const validateChain = ajv.compile(chainSchema);
113
119
 
114
120
  const files = readdirSync(PACK_MANIFEST_FIXTURES_DIR)
115
121
  .filter((f) => f.endsWith('.json'))
@@ -120,17 +126,22 @@ describe('fixtures: node-pack-manifest schema validity', () => {
120
126
  });
121
127
 
122
128
  for (const file of files) {
123
- it(`pack-manifests/${file} validates against node-pack-manifest.schema.json`, () => {
129
+ it(`pack-manifests/${file} validates against its kind's manifest schema`, () => {
124
130
  const data = JSON.parse(
125
131
  readFileSync(join(PACK_MANIFEST_FIXTURES_DIR, file), 'utf8'),
126
- );
127
- const ok = validate(data);
128
- const errors = (validate.errors ?? [])
132
+ ) as { kind?: string };
133
+ const isChain = data.kind === 'workflow-chain';
134
+ const v = isChain ? validateChain : validate;
135
+ const schemaName = isChain
136
+ ? 'workflow-chain-pack-manifest.schema.json'
137
+ : 'node-pack-manifest.schema.json';
138
+ const ok = v(data);
139
+ const errors = (v.errors ?? [])
129
140
  .map((e: ErrorObject) => `${e.instancePath || '/'}: ${e.message}`)
130
141
  .join('\n');
131
142
  expect(
132
143
  ok,
133
- `Fixture pack-manifests/${file} fails node-pack-manifest schema:\n${errors}`,
144
+ `Fixture pack-manifests/${file} fails ${schemaName}:\n${errors}`,
134
145
  ).toBe(true);
135
146
  });
136
147
  }