@openwop/openwop-conformance 1.1.1 → 1.3.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 (109) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +2 -2
  3. package/api/redocly.yaml +15 -0
  4. package/coverage.md +27 -14
  5. package/fixtures/conformance-agent-low-confidence.json +7 -4
  6. package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
  7. package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
  8. package/fixtures/conformance-agent-reasoning.json +23 -4
  9. package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
  10. package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
  11. package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
  12. package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
  13. package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
  14. package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
  15. package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
  16. package/fixtures/conformance-dispatch-input-mapping.json +49 -0
  17. package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
  18. package/fixtures/conformance-dispatch-output-mapping.json +49 -0
  19. package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
  20. package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
  21. package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
  22. package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
  23. package/fixtures.md +18 -2
  24. package/package.json +1 -1
  25. package/schemas/README.md +7 -0
  26. package/schemas/agent-ref.schema.json +1 -1
  27. package/schemas/ai-envelope.schema.json +106 -0
  28. package/schemas/capabilities.schema.json +264 -0
  29. package/schemas/core-conformance-mock-agent-config.schema.json +152 -0
  30. package/schemas/dispatch-config.schema.json +26 -0
  31. package/schemas/envelopes/clarification.request.schema.json +43 -0
  32. package/schemas/envelopes/error.schema.json +26 -0
  33. package/schemas/envelopes/schema.request.schema.json +22 -0
  34. package/schemas/envelopes/schema.response.schema.json +22 -0
  35. package/schemas/node-pack-manifest.schema.json +5 -0
  36. package/schemas/pack-lockfile.schema.json +16 -0
  37. package/schemas/run-event-payloads.schema.json +35 -1
  38. package/schemas/run-event.schema.json +2 -0
  39. package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
  40. package/src/lib/driver.ts +15 -0
  41. package/src/lib/env.ts +51 -0
  42. package/src/lib/event-log-query.ts +62 -0
  43. package/src/lib/fixtures.ts +38 -1
  44. package/src/lib/host-toggle.ts +54 -0
  45. package/src/lib/multi-agent-capabilities.ts +10 -0
  46. package/src/lib/otel-scrape.ts +59 -0
  47. package/src/lib/webhook-receiver.ts +137 -0
  48. package/src/lib/workflow-chain-expansion.ts +213 -0
  49. package/src/scenarios/agentPackCatalog.test.ts +216 -0
  50. package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
  51. package/src/scenarios/agentReasoningEvents.test.ts +58 -7
  52. package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
  53. package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
  54. package/src/scenarios/ai-envelope-shape.test.ts +362 -0
  55. package/src/scenarios/aiEnvelope.capBreached.test.ts +261 -0
  56. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +268 -0
  57. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +284 -0
  58. package/src/scenarios/aiEnvelope.redaction.test.ts +253 -0
  59. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +226 -0
  60. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +194 -0
  61. package/src/scenarios/aiEnvelope.universalKinds.test.ts +267 -0
  62. package/src/scenarios/append-ordering.test.ts +44 -0
  63. package/src/scenarios/artifact-auth.test.ts +58 -0
  64. package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
  65. package/src/scenarios/blob-presign-expiry.test.ts +99 -0
  66. package/src/scenarios/blob-roundtrip.test.ts +0 -0
  67. package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
  68. package/src/scenarios/cache-ttl-expiry.test.ts +73 -0
  69. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +129 -0
  70. package/src/scenarios/dispatch-input-mapping.test.ts +163 -0
  71. package/src/scenarios/dispatch-output-mapping.test.ts +155 -0
  72. package/src/scenarios/fixtures-gating.test.ts +139 -1
  73. package/src/scenarios/fs-path-traversal.test.ts +124 -0
  74. package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
  75. package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
  76. package/src/scenarios/kv-atomic-increment.test.ts +74 -0
  77. package/src/scenarios/kv-cas.test.ts +75 -0
  78. package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
  79. package/src/scenarios/kv-ttl-expiry.test.ts +78 -0
  80. package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
  81. package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
  82. package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
  83. package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
  84. package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
  85. package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
  86. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
  87. package/src/scenarios/pack-registry-publish.test.ts +231 -51
  88. package/src/scenarios/pause-resume.test.ts +43 -0
  89. package/src/scenarios/provider-usage.test.ts +185 -0
  90. package/src/scenarios/queue-ack-nack-dlq.test.ts +121 -0
  91. package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
  92. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +88 -0
  93. package/src/scenarios/replay-llm-cache-key.test.ts +166 -25
  94. package/src/scenarios/search-bm25-roundtrip.test.ts +92 -0
  95. package/src/scenarios/spec-corpus-validity.test.ts +17 -1
  96. package/src/scenarios/sql-injection-rejection.test.ts +84 -0
  97. package/src/scenarios/sql-transaction-atomicity.test.ts +95 -0
  98. package/src/scenarios/stream-subscribe-from-beginning.test.ts +103 -0
  99. package/src/scenarios/subworkflow-input-mapping.test.ts +170 -0
  100. package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
  101. package/src/scenarios/table-cursor-pagination.test.ts +85 -0
  102. package/src/scenarios/table-schema-enforcement.test.ts +84 -0
  103. package/src/scenarios/vector-knn-roundtrip.test.ts +88 -0
  104. package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
  105. package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
  106. package/src/scenarios/workflow-chain-host-expansion.test.ts +202 -0
  107. package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
  108. package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
  109. package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
