@openwop/openwop-conformance 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -2
  3. package/coverage.md +29 -17
  4. package/fixtures/conformance-agent-low-confidence.json +7 -4
  5. package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
  6. package/fixtures/conformance-agent-reasoning.json +23 -4
  7. package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
  8. package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
  9. package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
  10. package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
  11. package/fixtures/conformance-dispatch-input-mapping.json +49 -0
  12. package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
  13. package/fixtures/conformance-dispatch-output-mapping.json +49 -0
  14. package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
  15. package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
  16. package/fixtures.md +12 -2
  17. package/package.json +1 -1
  18. package/schemas/README.md +7 -0
  19. package/schemas/agent-ref.schema.json +1 -1
  20. package/schemas/ai-envelope.schema.json +106 -0
  21. package/schemas/capabilities.schema.json +300 -3
  22. package/schemas/core-conformance-mock-agent-config.schema.json +147 -0
  23. package/schemas/dispatch-config.schema.json +26 -0
  24. package/schemas/envelopes/clarification.request.schema.json +43 -0
  25. package/schemas/envelopes/error.schema.json +26 -0
  26. package/schemas/envelopes/schema.request.schema.json +22 -0
  27. package/schemas/envelopes/schema.response.schema.json +22 -0
  28. package/schemas/node-pack-manifest.schema.json +5 -0
  29. package/schemas/pack-lockfile.schema.json +16 -0
  30. package/schemas/run-event-payloads.schema.json +18 -2
  31. package/schemas/run-event.schema.json +2 -1
  32. package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
  33. package/src/lib/behavior-gate.ts +44 -5
  34. package/src/lib/env.ts +27 -0
  35. package/src/lib/webhook-receiver.ts +137 -0
  36. package/src/lib/workflow-chain-expansion.ts +213 -0
  37. package/src/scenarios/agentPackCatalog.test.ts +216 -0
  38. package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
  39. package/src/scenarios/agentReasoningEvents.test.ts +58 -7
  40. package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
  41. package/src/scenarios/ai-envelope-shape.test.ts +362 -0
  42. package/src/scenarios/aiEnvelope.capBreached.test.ts +173 -0
  43. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
  44. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
  45. package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
  46. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
  47. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
  48. package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -0
  49. package/src/scenarios/append-ordering.test.ts +44 -0
  50. package/src/scenarios/artifact-auth.test.ts +58 -0
  51. package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
  52. package/src/scenarios/blob-presign-expiry.test.ts +66 -0
  53. package/src/scenarios/blob-roundtrip.test.ts +48 -0
  54. package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
  55. package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
  56. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
  57. package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
  58. package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
  59. package/src/scenarios/fs-path-traversal.test.ts +124 -0
  60. package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
  61. package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
  62. package/src/scenarios/kv-atomic-increment.test.ts +74 -0
  63. package/src/scenarios/kv-cas.test.ts +75 -0
  64. package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
  65. package/src/scenarios/kv-ttl-expiry.test.ts +47 -0
  66. package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
  67. package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
  68. package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
  69. package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
  70. package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
  71. package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
  72. package/src/scenarios/mcp-tool-roundtrip.test.ts +13 -6
  73. package/src/scenarios/memory-compaction-event-emitted.test.ts +121 -0
  74. package/src/scenarios/memory-compaction-provenance-tag.test.ts +116 -0
  75. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +127 -0
  76. package/src/scenarios/multi-region-idempotency.test.ts +39 -4
  77. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +139 -0
  78. package/src/scenarios/pause-resume.test.ts +43 -0
  79. package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
  80. package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
  81. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
  82. package/src/scenarios/registry-public.test.ts +91 -0
  83. package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
  84. package/src/scenarios/spec-corpus-validity.test.ts +28 -7
  85. package/src/scenarios/sql-injection-rejection.test.ts +84 -0
  86. package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
  87. package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
  88. package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
  89. package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
  90. package/src/scenarios/table-cursor-pagination.test.ts +47 -0
  91. package/src/scenarios/table-schema-enforcement.test.ts +47 -0
  92. package/src/scenarios/vector-knn-roundtrip.test.ts +48 -0
  93. package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
  94. package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
  95. package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
  96. package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
  97. package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Workflow-chain pack signature verification — `workflow-chain-packs.md`
