@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,139 @@
1
+ /**
2
+ * Track 11 close-out: cross-run trace-context propagation across
3
+ * `core.subWorkflow` invocation.
4
+ *
5
+ * `otel-trace-propagation.test.ts` verifies that a single run's spans
6
+ * inherit an inbound `traceparent`'s traceId. This scenario closes the
7
+ * remaining gap (`conformance/coverage.md` row 52: "Cross-host
8
+ * propagation across `core.subWorkflow` invocation"): when a parent
9
+ * run with a known inbound traceparent dispatches a child run via
10
+ * `core.subWorkflow`, the CHILD run's emitted spans MUST also share
11
+ * the parent's traceId — distributed traces stitch across the
12
+ * sub-workflow boundary without operator-side correlation hacks.
13
+ *
14
+ * Operator-tier value: in production deployments, a sub-workflow may
15
+ * execute on a different host instance (`core.subWorkflow` is a
16
+ * dispatch boundary, not necessarily an in-process call). The
17
+ * traceparent-propagation contract guarantees the operator's OTel
18
+ * backend can render parent + child as one trace tree even when
19
+ * they're on separate hosts.
20
+ *
21
+ * Skip conditions:
22
+ * - Collector disabled.
23
+ * - Host doesn't advertise `capabilities.observability`.
24
+ * - `conformance-subworkflow-parent` fixture not advertised (host
25
+ * doesn't implement `core.subWorkflow`).
26
+ *
27
+ * @see spec/v1/observability.md §"Trace context propagation"
28
+ * @see spec/v1/node-packs.md §`core.subWorkflow`
29
+ * @see conformance/coverage.md row 52
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { driver } from '../lib/driver.js';
34
+ import { pollUntilTerminal } from '../lib/polling.js';
35
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
36
+ import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
37
+
38
+ const PARENT_FIXTURE = 'conformance-subworkflow-parent';
39
+
40
+ interface RunEvent {
41
+ type: string;
42
+ nodeId?: string;
43
+ payload?: { outputs?: { childRunId?: string } };
44
+ }
45
+
46
+ function makeTraceparent(): { header: string; traceId: string } {
47
+ // W3C format: 00-<32 hex traceId>-<16 hex spanId>-01.
48
+ // Use a distinct id from the parent-only scenario so collector
49
+ // matching is unambiguous when both scenarios run back-to-back.
50
+ const traceId = '7c3e51b9d2a04e6f8b1c0d2e3f4a5b6c';
51
+ const spanId = '00f067aa0ba902b7';
52
+ return { header: `00-${traceId}-${spanId}-01`, traceId };
53
+ }
54
+
55
+ async function isObservabilityAdvertised(): Promise<boolean> {
56
+ try {
57
+ const disco = await driver.get('/.well-known/openwop');
58
+ const caps = (disco.json as { capabilities?: { observability?: unknown } }).capabilities ?? {};
59
+ return caps.observability !== undefined;
60
+ } catch {
61
+ return false;
62
+ }
63
+ }
64
+
65
+ describe('otel-trace-propagation-subworkflow: traceparent threads parent → child via core.subWorkflow', () => {
66
+ it('child run spans inherit the parent run\'s inbound traceId', async () => {
67
+ if (!getCollector()) {
68
+ // eslint-disable-next-line no-console
69
+ console.warn('[otel-trace-propagation-subworkflow] collector not started; skipping');
70
+ return;
71
+ }
72
+ if (!isFixtureAdvertised(PARENT_FIXTURE)) {
73
+ // eslint-disable-next-line no-console
74
+ console.warn(`[otel-trace-propagation-subworkflow] ${PARENT_FIXTURE} not advertised; skipping`);
75
+ return;
76
+ }
77
+ if (!(await isObservabilityAdvertised())) {
78
+ // eslint-disable-next-line no-console
79
+ console.warn('[otel-trace-propagation-subworkflow] capabilities.observability not advertised; skipping');
80
+ return;
81
+ }
82
+
83
+ const collector = getCollector()!;
84
+ collector.reset();
85
+
86
+ const { header, traceId } = makeTraceparent();
87
+ const create = await driver.post(
88
+ '/v1/runs',
89
+ { workflowId: PARENT_FIXTURE },
90
+ { headers: { traceparent: header } },
91
+ );
92
+ expect(create.status).toBe(201);
93
+ const parentRunId = (create.json as { runId: string }).runId;
94
+
95
+ await pollUntilTerminal(parentRunId, { timeoutMs: 30_000 });
96
+
97
+ // Walk the parent's event log to discover the child run id.
98
+ const eventsRes = await driver.get(
99
+ `/v1/runs/${encodeURIComponent(parentRunId)}/events/poll?lastSequence=0&timeout=1`,
100
+ );
101
+ expect(eventsRes.status).toBe(200);
102
+ const events = (eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? [];
103
+
104
+ const subwfCompleted = events.find(
105
+ (e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
106
+ );
107
+ expect(subwfCompleted, driver.describe(
108
+ 'node-packs.md §core.subWorkflow',
109
+ 'parent event log MUST include node.completed for the subwf-call node',
110
+ )).toBeDefined();
111
+
112
+ const childRunId = subwfCompleted?.payload?.outputs?.childRunId;
113
+ expect(typeof childRunId, driver.describe(
114
+ 'node-packs.md §core.subWorkflow outputSchema',
115
+ 'subwf-call node.completed payload MUST carry outputs.childRunId',
116
+ )).toBe('string');
117
+
118
+ // Both parent + child spans MUST share the inbound traceId.
119
+ const parentSpans = await waitForRunSpans(parentRunId, { timeoutMs: 10_000, minCount: 1 });
120
+ const childSpans = await waitForRunSpans(childRunId!, { timeoutMs: 10_000, minCount: 1 });
121
+
122
+ expect(parentSpans.length, 'collector MUST receive ≥1 span for the parent run').toBeGreaterThan(0);
123
+ expect(childSpans.length, 'collector MUST receive ≥1 span for the child run').toBeGreaterThan(0);
124
+
125
+ const wantTrace = traceId.toLowerCase();
126
+
127
+ const parentMatching = parentSpans.filter((s) => s.traceId.toLowerCase() === wantTrace);
128
+ expect(parentMatching.length, driver.describe(
129
+ 'observability.md §"Trace context propagation"',
130
+ 'parent-run spans MUST share the inbound traceparent traceId',
131
+ )).toBeGreaterThan(0);
132
+
133
+ const childMatching = childSpans.filter((s) => s.traceId.toLowerCase() === wantTrace);
134
+ expect(childMatching.length, driver.describe(
135
+ 'observability.md §"Trace context propagation" + node-packs.md §core.subWorkflow',
136
+ 'child-run spans dispatched via core.subWorkflow MUST inherit the parent run\'s traceId so distributed traces stitch across the dispatch boundary',
137
+ )).toBeGreaterThan(0);
138
+ });
139
+ });
@@ -226,3 +226,46 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
226
226
  });
227
227
  });
228
228
  });
229
+
230
+ // CF-2 close-out — drain-policy discrimination per
231
+ // `capabilities.md` §`runs.pauseResume`. When a host advertises
232
+ // `drainPolicies[]`, each advertised value MUST be accepted with 202.
233
+ // Skips entirely when no advertisement is present.
234
+ describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities advertisement', () => {
235
+ it('every drainPolicy advertised by the host is accepted on :pause', async () => {
236
+ const disco = await driver.get('/.well-known/openwop');
237
+ const drainPolicies =
238
+ (disco.json as {
239
+ capabilities?: { runs?: { pauseResume?: { drainPolicies?: string[] } } };
240
+ }).capabilities?.runs?.pauseResume?.drainPolicies ?? [];
241
+ if (drainPolicies.length === 0) {
242
+ // eslint-disable-next-line no-console
243
+ console.warn('[pause-resume] host advertises no drainPolicies; skipping policy-discrimination subtest');
244
+ return;
245
+ }
246
+
247
+ for (const policy of drainPolicies) {
248
+ const create = await driver.post('/v1/runs', {
249
+ workflowId: FIXTURE!,
250
+ inputs: { delaySeconds: 30 },
251
+ });
252
+ expect(create.status).toBe(201);
253
+ const runId = (create.json as { runId: string }).runId;
254
+
255
+ await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
256
+
257
+ const pause = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {
258
+ reason: `conformance-drainpolicy-${policy}`,
259
+ drainPolicy: policy,
260
+ });
261
+ expect(pause.status, driver.describe(
262
+ 'capabilities.md §`runs.pauseResume.drainPolicies` + rest-endpoints.md POST /v1/runs/{runId}:pause',
263
+ `host-advertised drainPolicy='${policy}' MUST be accepted on :pause`,
264
+ )).toBe(202);
265
+
266
+ await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
267
+ reason: 'conformance-cleanup',
268
+ });
269
+ }
270
+ });
271
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * queue-ack-nack-dlq — RFC 0017 advertisement-shape verification + behavioral placeholders.
3
+ *
4
+ * Status: ACTIVE (advertisement-shape). RFC 0017 promoted to `Active`
5
+ * 2026-05-17. The matching `capabilities.queueBus` block has landed in
6
+ * `schemas/capabilities.schema.json`. This scenario asserts the advertisement
7
+ * shape against any host that boots the conformance suite, and keeps the
8
+ * deeper behavioral assertions as `it.todo()` until a reference host wires
9
+ * a test seam.
10
+ *
11
+ * Summary: nack returns for redelivery; deadLetter routes to the configured DLQ.
12
+ *
13
+ * @see RFCS/0017-*.md
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+
19
+ interface DiscoveryDoc {
20
+ capabilities?: Record<string, unknown>;
21
+ }
22
+
23
+ async function readCap(): Promise<Record<string, unknown> | null> {
24
+ const res = await driver.get('/.well-known/openwop');
25
+ const body = res.json as DiscoveryDoc | undefined;
26
+ const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
28
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
+ }
30
+
31
+ describe('queue-ack-nack-dlq: advertisement shape (RFC 0017)', () => {
32
+ it('capabilities.queueBus is either absent or a well-formed object', async () => {
33
+ const cap = await readCap();
34
+ if (cap === null) return; // host doesn't advertise — skip
35
+ expect(
36
+ typeof cap.supported,
37
+ driver.describe(
38
+ 'capabilities.schema.json §queueBus',
39
+ 'capabilities.queueBus.supported MUST be a boolean when present',
40
+ ),
41
+ ).toBe('boolean');
42
+ });
43
+
44
+ it('deadLetterSupported is a boolean when set', async () => {
45
+ const cap = await readCap();
46
+ if (!cap || cap.supported !== true) return;
47
+ const subParts = ["deadLetterSupported"];
48
+ let sub: unknown = cap;
49
+ for (const p of subParts) {
50
+ if (sub && typeof sub === 'object') sub = (sub as Record<string, unknown>)[p];
51
+ else { sub = undefined; break; }
52
+ }
53
+ if (sub === undefined) return; // optional sub-field
54
+ expect(
55
+ typeof sub,
56
+ driver.describe(
57
+ 'RFC 0017 §A',
58
+ 'queueBus.deadLetterSupported MUST be boolean when present',
59
+ ),
60
+ ).toBe('boolean');
61
+ });
62
+ });
63
+
64
+ describe('queue-ack-nack-dlq: behavioral assertions (placeholders — need host test seam)', () => {
65
+ it.todo("nack(requeue=true) → message is redelivered on next consume");
66
+ it.todo("deadLetter → message appears on the configured DLQ");
67
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * queue-cross-tenant-isolation — RFC 0017 §C + SECURITY/invariants.yaml
3
+ * `queue-cross-tenant-isolation`.
4
+ *
5
+ * Status: ACTIVE (advertisement + behavioral). Asserts that messages
6
+ * published under tenant A on topic T MUST NOT be consumed under tenant B
7
+ * on the same topic.
8
+ *
9
+ * @see RFCS/0017-host-queue-bus-capability.md
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+
15
+ interface DiscoveryDoc {
16
+ capabilities?: Record<string, unknown>;
17
+ }
18
+
19
+ async function readCap(): Promise<Record<string, unknown> | null> {
20
+ const res = await driver.get('/.well-known/openwop');
21
+ const body = res.json as DiscoveryDoc | undefined;
22
+ const top = body?.capabilities as Record<string, unknown> | undefined;
23
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
24
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
25
+ }
26
+
27
+ async function call(tenantId: string, op: string, args: Record<string, unknown>) {
28
+ return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'queueBus', op, args });
29
+ }
30
+
31
+ describe('queue-cross-tenant-isolation: advertisement shape (RFC 0017)', () => {
32
+ it('capabilities.queueBus is either absent or a well-formed object', async () => {
33
+ const cap = await readCap();
34
+ if (cap === null) return;
35
+ expect(
36
+ typeof cap.supported,
37
+ driver.describe(
38
+ 'capabilities.schema.json §queueBus',
39
+ 'capabilities.queueBus.supported MUST be a boolean when present',
40
+ ),
41
+ ).toBe('boolean');
42
+ });
43
+ });
44
+
45
+ describe('queue-cross-tenant-isolation: behavioral (RFC 0017 §C)', () => {
46
+ it('publish under tenant A → consume under tenant B returns not-found', async () => {
47
+ const cap = await readCap();
48
+ if (!cap || cap.supported !== true) return;
49
+ const topic = `xtenant.${Date.now()}.${Math.random().toString(36).slice(2, 6)}`;
50
+
51
+ const pubRes = await call('tenant-a', 'publish', { topic, payload: { hello: 'A' } });
52
+ if (pubRes.status === 404) return;
53
+ expect(pubRes.status, 'publish MUST succeed').toBe(200);
54
+
55
+ const consRes = await call('tenant-b', 'consume', { topic, timeoutMs: 100 });
56
+ expect(consRes.status).toBe(200);
57
+ const body = consRes.json as { found?: boolean };
58
+ expect(
59
+ body.found,
60
+ driver.describe(
61
+ 'SECURITY/invariants.yaml queue-cross-tenant-isolation',
62
+ 'tenant B MUST NOT consume tenant A messages on the same topic',
63
+ ),
64
+ ).toBe(false);
65
+ });
66
+ });
@@ -0,0 +1,48 @@
1
+ /**
2
+ * queue-publish-consume-roundtrip — RFC 0017 advertisement-shape verification + behavioral placeholders.
3
+ *
4
+ * Status: ACTIVE (advertisement-shape). RFC 0017 promoted to `Active`
5
+ * 2026-05-17. The matching `capabilities.queueBus` block has landed in
6
+ * `schemas/capabilities.schema.json`. This scenario asserts the advertisement
7
+ * shape against any host that boots the conformance suite, and keeps the
8
+ * deeper behavioral assertions as `it.todo()` until a reference host wires
9
+ * a test seam.
10
+ *
11
+ * Summary: publish + consume + ack roundtrip.
12
+ *
13
+ * @see RFCS/0017-*.md
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+
19
+ interface DiscoveryDoc {
20
+ capabilities?: Record<string, unknown>;
21
+ }
22
+
23
+ async function readCap(): Promise<Record<string, unknown> | null> {
24
+ const res = await driver.get('/.well-known/openwop');
25
+ const body = res.json as DiscoveryDoc | undefined;
26
+ const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
28
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
+ }
30
+
31
+ describe('queue-publish-consume-roundtrip: advertisement shape (RFC 0017)', () => {
32
+ it('capabilities.queueBus is either absent or a well-formed object', async () => {
33
+ const cap = await readCap();
34
+ if (cap === null) return; // host doesn't advertise — skip
35
+ expect(
36
+ typeof cap.supported,
37
+ driver.describe(
38
+ 'capabilities.schema.json §queueBus',
39
+ 'capabilities.queueBus.supported MUST be a boolean when present',
40
+ ),
41
+ ).toBe('boolean');
42
+ });
43
+ });
44
+
45
+ describe('queue-publish-consume-roundtrip: behavioral assertions (placeholders — need host test seam)', () => {
46
+ it.todo("publish → consume returns the message with the right payload + headers");
47
+ it.todo("ack removes the message; subsequent consume returns not-found within timeout");
48
+ });
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { describe, it, expect } from 'vitest';
29
+ import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
29
30
 
30
31
  const REGISTRY_BASE = 'https://packs.openwop.dev';
31
32
  const ENABLED = process.env.OPENWOP_TEST_PUBLIC_REGISTRY === 'true';
@@ -129,3 +130,93 @@ describe('registry-public: spec-canonical pack manifests resolve', () => {
129
130
  });
130
131
  }
131
132
  });
133
+
134
+ describe('registry-public: tarball + signature + Ed25519 verify roundtrip', () => {
135
+ // core.openwop.examples@1.0.0 is the canonical reference pack for this
136
+ // check: published since the registry MVP, signed with the
137
+ // `openwop-registry-root` key over the whole tarball (method='ed25519').
138
+ // The same recipe applies to any pack at the registry; this scenario
139
+ // exercises the worst-case full roundtrip so clients have a wire-level
140
+ // contract for verifying packs before installing them.
141
+ //
142
+ // What gets asserted, in order:
143
+ // 1. Version manifest, tarball, signature, and public key all 200.
144
+ // 2. SRI integrity in the manifest matches a fresh sha256 of the
145
+ // tarball bytes — protects against tarball tampering between
146
+ // publish + retrieval.
147
+ // 3. Detached Ed25519 signature verifies against the public key over
148
+ // the bytes the publisher signed (per signing.method).
149
+ const PACK_NAME = 'core.openwop.examples';
150
+ const PACK_VERSION = '1.0.0';
151
+
152
+ async function getBinary(path: string): Promise<{ status: number; bytes: Buffer }> {
153
+ const res = await fetch(`${REGISTRY_BASE}${path}`);
154
+ const ab = await res.arrayBuffer();
155
+ return { status: res.status, bytes: Buffer.from(ab) };
156
+ }
157
+
158
+ async function getText(path: string): Promise<{ status: number; body: string }> {
159
+ const res = await fetch(`${REGISTRY_BASE}${path}`);
160
+ const body = await res.text();
161
+ return { status: res.status, body };
162
+ }
163
+
164
+ it(`tarball + sig + public key all retrievable, SRI matches, Ed25519 verifies for ${PACK_NAME}@${PACK_VERSION}`, async () => {
165
+ if (!ENABLED) return;
166
+
167
+ // 1. Manifest (JSON).
168
+ const manifestRes = await get(`/v1/packs/${PACK_NAME}/-/${PACK_VERSION}.json`);
169
+ expect(manifestRes.status).toBe(200);
170
+ const manifest = manifestRes.json as {
171
+ signing?: { method?: string; keyId?: string; publicKeyUrl?: string };
172
+ integrity?: string;
173
+ };
174
+ expect(typeof manifest.signing?.method).toBe('string');
175
+ expect(typeof manifest.signing?.keyId).toBe('string');
176
+ expect(typeof manifest.integrity).toBe('string');
177
+
178
+ // 2. Tarball.
179
+ const tarball = await getBinary(`/v1/packs/${PACK_NAME}/-/${PACK_VERSION}.tgz`);
180
+ expect(tarball.status, 'tarball MUST be retrievable').toBe(200);
181
+ expect(tarball.bytes.byteLength, 'tarball MUST be non-empty').toBeGreaterThan(0);
182
+
183
+ // 3. Detached signature (64-byte raw Ed25519).
184
+ const sig = await getBinary(`/v1/packs/${PACK_NAME}/-/${PACK_VERSION}.sig`);
185
+ expect(sig.status, 'signature MUST be retrievable').toBe(200);
186
+ expect(sig.bytes.byteLength, 'Ed25519 detached signature MUST be 64 bytes').toBe(64);
187
+
188
+ // 4. Public key — fetch from the publisher-declared URL when present,
189
+ // else fall back to the canonical `/keys/<keyId>.pub` shape.
190
+ const keyUrl = manifest.signing!.publicKeyUrl ?? `/keys/${manifest.signing!.keyId}.pub`;
191
+ const keyRes = await getText(keyUrl);
192
+ expect(keyRes.status, `public key MUST be retrievable at ${keyUrl}`).toBe(200);
193
+ const publicKey = createPublicKey(keyRes.body);
194
+
195
+ // 5. SRI integrity check: `sha256-<base64>=` MUST match a fresh
196
+ // sha256 of the tarball bytes per registry-operations.md.
197
+ expect(manifest.integrity).toMatch(/^sha256-[A-Za-z0-9+/]+=*$/);
198
+ const expectedSri = `sha256-${createHash('sha256').update(tarball.bytes).digest('base64')}`;
199
+ expect(expectedSri, 'SRI integrity in manifest MUST match a fresh sha256 of the tarball').toBe(
200
+ manifest.integrity,
201
+ );
202
+
203
+ // 6. Ed25519 verification. Two canonical signing conventions per
204
+ // `node-packs.md` §"Signing recipe": `method=ed25519` signs the
205
+ // whole tarball; `method=manual` signs the pack.json bytes inside
206
+ // the tarball. core.openwop.examples uses `ed25519`.
207
+ const method = manifest.signing!.method;
208
+ expect(['ed25519', 'manual']).toContain(method);
209
+
210
+ // For `ed25519` the signed bytes are the tarball; for `manual` the
211
+ // signed bytes are pack.json extracted from the tarball. This
212
+ // scenario picks `core.openwop.examples` specifically because it's
213
+ // `method=ed25519` — the simpler path. Extending to `manual` would
214
+ // require the tarball extractor from registry/scripts/verify-
215
+ // signatures.mjs which is intentionally out of scope here.
216
+ if (method !== 'ed25519') return;
217
+
218
+ const verified = cryptoVerify(null, tarball.bytes, publicKey, sig.bytes);
219
+ expect(verified, `Ed25519 signature over ${PACK_NAME}@${PACK_VERSION}.tgz MUST verify against ${manifest.signing!.keyId}`)
220
+ .toBe(true);
221
+ });
222
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * search-bm25-roundtrip — RFC 0018 advertisement-shape verification + behavioral placeholders.
3
+ *
4
+ * Status: ACTIVE (advertisement-shape). RFC 0018 promoted to `Active`
5
+ * 2026-05-17. The matching `capabilities.searchIndex` block has landed in
6
+ * `schemas/capabilities.schema.json`. This scenario asserts the advertisement
7
+ * shape against any host that boots the conformance suite, and keeps the
8
+ * deeper behavioral assertions as `it.todo()` until a reference host wires
9
+ * a test seam.
10
+ *
11
+ * Summary: index then query returns relevant documents.
12
+ *
13
+ * @see RFCS/0018-*.md
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+
19
+ interface DiscoveryDoc {
20
+ capabilities?: Record<string, unknown>;
21
+ }
22
+
23
+ async function readCap(): Promise<Record<string, unknown> | null> {
24
+ const res = await driver.get('/.well-known/openwop');
25
+ const body = res.json as DiscoveryDoc | undefined;
26
+ const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["searchIndex"] : undefined;
28
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
+ }
30
+
31
+ describe('search-bm25-roundtrip: advertisement shape (RFC 0018)', () => {
32
+ it('capabilities.searchIndex is either absent or a well-formed object', async () => {
33
+ const cap = await readCap();
34
+ if (cap === null) return; // host doesn't advertise — skip
35
+ expect(
36
+ typeof cap.supported,
37
+ driver.describe(
38
+ 'capabilities.schema.json §searchIndex',
39
+ 'capabilities.searchIndex.supported MUST be a boolean when present',
40
+ ),
41
+ ).toBe('boolean');
42
+ });
43
+ });
44
+
45
+ describe('search-bm25-roundtrip: behavioral assertions (placeholders — need host test seam)', () => {
46
+ it.todo("index 3 docs → query returns relevance-ranked hits");
47
+ });
@@ -69,7 +69,23 @@ import {
69
69
  // ── Helpers ─────────────────────────────────────────────────────────────
70
70
 
71
71
  function listJsonFiles(dir: string): string[] {
72
- return readdirSync(dir).filter((f) => f.endsWith('.json'));
72
+ // Recurse into subdirectories so e.g. `schemas/envelopes/*.schema.json`
73
+ // appears as `envelopes/<file>` to match the README's path-prefixed
74
+ // table entries. Preserves the non-recursive-relative-output contract
75
+ // for files directly under `dir`.
76
+ const out: string[] = [];
77
+ const walk = (subPath: string): void => {
78
+ const fullPath = subPath === '' ? dir : `${dir}/${subPath}`;
79
+ for (const entry of readdirSync(fullPath, { withFileTypes: true })) {
80
+ if (entry.isDirectory()) {
81
+ walk(subPath === '' ? entry.name : `${subPath}/${entry.name}`);
82
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
83
+ out.push(subPath === '' ? entry.name : `${subPath}/${entry.name}`);
84
+ }
85
+ }
86
+ };
87
+ walk('');
88
+ return out;
73
89
  }
74
90
 
75
91
  function listScenarioTestFiles(dir: string): string[] {
@@ -789,11 +805,14 @@ describe.skipIf(
789
805
  () => {
790
806
  // describe.skipIf still evaluates the body for test registration; defaults guard against null
791
807
  // dirname() when sources are missing under the published-tarball layout. it() blocks below are
792
- // skipped at run time, so the path values are never actually read.
808
+ // skipped at run time, so the path values are never actually read. The sentinel is
809
+ // intentionally an obviously-invalid path so a stack trace from any future code that DOES
810
+ // dereference it points the reader at this comment.
811
+ const UNUSED_IN_PUBLISHED_LAYOUT = '/__sdk_paths_unused_in_published_layout__';
793
812
  const sdkSources = {
794
- typescript: TYPESCRIPT_RUN_HELPERS_PATH ?? '.',
795
- python: PYTHON_TYPES_PATH ?? '.',
796
- go: GO_TYPES_PATH ?? '.',
813
+ typescript: TYPESCRIPT_RUN_HELPERS_PATH ?? UNUSED_IN_PUBLISHED_LAYOUT,
814
+ python: PYTHON_TYPES_PATH ?? UNUSED_IN_PUBLISHED_LAYOUT,
815
+ go: GO_TYPES_PATH ?? UNUSED_IN_PUBLISHED_LAYOUT,
797
816
  };
798
817
  const sdkReadmes = {
799
818
  typescript: pathResolve(dirname(sdkSources.typescript), '..', 'README.md'),
@@ -1145,8 +1164,10 @@ describe.skipIf(README_PATH === null)('spec-corpus: local Markdown links resolve
1145
1164
 
1146
1165
  const target = pathResolve(dirname(file), decoded);
1147
1166
  // Published-tarball layout: the conformance README references ../spec/v1/... and other paths
1148
- // that resolve OUTSIDE the package boundary. Repo layout has the full tree available.
1149
- if (LAYOUT === 'published' && !target.startsWith(repoRoot)) continue;
1167
+ // that resolve OUTSIDE the package boundary. Repo layout has the full tree available. The
1168
+ // `target === repoRoot || target.startsWith(repoRoot + sep)` form avoids a sibling-path
1169
+ // false-negative when repoRoot=/foo/bar and target=/foo/barbaz.
1170
+ if (LAYOUT === 'published' && target !== repoRoot && !target.startsWith(repoRoot + '/')) continue;
1150
1171
  expect(
1151
1172
  existsSync(target),
1152
1173
  `${relFile} links to missing local target: ${link}`,
@@ -0,0 +1,84 @@
1
+ /**
2
+ * sql-injection-rejection — RFC 0018 §C + SECURITY/invariants.yaml
3
+ * `sql-parametric-only`.
4
+ *
5
+ * Status: ACTIVE (advertisement + behavioral). The host's SQL surface
6
+ * MUST treat parameter values as literal data, not SQL fragments. We
7
+ * verify by binding an injection-shape string as a parameter and
8
+ * confirming it returns no rows (parametric binding turns it into a
9
+ * literal value comparison rather than an OR-true).
10
+ *
11
+ * @see RFCS/0018-host-sql-vector-search-capability.md
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { driver } from '../lib/driver.js';
16
+
17
+ interface DiscoveryDoc {
18
+ capabilities?: Record<string, unknown>;
19
+ }
20
+
21
+ async function readCap(): Promise<Record<string, unknown> | null> {
22
+ const res = await driver.get('/.well-known/openwop');
23
+ const body = res.json as DiscoveryDoc | undefined;
24
+ const top = body?.capabilities as Record<string, unknown> | undefined;
25
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["sql"] : undefined;
26
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
27
+ }
28
+
29
+ async function call(op: string, args: Record<string, unknown>) {
30
+ return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'sql', op, args });
31
+ }
32
+
33
+ describe('sql-injection-rejection: advertisement shape (RFC 0018)', () => {
34
+ it('capabilities.sql is either absent or a well-formed object', async () => {
35
+ const cap = await readCap();
36
+ if (cap === null) return;
37
+ expect(
38
+ typeof cap.supported,
39
+ driver.describe(
40
+ 'capabilities.schema.json §sql',
41
+ 'capabilities.sql.supported MUST be a boolean when present',
42
+ ),
43
+ ).toBe('boolean');
44
+ });
45
+ });
46
+
47
+ describe('sql-injection-rejection: behavioral (RFC 0018 §C)', () => {
48
+ it('parametric SELECT with bound user input rejects injection-shape strings as data', async () => {
49
+ const cap = await readCap();
50
+ if (!cap || cap.supported !== true) return;
51
+
52
+ const create = await call('execute', {
53
+ sql: `CREATE TABLE IF NOT EXISTS sql_inj_t (id TEXT PRIMARY KEY, body TEXT)`,
54
+ params: [],
55
+ });
56
+ if (create.status === 404) return;
57
+ await call('execute', { sql: `INSERT OR REPLACE INTO sql_inj_t VALUES (?, ?)`, params: ['k1', 'ok'] });
58
+
59
+ // Parametric round-trip MUST succeed.
60
+ const okRes = await call('query', {
61
+ sql: `SELECT body FROM sql_inj_t WHERE id = ?`,
62
+ params: ['k1'],
63
+ });
64
+ expect(okRes.status).toBe(200);
65
+ const okBody = okRes.json as { rows?: Array<Record<string, unknown>> };
66
+ expect(okBody.rows?.[0]?.body, 'parametric round-trip MUST return stored value').toBe('ok');
67
+
68
+ // Injection-shape input MUST be bound as a literal value, not SQL.
69
+ const attack = `' OR '1'='1`;
70
+ const attackRes = await call('query', {
71
+ sql: `SELECT body FROM sql_inj_t WHERE id = ?`,
72
+ params: [attack],
73
+ });
74
+ expect(attackRes.status).toBe(200);
75
+ const attackBody = attackRes.json as { rows?: Array<Record<string, unknown>> };
76
+ expect(
77
+ Array.isArray(attackBody.rows) ? attackBody.rows.length : -1,
78
+ driver.describe(
79
+ 'SECURITY/invariants.yaml sql-parametric-only',
80
+ 'parametric binding MUST treat injection-shape input as a literal value, not SQL',
81
+ ),
82
+ ).toBe(0);
83
+ });
84
+ });