@@ -0,0 +1,267 @@
1
+ /**
2
+ * aiEnvelope.universalKinds — FINAL v1.1 advertisement-shape verification + behavioral placeholders.
3
+ *
4
+ * Status: DRAFT (advertisement-shape). `spec/v1/ai-envelope.md` landed
5
+ * 2026-05-17 as DRAFT v1.x. This scenario asserts the advertisement shape
6
+ * for hosts that opt into the new envelope-contracts surface
7
+ * (`capabilities.envelopeContracts.advertised: true`) and keeps the deeper
8
+ * behavioral assertions as `it.todo()` until a reference host wires the
9
+ * accept path.
10
+ *
11
+ * Summary: hosts MUST advertise the four universal kinds (`clarification.request`,
12
+ * `schema.request`, `schema.response`, `error`) in `capabilities.supportedEnvelopes`
13
+ * once they opt in. Universals are always-allowed; Envelope Contract gates MUST NOT
14
+ * refuse them.
15
+ *
16
+ * @see spec/v1/ai-envelope.md §"Universal kinds"
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { driver } from '../lib/driver.js';
21
+
22
+ interface DiscoveryDoc {
23
+ capabilities?: Record<string, unknown>;
24
+ supportedEnvelopes?: string[];
25
+ }
26
+
27
+ const UNIVERSALS = ['clarification.request', 'schema.request', 'schema.response', 'error'] as const;
28
+
29
+ async function readEnvelopeContracts(): Promise<{ advertised: boolean } | null> {
30
+ const res = await driver.get('/.well-known/openwop');
31
+ const body = res.json as DiscoveryDoc | undefined;
32
+ const top = body?.capabilities as Record<string, unknown> | undefined;
33
+ const block = top && typeof top === 'object' ? (top['envelopeContracts'] as Record<string, unknown> | undefined) : undefined;
34
+ if (!block || typeof block !== 'object') return null;
35
+ return { advertised: block['advertised'] === true };
36
+ }
37
+
38
+ async function readSupportedEnvelopes(): Promise<string[] | null> {
39
+ const res = await driver.get('/.well-known/openwop');
40
+ const body = res.json as DiscoveryDoc | undefined;
41
+ // `supportedEnvelopes` is required v1 at the top level of the discovery payload
42
+ // per capabilities.schema.json. Some hosts nest it under `capabilities`.
43
+ const top = body?.supportedEnvelopes ?? (body?.capabilities as { supportedEnvelopes?: string[] } | undefined)?.supportedEnvelopes;
44
+ return Array.isArray(top) ? top : null;
45
+ }
46
+
47
+ describe('aiEnvelope.universalKinds: advertisement shape (FINAL v1.1)', () => {
48
+ it('capabilities.envelopeContracts is either absent or a well-formed object', async () => {
49
+ const block = await readEnvelopeContracts();
50
+ if (block === null) return; // host doesn't opt in — skip
51
+ expect(
52
+ typeof block.advertised,
53
+ driver.describe(
54
+ 'ai-envelope.md §"Capability handshake integration"',
55
+ 'capabilities.envelopeContracts.advertised MUST be a boolean when present',
56
+ ),
57
+ ).toBe('boolean');
58
+ });
59
+
60
+ it('opted-in hosts advertise every universal kind in supportedEnvelopes', async () => {
61
+ const block = await readEnvelopeContracts();
62
+ if (block === null || !block.advertised) return; // not opted in — skip
63
+ const advertised = await readSupportedEnvelopes();
64
+ expect(
65
+ Array.isArray(advertised),
66
+ driver.describe(
67
+ 'capabilities.schema.json §supportedEnvelopes',
68
+ 'supportedEnvelopes MUST be present as an array on hosts that advertise envelopeContracts',
69
+ ),
70
+ ).toBe(true);
71
+ for (const kind of UNIVERSALS) {
72
+ expect(
73
+ advertised!.includes(kind),
74
+ driver.describe(
75
+ 'ai-envelope.md §"Universal kinds"',
76
+ `supportedEnvelopes MUST include "${kind}" — universals are always-allowed`,
77
+ ),
78
+ ).toBe(true);
79
+ }
80
+ });
81
+ });
82
+
83
+ // Behavioral assertions through the workflow-engine sample's env-gated
84
+ // `POST /v1/host/sample/envelope/accept` seam (the RFC 0021 §A
85
+ // AIEnvelopeAcceptor reference implementation at
86
+ // `apps/workflow-engine/backend/typescript/src/host/envelopeAcceptor.ts`).
87
+ // Each test soft-skips on HTTP 404 (host doesn't expose the seam) so
88
+ // non-sample hosts keep the advertisement-shape coverage above.
89
+ async function accept(envelope: unknown, opts: Record<string, unknown> = {}): Promise<{ status: number; body: { status?: string; reason?: string; details?: unknown[]; envelopeId?: string } }> {
90
+ const res = await driver.post('/v1/host/sample/envelope/accept', { envelope, ...opts });
91
+ return { status: res.status, body: res.json as { status?: string; reason?: string; details?: unknown[]; envelopeId?: string } };
92
+ }
93
+
94
+ const baseMeta = { source: 'ai-generation' as const, ts: '2026-05-18T10:00:00Z' };
95
+
96
+ describe('aiEnvelope.universalKinds: behavioral accept via /v1/host/sample/envelope/accept (FINAL v1.1)', () => {
97
+ it('accept clarification.request with valid payload → status: accepted', async () => {
98
+ const r = await accept({
99
+ type: 'clarification.request',
100
+ schemaVersion: 1,
101
+ envelopeId: 'env-uk-clar',
102
+ correlationId: 'r:n:0:clar',
103
+ payload: { questions: [{ id: 'q1', question: 'Which provider?' }] },
104
+ meta: baseMeta,
105
+ });
106
+ if (r.status === 404) return;
107
+ expect(r.body.status, driver.describe('ai-envelope.md §"Universal kinds"', 'valid clarification.request MUST be accepted')).toBe('accepted');
108
+ });
109
+
110
+ it('accept schema.request → status: accepted', async () => {
111
+ const r = await accept({
112
+ type: 'schema.request',
113
+ schemaVersion: 1,
114
+ envelopeId: 'env-uk-sr',
115
+ correlationId: 'r:n:0:sr',
116
+ payload: { envelopeType: 'vendor.acme.prd.create' },
117
+ meta: baseMeta,
118
+ });
119
+ if (r.status === 404) return;
120
+ expect(r.body.status).toBe('accepted');
121
+ });
122
+
123
+ it('accept schema.response (ack:true) → status: accepted', async () => {
124
+ const r = await accept({
125
+ type: 'schema.response',
126
+ schemaVersion: 1,
127
+ envelopeId: 'env-uk-sresp',
128
+ correlationId: 'r:n:0:sresp',
129
+ payload: { envelopeType: 'vendor.acme.prd.create', ack: true },
130
+ meta: baseMeta,
131
+ });
132
+ if (r.status === 404) return;
133
+ expect(r.body.status).toBe('accepted');
134
+ });
135
+
136
+ it('accept error envelope (LLM-emitted) → status: accepted (distinct from host-level ErrorEnvelope)', async () => {
137
+ const r = await accept({
138
+ type: 'error',
139
+ schemaVersion: 1,
140
+ envelopeId: 'env-uk-err',
141
+ correlationId: 'r:n:0:err',
142
+ payload: { code: 'validation_failed', message: 'I cannot produce JSON matching that schema' },
143
+ meta: baseMeta,
144
+ });
145
+ if (r.status === 404) return;
146
+ expect(r.body.status, driver.describe('ai-envelope.md §error', 'LLM-emitted error envelope MUST be accepted (NOT the host HTTP ErrorEnvelope)')).toBe('accepted');
147
+ });
148
+
149
+ it('refuse invalid clarification.request (missing questions[]) → status: invalid', async () => {
150
+ const r = await accept({
151
+ type: 'clarification.request',
152
+ schemaVersion: 1,
153
+ envelopeId: 'env-uk-bad',
154
+ correlationId: 'r:n:0:bad',
155
+ payload: { contextType: 'form-field' }, // missing required `questions`
156
+ meta: baseMeta,
157
+ });
158
+ if (r.status === 404) return;
159
+ expect(
160
+ r.body.status,
161
+ driver.describe('ai-envelope.md §"Schema discipline"', 'malformed payload MUST be rejected with invalid'),
162
+ ).toBe('invalid');
163
+ expect(Array.isArray(r.body.details), 'invalid outcome MUST carry validation details').toBe(true);
164
+ });
165
+ });
166
+
167
+ // E.1 engine-projection via the test-only event-log seam.
168
+ import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
169
+
170
+ describe('aiEnvelope.universalKinds: engine projection via event-log seam', () => {
171
+ it('clarification.request MUST be lifted to interrupt.requested { kind: "clarification" } per interrupt.md', async () => {
172
+ if (!(await isEventLogSeamAvailable())) return;
173
+ const runId = `r-uk-clar-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
174
+ const r = await accept(
175
+ {
176
+ type: 'clarification.request',
177
+ schemaVersion: 1,
178
+ envelopeId: 'env-uk-proj-clar',
179
+ correlationId: `${runId}:n:0:uk-clar`,
180
+ payload: { questions: [{ id: 'q1', question: 'why?' }] },
181
+ meta: baseMeta,
182
+ },
183
+ { projectTo: { runId, nodeId: 'n' } },
184
+ );
185
+ if (r.status === 404) return;
186
+ expect(r.body.status).toBe('accepted');
187
+ const events = await queryTestEvents(runId, { type: 'interrupt.requested' });
188
+ if (!events.ok) return;
189
+ expect(
190
+ events.events.length,
191
+ driver.describe('ai-envelope.md §"Universal kinds"', 'accepted clarification.request MUST project to interrupt.requested per interrupt.md'),
192
+ ).toBe(1);
193
+ expect((events.events[0]!.payload as { kind?: string }).kind).toBe('clarification');
194
+ await resetTestSeam();
195
+ });
196
+
197
+ it('error envelope MUST project to log.appended { level: "error" } — NOT node.failed', async () => {
198
+ if (!(await isEventLogSeamAvailable())) return;
199
+ const runId = `r-uk-err-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
200
+ await accept(
201
+ {
202
+ type: 'error',
203
+ schemaVersion: 1,
204
+ envelopeId: 'env-uk-proj-err',
205
+ correlationId: `${runId}:n:0:uk-err`,
206
+ payload: { code: 'validation_failed', message: 'cannot produce JSON' },
207
+ meta: baseMeta,
208
+ },
209
+ { projectTo: { runId, nodeId: 'n' } },
210
+ );
211
+ const logs = await queryTestEvents(runId, { type: 'log.appended' });
212
+ const fails = await queryTestEvents(runId, { type: 'node.failed' });
213
+ if (!logs.ok || !fails.ok) return;
214
+ expect(
215
+ logs.events.some((e) => (e.payload as { level?: string }).level === 'error'),
216
+ driver.describe('ai-envelope.md §"Universal kinds"', 'LLM-emitted error envelope MUST project to log.appended at error level'),
217
+ ).toBe(true);
218
+ expect(
219
+ fails.events.length,
220
+ driver.describe('ai-envelope.md §"Universal kinds"', 'LLM-emitted error envelope MUST NOT project to node.failed (distinct from terminal node failure)'),
221
+ ).toBe(0);
222
+ await resetTestSeam();
223
+ });
224
+
225
+ it('schema.request projects to log.appended (host implements next-turn injection out-of-band)', async () => {
226
+ if (!(await isEventLogSeamAvailable())) return;
227
+ const runId = `r-uk-sr-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
228
+ await accept(
229
+ {
230
+ type: 'schema.request',
231
+ schemaVersion: 1,
232
+ envelopeId: 'env-uk-proj-sr',
233
+ correlationId: `${runId}:n:0:uk-sr`,
234
+ payload: { envelopeType: 'vendor.acme.foo' },
235
+ meta: baseMeta,
236
+ },
237
+ { projectTo: { runId, nodeId: 'n' } },
238
+ );
239
+ const events = await queryTestEvents(runId, { type: 'log.appended' });
240
+ if (!events.ok) return;
241
+ expect(
242
+ events.events.length,
243
+ driver.describe('ai-envelope.md §"Universal kinds"', 'schema.request MUST project to log.appended (the schema delivery itself happens out-of-band via the host\'s next-turn system prompt)'),
244
+ ).toBeGreaterThan(0);
245
+ await resetTestSeam();
246
+ });
247
+ });
248
+
249
+ describe('aiEnvelope.universalKinds: schema.response counter-policy advertisement (ai-envelope.md §"Universal kinds")', () => {
250
+ it('host MAY count or exempt schema.response against envelopesPerTurn; when advertised, the policy field MUST be a documented enum value', async () => {
251
+ // Per ai-envelope.md §"Universal kinds": "Engines MAY count this against
252
+ // Capabilities.limits.envelopesPerTurn or exempt it; conformance does
253
+ // not lock this choice." The conformance test only verifies that hosts
254
+ // advertising a policy field use a documented value.
255
+ const res = await driver.get('/.well-known/openwop');
256
+ const body = res.json as { capabilities?: { aiEnvelope?: { schemaResponseCounterPolicy?: string } } } | undefined;
257
+ const policy = body?.capabilities?.aiEnvelope?.schemaResponseCounterPolicy;
258
+ if (policy === undefined) return; // no policy advertised — host MAY omit
259
+ expect(
260
+ ['counted', 'exempt'].includes(policy),
261
+ driver.describe(
262
+ 'ai-envelope.md §"Universal kinds"',
263
+ 'when advertised, schemaResponseCounterPolicy MUST be either "counted" or "exempt"',
264
+ ),
265
+ ).toBe(true);
266
+ });
267
+ });
@@ -26,11 +26,21 @@ const SKIP = !FIXTURE;
26
26
  interface ChannelWrittenPayload {
27
27
  channel?: string;
28
28
  value?: unknown;
29
+ /**
30
+ * Per `channel-written-payload.schema.json` — present on inbound
31
+ * cross-engine writes per `channels-and-reducers.md §"Across
32
+ * engines"`. When ANY event in the run carries this field, the
33
+ * cross-engine assertions (CF-8) MUST hold in addition to the
34
+ * intra-engine ordering rule.
35
+ */
36
+ sourceEngineId?: string;
37
+ sourceRunId?: string;
29
38
  }