3
+ * §"Expansion semantics" step 2 + `node-packs.md` §Signing (reused
4
+ * verification flow, identical to node packs).
5
+ *
6
+ * Server-free scenario. Asserts that the Ed25519 signature verification
7
+ * recipe from `node-packs.md` §Signing — "`pack.json.sig` is an Ed25519
8
+ * signature over `pack.json` using the key at `keys/<key-id>.pem`" —
9
+ * works unchanged for workflow-chain pack manifests. The spec's design
10
+ * intent is that workflow-chain packs reuse 100% of the node-pack
11
+ * signing infrastructure (same algorithm, same byte recipe, same key
12
+ * format); this scenario proves the reuse is real, not just claimed.
13
+ *
14
+ * What this scenario covers:
15
+ * - A valid (manifest + signature) pair verifies cleanly.
16
+ * - A tampered manifest fails verification with the same shape of
17
+ * failure as a tampered node pack would produce.
18
+ * - A wrong-key signature fails verification.
19
+ *
20
+ * Why this matters: any host implementing chain expansion MUST verify
21
+ * the source pack's signature before resolving + expanding (step 2 of
22
+ * the expansion algorithm). If chain packs needed a different signing
23
+ * recipe than node packs, implementers would need two verification
24
+ * paths — that's a footgun. The spec explicitly designs them to share.
25
+ *
26
+ * @see spec/v1/workflow-chain-packs.md §"Expansion semantics" step 2
27
+ * @see spec/v1/node-packs.md §Signing
28
+ * @see RFCS/0013-workflow-chain-packs.md
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { generateKeyPairSync, sign, verify } from 'node:crypto';
33
+
34
+ /** Canonical workflow-chain pack manifest used as the signing target.
35
+ * Mirrors the spec doc's Positive example, kept tight so the test
36
+ * focuses on the signing path, not the manifest shape. */
37
+ const MANIFEST = {
38
+ name: 'vendor.acme.editor-presets',
39
+ version: '1.0.0',
40
+ kind: 'workflow-chain',
41
+ description: 'Sample workflow-chain pack used by signature-verification conformance.',
42
+ engines: { openwop: '>=1.0.0 <2.0.0' },
43
+ chains: [
44
+ {
45
+ chainId: 'vendor.acme.generatePRD',
46
+ version: '1.0.0',
47
+ label: 'Generate PRD',
48
+ description: 'Single-node AI call.',
49
+ parameters: { type: 'object', properties: { productIdea: { type: 'string' } } },
50
+ dag: {
51
+ nodes: [{ id: 'prd-call', typeId: 'core.ai.callPrompt', config: {} }],
52
+ edges: [],
53
+ },
54
+ },
55
+ ],
56
+ };
57
+
58
+ /** Canonical byte serialization of the manifest used for signing.
59
+ * Matches the recipe in node-packs.md §Signing: "Ed25519 signature
60
+ * over `pack.json`" — the on-disk JSON bytes ARE the signing payload.
61
+ * Production tooling would use the EXACT bytes from `pack.json` in the
62
+ * tarball (preserved whitespace, byte-for-byte) so the verification
63
+ * recipe is deterministic. */
64
+ function manifestBytes(): Buffer {
65
+ return Buffer.from(JSON.stringify(MANIFEST, null, 2));
66
+ }
67
+
68
+ describe('category: workflow-chain pack signature — Ed25519 verification reuse from node-packs', () => {
69
+ it('valid manifest + valid signature MUST verify (positive path)', () => {
70
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519');
71
+ const manifest = manifestBytes();
72
+ // Per node-packs.md §Signing: Ed25519, no separate hash step (Ed25519
73
+ // signs the message directly). The `algorithm` arg to crypto.sign is
74
+ // `null` for Ed25519 — confirmed by Node's docs.
75
+ const signature = sign(null, manifest, privateKey);
76
+ const ok = verify(null, manifest, publicKey, signature);
77
+ expect(
78
+ ok,
79
+ 'Per workflow-chain-packs.md §"Expansion semantics" step 2: the signature recipe is IDENTICAL to node-packs (Ed25519 over pack.json bytes). A valid pair MUST verify with the canonical recipe.',
80
+ ).toBe(true);
81
+ });
82
+
83
+ it('tampered manifest MUST fail verification (sha-level tamper detection)', () => {
84
+ const { privateKey, publicKey } = generateKeyPairSync('ed25519');
85
+ const original = manifestBytes();
86
+ const signature = sign(null, original, privateKey);
87
+ // Tamper: change a single byte in the manifest after signing. This
88
+ // simulates a registry-side or man-in-the-middle modification — the
89
+ // signing recipe MUST detect ANY byte-level change.
90
+ const tampered = Buffer.from(original);
91
+ tampered[10] = tampered[10]! ^ 0x01;
92
+ const ok = verify(null, tampered, publicKey, signature);
93
+ expect(
94
+ ok,
95
+ 'Per node-packs.md §Signing (reused unchanged for chain packs): Ed25519 verification MUST detect any byte-level tamper to the signed payload.',
96
+ ).toBe(false);
97
+ });
98
+
99
+ it('wrong-key signature MUST fail verification', () => {
100
+ const { privateKey: legitPrivKey } = generateKeyPairSync('ed25519');
101
+ const { publicKey: attackerPubKey } = generateKeyPairSync('ed25519');
102
+ const manifest = manifestBytes();
103
+ const signature = sign(null, manifest, legitPrivKey);
104
+ // Try to verify with a DIFFERENT public key than the one paired with
105
+ // the signing private key. This is the supply-chain attack the
106
+ // signing recipe defends against (attacker substitutes their own
107
+ // key in the manifest's `signing.publicKeyRef`).
108
+ const ok = verify(null, manifest, attackerPubKey, signature);
109
+ expect(
110
+ ok,
111
+ 'Ed25519 verification MUST fail when the public key doesn\'t match the private key that produced the signature — the spec\'s trust model depends on this property.',
112
+ ).toBe(false);
113
+ });
114
+
115
+ it('chain pack manifests carry the SAME signing block shape as node packs', () => {
116
+ // node-packs.md §signing declares `pack.json` MAY carry a `signing`
117
+ // block with `publicKeyRef` / `signatureRef` / `method`. Chain packs
118
+ // reuse the same shape unchanged — workflow-chain-pack-manifest.
119
+ // schema.json's `$defs.Signing` is byte-identical to the node-pack
120
+ // schema's `$defs.Signing`. This assertion documents the spec
121
+ // design intent so future schema evolution doesn't silently
122
+ // diverge.
123
+ const signedManifest = {
124
+ ...MANIFEST,
125
+ signing: {
126
+ publicKeyRef: 'keys/2026-05.pem',
127
+ signatureRef: 'pack.json.sig',
128
+ method: 'manual' as const,
129
+ },
130
+ };
131
+ expect(
132
+ signedManifest.signing.method,
133
+ 'workflow-chain packs MUST accept the same signing block keys as node packs (publicKeyRef + signatureRef + method) per spec design.',
134
+ ).toBe('manual');
135
+ expect(signedManifest.signing.publicKeyRef).toBe('keys/2026-05.pem');
136
+ expect(signedManifest.signing.signatureRef).toBe('pack.json.sig');
137
+ });
138
+ });
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Workflow-chain expansion — unresolvable typeId rejection per
3
+ * `workflow-chain-packs.md` §"Expansion semantics (normative)" step 3
4
+ * + `workflow-chain-packs.md` §"Error codes" `chain_unresolvable_typeid`.
5
+ *
6
+ * Server-free scenario. Asserts that when a chain's `dag.nodes[].typeId`
7
+ * references an UNPUBLISHED typeId (one the destination host's pack
8
+ * registry can't resolve), expansion is rejected with
9
+ * `chain_unresolvable_typeid` BEFORE any node ids are rewritten or
10
+ * placeholders substituted.
11
+ *
12
+ * The "rejection at expansion time" path is load-bearing for the
13
+ * "additive — no new dispatch surface" invariant: if expansion produced
14
+ * a workflow referencing an unknown typeId, the dispatching engine
15
+ * would fail at run time with `unknown_typeid` and the user would see
16
+ * a confusing failure far from the chain-pack tile they dragged.
17
+ * Rejecting at edit time keeps the failure local to the author's
18
+ * action.
19
+ *
20
+ * Per the spec's "Negative: chain references unpublished typeId"
21
+ * example: the pack's manifest validation does NOT cross-check
22
+ * published typeId existence (cycle issues + registry-availability
23
+ * concerns); only the host-editor-time expansion step verifies — by
24
+ * which point the destination host's pack registry has authoritative
25
+ * knowledge of which typeIds it can resolve.
26
+ *
27
+ * @see spec/v1/workflow-chain-packs.md §"Expansion semantics" step 3
28
+ * @see spec/v1/workflow-chain-packs.md §"Error codes"
29
+ * @see conformance/src/lib/workflow-chain-expansion.ts
30
+ * @see RFCS/0013-workflow-chain-packs.md
31
+ */
32
+
33
+ import { describe, it, expect } from 'vitest';
34
+ import {
35
+ expandChain,
36
+ ChainUnresolvableTypeIdError,
37
+ type WorkflowChain,
38
+ } from '../lib/workflow-chain-expansion.js';
39
+
40
+ const CHAIN_WITH_UNKNOWN_TYPEID: WorkflowChain = {
41
+ chainId: 'vendor.acme.someChain',
42
+ version: '1.0.0',
43
+ label: 'Some Chain',
44
+ description: 'References an unpublished typeId — should fail expansion.',
45
+ parameters: {},
46
+ dag: {
47
+ nodes: [
48
+ { id: 'n1', typeId: 'made.up.foo', config: {} },
49
+ ],
50
+ edges: [],
51
+ },
52
+ };
53
+
54
+ /** Resolver that only accepts a hardcoded set of "known" typeIds — the
55
+ * set the destination host's pack registry can satisfy. Mimics the
56
+ * realistic host-editor-time check. */
57
+ const KNOWN_TYPEIDS = new Set(['core.identity', 'core.ai.callPrompt']);
58
+ const isKnown = (typeId: string): boolean => KNOWN_TYPEIDS.has(typeId);
59
+
60
+ describe('category: workflow-chain expansion — unresolvable typeId rejection', () => {
61
+ it('throws ChainUnresolvableTypeIdError when chain references an unknown typeId', () => {
62
+ expect(
63
+ () =>
64
+ expandChain(CHAIN_WITH_UNKNOWN_TYPEID, {
65
+ expansionId: 'x',
66
+ params: {},
67
+ isTypeIdResolvable: isKnown,
68
+ }),
69
+ 'Per spec §"Expansion semantics" step 3: hosts MUST reject expansion with `chain_unresolvable_typeid` when any fragment node\'s typeId can\'t be resolved by the destination host.',
70
+ ).toThrow(ChainUnresolvableTypeIdError);
71
+ });
72
+
73
+ it('error carries the offending typeId AND the chainId in `details`', () => {
74
+ try {
75
+ expandChain(CHAIN_WITH_UNKNOWN_TYPEID, {
76
+ expansionId: 'x',
77
+ params: {},
78
+ isTypeIdResolvable: isKnown,
79
+ });
80
+ expect.fail('expandChain MUST have thrown ChainUnresolvableTypeIdError');
81
+ } catch (err) {
82
+ expect(err, 'expected ChainUnresolvableTypeIdError').toBeInstanceOf(
83
+ ChainUnresolvableTypeIdError,
84
+ );
85
+ const e = err as ChainUnresolvableTypeIdError;
86
+ expect(
87
+ e.code,
88
+ 'Per spec §"Error codes": the wire-level error code MUST be `chain_unresolvable_typeid`.',
89
+ ).toBe('chain_unresolvable_typeid');
90
+ expect(
91
+ e.typeId,
92
+ 'The error MUST surface the offending typeId so the host editor can render an actionable diagnostic.',
93
+ ).toBe('made.up.foo');
94
+ expect(
95
+ e.chainId,
96
+ 'The error MUST surface the chainId so the host editor knows which chain-pack tile triggered the rejection.',
97
+ ).toBe('vendor.acme.someChain');
98
+ }
99
+ });
100
+
101
+ it('rejection happens BEFORE any id rewriting or placeholder substitution', () => {
102
+ // If the spec allowed expansion to proceed and produce a half-built
103
+ // fragment with an unknown typeId, downstream tooling would see a
104
+ // workflow with an undispatchable node. The contract is "reject
105
+ // cleanly at the resolution step, no partial output."
106
+ let threw = false;
107
+ try {
108
+ expandChain(CHAIN_WITH_UNKNOWN_TYPEID, {
109
+ expansionId: 'x',
110
+ params: { foo: 'bar' },
111
+ isTypeIdResolvable: isKnown,
112
+ });
113
+ } catch {
114
+ threw = true;
115
+ }
116
+ expect(
117
+ threw,
118
+ 'Expansion MUST throw before producing any output — host editors rely on the "no partial expansion" guarantee to keep parent workflows clean on rejection.',
119
+ ).toBe(true);
120
+ });
121
+
122
+ it('accepts the chain when every typeId IS resolvable', () => {
123
+ const resolvable: WorkflowChain = {
124
+ ...CHAIN_WITH_UNKNOWN_TYPEID,
125
+ dag: {
126
+ nodes: [{ id: 'n1', typeId: 'core.identity', config: {} }],
127
+ edges: [],
128
+ },
129
+ };
130
+ expect(
131
+ () =>
132
+ expandChain(resolvable, {
133
+ expansionId: 'x',
134
+ params: {},
135
+ isTypeIdResolvable: isKnown,
136
+ }),
137
+ 'Sanity: when every typeId resolves, expansion MUST proceed without throwing.',
138
+ ).not.toThrow();
139
+ });
140
+
141
+ it('throws on the FIRST unknown typeId encountered (fail-fast)', () => {
142
+ // Chain with two nodes — one resolvable, one not. The unresolvable one
143
+ // is second. The throw MUST identify the second one (the actual
144
+ // offender), not silently skip it.
145
+ const mixed: WorkflowChain = {
146
+ ...CHAIN_WITH_UNKNOWN_TYPEID,
147
+ dag: {
148
+ nodes: [
149
+ { id: 'n1', typeId: 'core.identity', config: {} },
150
+ { id: 'n2', typeId: 'made.up.foo', config: {} },
151
+ ],
152
+ edges: [],
153
+ },
154
+ };
155
+ try {
156
+ expandChain(mixed, {
157
+ expansionId: 'x',
158
+ params: {},
159
+ isTypeIdResolvable: isKnown,
160
+ });
161
+ expect.fail('MUST have thrown on n2\'s unknown typeId');
162
+ } catch (err) {
163
+ const e = err as ChainUnresolvableTypeIdError;
164
+ expect(
165
+ e.typeId,
166
+ 'When multiple nodes have unknown typeIds, the throw MUST identify the first unknown one encountered in declaration order.',
167
+ ).toBe('made.up.foo');
168
+ }
169
+ });
170
+ });