30
39
 
31
40
  interface RunEvent {
32
41
  type: string;
33
42
  sequence: number;
43
+ eventId?: string;
34
44
  payload?: ChannelWrittenPayload;
35
45
  }
36
46
 
@@ -74,6 +84,40 @@ describe.skipIf(SKIP)('append-ordering: folded channel reflects event sequence',
74
84
  }
75
85
  }
76
86
 
87
+ // CF-8: cross-engine ordering rule. When ANY channel.written
88
+ // event carries `sourceEngineId`, the owner-engine MUST have
89
+ // assigned the recorded `sequence` at append time and the event
90
+ // log MUST remain strictly monotonic regardless of source. The
91
+ // intra-engine check above already verifies monotonicity per
92
+ // channel; here we additionally verify (1) at least one cross-
93
+ // engine and one own-engine write coexist correctly, and (2) any
94
+ // tie in the secondary `(sequence, eventId)` order is broken
95
+ // deterministically (no two events share both fields).
96
+ const allWrites = Array.from(byChannel.values()).flat();
97
+ const crossEngineWrites = allWrites.filter(
98
+ (e) => typeof e.payload?.sourceEngineId === 'string',
99
+ );
100
+ if (crossEngineWrites.length > 0) {
101
+ // Property: every cross-engine event carries BOTH sourceEngineId
102
+ // AND sourceRunId (per channel-written-payload.schema.json).
103
+ for (const e of crossEngineWrites) {
104
+ expect(typeof e.payload?.sourceRunId, driver.describe(
105
+ 'channel-written-payload.schema.json',
106
+ 'cross-engine writes MUST carry sourceRunId alongside sourceEngineId',
107
+ )).toBe('string');
108
+ }
109
+ // Property: (sequence, eventId) is a total order — no duplicates.
110
+ const seen = new Set<string>();
111
+ for (const e of allWrites) {
112
+ const key = `${e.sequence}::${e.eventId ?? ''}`;
113
+ expect(seen.has(key), driver.describe(
114
+ 'channels-and-reducers.md §"Tie-breaking when sequences collide"',
115
+ `(sequence, eventId) MUST be unique across the run — duplicate found: ${key}`,
116
+ )).toBe(false);
117
+ seen.add(key);
118
+ }
119
+ }
120
+
77
121
  // Cross-check against the projected channel state on the run snapshot
78
122
  // (when surfaced) — projected array length MUST equal the number of writes.
79
123
  const snapshot = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CF-4 close-out (partial) — explicit scope-failure coverage on
3
+ * `GET /v1/runs/{runId}/artifacts/{artifactId}` per
4
+ * `plans/openwop-protocol-gap-closure-plan.md` Workstream 2.
5
+ *
6
+ * The existing `route-coverage.test.ts` covers 404 / 403 envelope
7
+ * shape on an unknown artifact. This scenario adds the 401 path —
8
+ * an unauthenticated request to the artifact endpoint MUST return
9
+ * 401 with the canonical `unauthenticated` envelope, NEVER 200 (no
10
+ * cross-tenant leak via missing-auth coercion) and NEVER a redirect.
11
+ *
12
+ * Positive-path coverage (reading an artifact a workflow actually
13
+ * produced) is host-pending — no reference host currently implements
14
+ * `getArtifact` end-to-end; the path lights up when the first host
15
+ * advertises an artifact-producing fixture.
16
+ *
17
+ * @see api/openapi.yaml §`getArtifact`
18
+ * @see spec/v1/rest-endpoints.md §"GET /v1/runs/{runId}/artifacts/{artifactId}"
19
+ */
20
+
21
+ import { describe, it, expect } from 'vitest';
22
+
23
+ import { driver } from '../lib/driver.js';
24
+ import { loadEnv } from '../lib/env.js';
25
+
26
+ describe('artifact-auth: unauthenticated artifact requests are rejected', () => {
27
+ it('GET /v1/runs/{runId}/artifacts/{artifactId} without Authorization returns 401', async () => {
28
+ const env = loadEnv();
29
+ // Use node:fetch directly with NO Authorization header — bypass the
30
+ // driver's auto-auth so we exercise the unauthenticated code path.
31
+ const res = await fetch(
32
+ `${env.baseUrl}/v1/runs/openwop-conformance-noauth-run/artifacts/openwop-conformance-noauth-artifact`,
33
+ );
34
+ expect(res.status, driver.describe(
35
+ 'rest-endpoints.md GET /v1/runs/{runId}/artifacts/{artifactId}',
36
+ 'artifact endpoint MUST reject unauthenticated requests with 401 (never 200, never redirect)',
37
+ )).toBe(401);
38
+ expect(res.status, 'redirect MUST NOT be used to handle missing auth on artifact endpoint').not.toBe(301);
39
+ expect(res.status, 'redirect MUST NOT be used to handle missing auth on artifact endpoint').not.toBe(302);
40
+ expect(res.status).not.toBe(200);
41
+
42
+ // Canonical error envelope: error: 'unauthenticated', message: string.
43
+ let body: { error?: string; message?: string };
44
+ try {
45
+ body = (await res.json()) as typeof body;
46
+ } catch {
47
+ // Some hosts return a bare 401 with no body — acceptable. Skip the
48
+ // envelope-shape assertion in that case.
49
+ return;
50
+ }
51
+ if (typeof body.error === 'string') {
52
+ expect(body.error, driver.describe(
53
+ 'auth.md §"Error envelope"',
54
+ '401 envelope SHOULD carry `error: "unauthenticated"` for missing-auth on artifact endpoint',
55
+ )).toBe('unauthenticated');
56
+ }
57
+ });
58
+ });
@@ -0,0 +1,66 @@
1
+ /**
2
+ * blob-cross-tenant-isolation — RFC 0019 §B point 1.
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). Asserts that blobs put
5
+ * under tenant A MUST NOT be retrievable under tenant B at the same key.
6
+ *
7
+ * @see RFCS/0019-host-blob-cache-capability.md
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { driver } from '../lib/driver.js';
12
+
13
+ interface DiscoveryDoc {
14
+ capabilities?: Record<string, unknown>;
15
+ }
16
+
17
+ async function readCap(): Promise<Record<string, unknown> | null> {
18
+ const res = await driver.get('/.well-known/openwop');
19
+ const body = res.json as DiscoveryDoc | undefined;
20
+ const top = body?.capabilities as Record<string, unknown> | undefined;
21
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["blobStorage"] : undefined;
22
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
23
+ }
24
+
25
+ async function call(tenantId: string, op: string, args: Record<string, unknown>) {
26
+ return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'blob', op, args });
27
+ }
28
+
29
+ describe('blob-cross-tenant-isolation: advertisement shape (RFC 0019)', () => {
30
+ it('capabilities.blobStorage is either absent or a well-formed object', async () => {
31
+ const cap = await readCap();
32
+ if (cap === null) return;
33
+ expect(
34
+ typeof cap.supported,
35
+ driver.describe(
36
+ 'capabilities.schema.json §blobStorage',
37
+ 'capabilities.blobStorage.supported MUST be a boolean when present',
38
+ ),
39
+ ).toBe('boolean');
40
+ });
41
+ });
42
+
43
+ describe('blob-cross-tenant-isolation: behavioral (RFC 0019 §B point 1)', () => {
44
+ it('put under tenant A → get under tenant B with same key returns found:false', async () => {
45
+ const cap = await readCap();
46
+ if (!cap || cap.supported !== true) return;
47
+ const key = `xtenant-blob-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
48
+
49
+ const putRes = await call('tenant-a', 'put', {
50
+ bucket: 'default',
51
+ key,
52
+ contentBase64: Buffer.from('from-A').toString('base64'),
53
+ contentType: 'text/plain',
54
+ });
55
+ if (putRes.status === 404) return;
56
+ expect(putRes.status, 'put MUST succeed').toBe(200);
57
+
58
+ const getRes = await call('tenant-b', 'get', { bucket: 'default', key });
59
+ expect(getRes.status).toBe(200);
60
+ const body = getRes.json as { found?: boolean };
61
+ expect(
62
+ body.found,
63
+ driver.describe('RFC 0019 §B point 1', 'tenant B MUST NOT retrieve tenant A blob at same key'),
64
+ ).toBe(false);
65
+ });
66
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * blob-presign-expiry — RFC 0019 advertisement-shape verification + behavioral placeholders.
3
+ *
4
+ * Status: ACTIVE (advertisement-shape). RFC 0019 promoted to `Active`
5
+ * 2026-05-17. The matching `capabilities.blobStorage` 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: Presigned URLs MUST expire at the advertised TTL.
12
+ *
13
+ * @see RFCS/0019-*.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>)["blobStorage"] : undefined;
28
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
+ }
30
+
31
+ describe('blob-presign-expiry: advertisement shape (RFC 0019)', () => {
32
+ it('capabilities.blobStorage 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 §blobStorage',
39
+ 'capabilities.blobStorage.supported MUST be a boolean when present',
40
+ ),
41
+ ).toBe('boolean');
42
+ });
43
+
44
+ it('presignSupported is a boolean when set', async () => {
45
+ const cap = await readCap();
46
+ if (!cap || cap.supported !== true) return;
47
+ const subParts = ["presignSupported"];
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 0019 §A',
58
+ 'blobStorage.presignSupported MUST be boolean when present',
59
+ ),
60
+ ).toBe('boolean');
61
+ });
62
+ });
63
+
64
+ async function call(op: string, args: Record<string, unknown>) {
65
+ return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'blob', op, args });
66
+ }
67
+
68
+ describe('blob-presign-expiry: behavioral (RFC 0019 §B point 1)', () => {
69
+ it('presigned URL MUST resolve to the blob inside its TTL window and return 403 after expiry', async () => {
70
+ const probe = await call('get', { key: '__probe__' });
71
+ if (probe.status === 404) return; // seam not exposed
72
+ const key = `pre-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
73
+ const contentBase64 = Buffer.from('presigned-payload').toString('base64');
74
+ await call('put', { key, contentBase64, contentType: 'text/plain' });
75
+
76
+ // presign with TTL=2s
77
+ const presign = await call('presign', { key, expiresInSeconds: 2 });
78
+ expect(presign.status).toBe(200);
79
+ const body = presign.json as { url?: string; expiresAtMs?: number };
80
+ expect(typeof body.url, 'presign MUST return a URL').toBe('string');
81
+
82
+ // Fetch within the window — MUST return 200 + the bytes
83
+ const within = await driver.get(body.url!);
84
+ if (within.status === 404) return; // host doesn't expose the resolver route — soft-skip the expiry side too
85
+ expect(
86
+ within.status,
87
+ driver.describe('RFC 0019 §B point 1', 'presigned URL MUST resolve to 200 within its TTL window'),
88
+ ).toBe(200);
89
+
90
+ // Wait past expiry (TTL=2s + 1s buffer)
91
+ await new Promise((r) => setTimeout(r, 3000));
92
+
93
+ const after = await driver.get(body.url!);
94
+ expect(
95
+ after.status,
96
+ driver.describe('RFC 0019 §B point 1', 'presigned URL MUST return 403 after TTL expiry'),
97
+ ).toBe(403);
98
+ });
99
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * cache-cross-tenant-isolation — RFC 0019 §B point 2.
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). Asserts that cache entries
5
+ * put under tenant A MUST NOT hit on get under tenant B at the same key.
6
+ *
7
+ * @see RFCS/0019-host-blob-cache-capability.md
8
+ */
9
+
10
+ import { describe, it, expect } from 'vitest';
11
+ import { driver } from '../lib/driver.js';
12
+
13
+ interface DiscoveryDoc {
14
+ capabilities?: Record<string, unknown>;
15
+ }
16
+
17
+ async function readCap(): Promise<Record<string, unknown> | null> {
18
+ const res = await driver.get('/.well-known/openwop');
19
+ const body = res.json as DiscoveryDoc | undefined;
20
+ const top = body?.capabilities as Record<string, unknown> | undefined;
21
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["cache"] : undefined;
22
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
23
+ }
24
+
25
+ async function call(tenantId: string, op: string, args: Record<string, unknown>) {
26
+ return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'cache', op, args });
27
+ }
28
+
29
+ describe('cache-cross-tenant-isolation: advertisement shape (RFC 0019)', () => {
30
+ it('capabilities.cache is either absent or a well-formed object', async () => {
31
+ const cap = await readCap();
32
+ if (cap === null) return;
33
+ expect(
34
+ typeof cap.supported,
35
+ driver.describe(
36
+ 'capabilities.schema.json §cache',
37
+ 'capabilities.cache.supported MUST be a boolean when present',
38
+ ),
39
+ ).toBe('boolean');
40
+ });
41
+ });
42
+
43
+ describe('cache-cross-tenant-isolation: behavioral (RFC 0019 §B point 2)', () => {
44
+ it('put under tenant A → get under tenant B returns miss', async () => {
45
+ const cap = await readCap();
46
+ if (!cap || cap.supported !== true) return;
47
+ const key = `xtenant-cache-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
48
+
49
+ const putRes = await call('tenant-a', 'put', { key, value: 'from-A', ttlSeconds: 60 });
50
+ if (putRes.status === 404) return;
51
+ expect(putRes.status, 'put MUST succeed').toBe(200);
52
+
53
+ const getRes = await call('tenant-b', 'get', { key });
54
+ expect(getRes.status).toBe(200);
55
+ const body = getRes.json as { hit?: boolean };
56
+ expect(
57
+ body.hit,
58
+ driver.describe('RFC 0019 §B point 2', 'tenant B MUST NOT hit tenant A cache entry at same key'),
59
+ ).toBe(false);
60
+ });
61
+ });