@openwop/openwop-conformance 1.3.0 → 1.5.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 (118) hide show
  1. package/CHANGELOG.md +132 -1
  2. package/README.md +3 -2
  3. package/api/asyncapi.yaml +8 -0
  4. package/api/openapi.yaml +371 -1
  5. package/coverage.md +26 -6
  6. package/fixtures/conformance-envelope-nl-to-format-engaged.json +41 -0
  7. package/fixtures/conformance-envelope-recovery-applied.json +39 -0
  8. package/fixtures/conformance-envelope-refusal.json +38 -0
  9. package/fixtures/conformance-envelope-retry-attempted.json +39 -0
  10. package/fixtures/conformance-envelope-retry-exhausted.json +38 -0
  11. package/fixtures/conformance-envelope-truncated.json +39 -0
  12. package/fixtures/conformance-envelope-truncation-cap-exhaustion.json +39 -0
  13. package/fixtures/conformance-model-capability-insufficient.json +25 -0
  14. package/fixtures/conformance-multi-agent-confidence-escalation.json +49 -0
  15. package/fixtures/conformance-multi-agent-handoff-child.json +27 -0
  16. package/fixtures/conformance-multi-agent-handoff.json +49 -0
  17. package/fixtures/conformance-prompt-all-four-kinds.json +39 -0
  18. package/fixtures/conformance-prompt-end-to-end.json +33 -0
  19. package/fixtures/conformance-subworkflow-mid-run-mutation-child.json +31 -0
  20. package/fixtures/conformance-subworkflow-mid-run-mutation.json +33 -0
  21. package/fixtures/openwop-smoke-cost-emit.json +37 -0
  22. package/fixtures/prompt-templates/conformance-prompt-few-shot-2.json +14 -0
  23. package/fixtures/prompt-templates/conformance-prompt-few-shot.json +14 -0
  24. package/fixtures/prompt-templates/conformance-prompt-schema-hint.json +14 -0
  25. package/fixtures/prompt-templates/conformance-prompt-secret-redaction.json +23 -0
  26. package/fixtures/prompt-templates/conformance-prompt-trust-marker.json +23 -0
  27. package/fixtures/prompt-templates/conformance-prompt-writer-system.json +15 -0
  28. package/fixtures/prompt-templates/conformance-prompt-writer-user.json +15 -0
  29. package/fixtures.md +39 -0
  30. package/package.json +1 -1
  31. package/schemas/README.md +5 -0
  32. package/schemas/agent-manifest.schema.json +16 -0
  33. package/schemas/capabilities.schema.json +384 -1
  34. package/schemas/envelopes/clarification.request.schema.json +9 -0
  35. package/schemas/envelopes/error.schema.json +4 -0
  36. package/schemas/envelopes/schema.request.schema.json +4 -0
  37. package/schemas/envelopes/schema.response.schema.json +1 -1
  38. package/schemas/node-pack-manifest.schema.json +28 -0
  39. package/schemas/orchestrator-decision.schema.json +12 -0
  40. package/schemas/prompt-kind.schema.json +8 -0
  41. package/schemas/prompt-pack-manifest.schema.json +80 -0
  42. package/schemas/prompt-ref.schema.json +40 -0
  43. package/schemas/prompt-template.schema.json +149 -0
  44. package/schemas/registry-version-manifest.schema.json +5 -0
  45. package/schemas/run-ancestry-response.schema.json +54 -0
  46. package/schemas/run-event-payloads.schema.json +479 -11
  47. package/schemas/run-event.schema.json +15 -1
  48. package/schemas/run-snapshot.schema.json +3 -2
  49. package/schemas/workflow-definition.schema.json +19 -1
  50. package/src/lib/llm-cache-key-recipe.ts +68 -0
  51. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +104 -13
  52. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +32 -15
  53. package/src/scenarios/aiEnvelope.redaction.test.ts +6 -5
  54. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +5 -5
  55. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +211 -12
  56. package/src/scenarios/aiEnvelope.universalKinds.test.ts +7 -7
  57. package/src/scenarios/blob-presign-expiry.test.ts +7 -7
  58. package/src/scenarios/cache-ttl-expiry.test.ts +6 -6
  59. package/src/scenarios/cost-attribution.test.ts +124 -11
  60. package/src/scenarios/cross-engine-append-ordering.test.ts +99 -0
  61. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +136 -0
  62. package/src/scenarios/cross-host-causation-shape.test.ts +117 -0
  63. package/src/scenarios/cross-host-traceparent-propagation.test.ts +60 -0
  64. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +223 -0
  65. package/src/scenarios/envelope-nl-to-format-engaged.test.ts +152 -0
  66. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +343 -0
  67. package/src/scenarios/envelope-reasoning-shape.test.ts +190 -0
  68. package/src/scenarios/envelope-recovery-applied.test.ts +229 -0
  69. package/src/scenarios/envelope-refusal-shape.test.ts +289 -0
  70. package/src/scenarios/envelope-retry-attempted.test.ts +258 -0
  71. package/src/scenarios/envelope-retry-exhausted.test.ts +168 -0
  72. package/src/scenarios/envelope-tier-one-subset-static.test.ts +229 -0
  73. package/src/scenarios/envelope-truncated.test.ts +136 -0
  74. package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +144 -0
  75. package/src/scenarios/envelope-variant-discriminator-static.test.ts +152 -0
  76. package/src/scenarios/fixtures-valid.test.ts +123 -15
  77. package/src/scenarios/kv-ttl-expiry.test.ts +7 -7
  78. package/src/scenarios/model-capability-insufficient.test.ts +221 -0
  79. package/src/scenarios/model-capability-substituted.test.ts +203 -0
  80. package/src/scenarios/multi-agent-confidence-escalation.test.ts +201 -0
  81. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +167 -0
  82. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +124 -0
  83. package/src/scenarios/multi-region-idempotency.test.ts +58 -0
  84. package/src/scenarios/node-module-required-capabilities-shape.test.ts +185 -0
  85. package/src/scenarios/prompt-all-four-kinds-events.test.ts +198 -0
  86. package/src/scenarios/prompt-composed-secret-redaction.test.ts +178 -0
  87. package/src/scenarios/prompt-composed-trust-marker.test.ts +165 -0
  88. package/src/scenarios/prompt-end-to-end-events.test.ts +202 -0
  89. package/src/scenarios/prompt-list-and-fetch.test.ts +207 -0
  90. package/src/scenarios/prompt-mutable-lifecycle.test.ts +216 -0
  91. package/src/scenarios/prompt-pack-install.test.ts +187 -0
  92. package/src/scenarios/prompt-render-deterministic.test.ts +240 -0
  93. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +140 -0
  94. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +172 -0
  95. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +144 -0
  96. package/src/scenarios/prompt-template-shape.test.ts +359 -0
  97. package/src/scenarios/queue-ack-nack-dlq.test.ts +7 -7
  98. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +7 -7
  99. package/src/scenarios/replay-divergence-at-refusal.test.ts +134 -0
  100. package/src/scenarios/replay-llm-cache-key-portable.test.ts +197 -0
  101. package/src/scenarios/replay-llm-cache-key.test.ts +1 -40
  102. package/src/scenarios/replay-observable-sequence-determinism.test.ts +80 -0
  103. package/src/scenarios/sandbox-capability-gate-respected.test.ts +27 -0
  104. package/src/scenarios/sandbox-memory-cap.test.ts +58 -0
  105. package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +30 -0
  106. package/src/scenarios/sandbox-no-host-env-leak.test.ts +27 -0
  107. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +88 -0
  108. package/src/scenarios/sandbox-no-host-process-escape.test.ts +31 -0
  109. package/src/scenarios/sandbox-no-network-escape.test.ts +28 -0
  110. package/src/scenarios/sandbox-timeout-cap.test.ts +58 -0
  111. package/src/scenarios/search-bm25-roundtrip.test.ts +7 -7
  112. package/src/scenarios/spec-corpus-validity.test.ts +34 -6
  113. package/src/scenarios/sql-transaction-atomicity.test.ts +6 -6
  114. package/src/scenarios/stream-subscribe-from-beginning.test.ts +7 -7
  115. package/src/scenarios/subworkflow-input-mapping.test.ts +70 -4
  116. package/src/scenarios/table-cursor-pagination.test.ts +7 -7
  117. package/src/scenarios/table-schema-enforcement.test.ts +7 -7
  118. package/src/scenarios/vector-knn-roundtrip.test.ts +7 -7
@@ -0,0 +1,229 @@
1
+ /**
2
+ * envelope-recovery-applied — RFC 0032 §B.6 runtime behavior (MAY tier).
3
+ *
4
+ * Capability-gated on `capabilities.envelopes.reliability.supported: true`
5
+ * AND `events[]` includes `envelope.recovery.applied`. Soft-skip cleanly on
6
+ * hosts that don't implement lenient parsing.
7
+ *
8
+ * Also exercises SECURITY invariant `envelope-recovery-no-content-leak`:
9
+ * the seam refuses payloads with any field outside the closed schema
10
+ * (`{nodeId, path, byteOffset?}`) so a future regression that adds a
11
+ * `recoveredContent` field (or any other carrier of pre-recovery output)
12
+ * fails fast at the CI gate.
13
+ *
14
+ * @see RFCS/0032-envelope-reliability-events.md §B.6 + §G
15
+ * @see SECURITY/invariants.yaml envelope-recovery-no-content-leak
16
+ * @see schemas/run-event-payloads.schema.json §envelopeRecoveryApplied
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { driver } from '../lib/driver.js';
21
+
22
+ const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
23
+
24
+ async function emit(input: Record<string, unknown>): Promise<{
25
+ status: number;
26
+ body: { event?: { type?: string; payload?: Record<string, unknown> }; error?: { code?: string } };
27
+ }> {
28
+ const res = await driver.post('/v1/host/sample/test/emit-envelope-reliability', input);
29
+ return {
30
+ status: res.status,
31
+ body: res.json as { event?: { type?: string; payload?: Record<string, unknown> }; error?: { code?: string } },
32
+ };
33
+ }
34
+
35
+ describe.skipIf(HTTP_SKIP)('envelope-recovery-applied: seam emission (RFC 0032 §B.6)', () => {
36
+ it('accepts a well-formed `envelope.recovery.applied` payload with markdown-fence path', async () => {
37
+ const r = await emit({
38
+ runId: 'conformance-recovery-1',
39
+ type: 'envelope.recovery.applied',
40
+ payload: {
41
+ nodeId: 'writer',
42
+ path: 'markdown-fence',
43
+ byteOffset: 42,
44
+ },
45
+ });
46
+ if (r.status === 404) return;
47
+ expect(r.status).toBe(200);
48
+ expect(r.body.event?.type).toBe('envelope.recovery.applied');
49
+ expect(r.body.event?.payload?.path).toBe('markdown-fence');
50
+ expect(r.body.event?.payload?.byteOffset).toBe(42);
51
+ });
52
+
53
+ it('accepts each spec-reserved `path` enum value', async () => {
54
+ for (const path of ['direct', 'jsonrepair', 'markdown-fence', 'brace-walker', 'custom']) {
55
+ const r = await emit({
56
+ runId: `conformance-recovery-path-${path}`,
57
+ type: 'envelope.recovery.applied',
58
+ payload: { nodeId: 'writer', path },
59
+ });
60
+ if (r.status === 404) return;
61
+ expect(r.status, `path: ${path} MUST be accepted`).toBe(200);
62
+ expect(r.body.event?.payload?.path).toBe(path);
63
+ }
64
+ });
65
+ });
66
+
67
+ describe.skipIf(HTTP_SKIP)('envelope-recovery-applied: SECURITY invariant envelope-recovery-no-content-leak', () => {
68
+ it('rejects payloads carrying a `recoveredContent` field (pre-recovery output MUST NOT appear in the event)', async () => {
69
+ const r = await emit({
70
+ runId: 'conformance-recovery-leak',
71
+ type: 'envelope.recovery.applied',
72
+ payload: {
73
+ nodeId: 'writer',
74
+ path: 'markdown-fence',
75
+ recoveredContent: 'this is the pre-recovery output that should NOT be in the event', // forbidden per §G
76
+ },
77
+ });
78
+ if (r.status === 404) return;
79
+ expect(
80
+ r.status,
81
+ driver.describe(
82
+ 'SECURITY/invariants.yaml §envelope-recovery-no-content-leak',
83
+ 'envelope.recovery.applied payload MUST NOT carry pre-recovery output substrings; only the canonical {nodeId, path, byteOffset?} keys per RFC 0032 §B.6 + §G — the recovered content rides on downstream RunEventDoc, not on the recovery event',
84
+ ),
85
+ ).toBe(400);
86
+ expect(r.body.error?.code).toBe('envelope_recovery_content_leak');
87
+ });
88
+
89
+ it('rejects payloads carrying any extra field outside {nodeId, path, byteOffset}', async () => {
90
+ const r = await emit({
91
+ runId: 'conformance-recovery-extra',
92
+ type: 'envelope.recovery.applied',
93
+ payload: {
94
+ nodeId: 'writer',
95
+ path: 'markdown-fence',
96
+ sourceSnippet: 'arbitrary extra key', // forbidden by additionalProperties: false in the schema
97
+ },
98
+ });
99
+ if (r.status === 404) return;
100
+ expect(
101
+ r.status,
102
+ driver.describe(
103
+ 'schemas/run-event-payloads.schema.json §envelopeRecoveryApplied',
104
+ 'envelope.recovery.applied has additionalProperties: false on the payload — any extra field MUST be rejected to prevent regression carriers for pre-recovery output (defense-in-depth on top of envelope-recovery-no-content-leak)',
105
+ ),
106
+ ).toBe(400);
107
+ expect(r.body.error?.code).toBe('envelope_recovery_content_leak');
108
+ });
109
+ });
110
+
111
+ // Live end-to-end through dispatchStructured()'s lenient-parse fallback.
112
+ // Drives the mock provider with a markdown-fenced JSON response on the
113
+ // FIRST attempt; the host's `tryLenientParse()` strips the fence,
114
+ // returns the parsed payload, and emits `envelope.recovery.applied`
115
+ // without consuming a retry slot per RFC 0032 §B.6 + RFC 0033 §D.
116
+ //
117
+ // Reuses the existing `conformance-envelope-recovery-applied`
118
+ // fixture + mock-program seam established by the keystone work
119
+ // (`f5148cf`, `5817523`). Fixture- + capability- + seam-gated:
120
+ // soft-skip when any layer is absent.
121
+
122
+ import { pollUntilTerminal } from '../lib/polling.js';
123
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
124
+
125
+ const RECOVERY_FIXTURE = 'conformance-envelope-recovery-applied';
126
+ const RECOVERY_NODE_ID = 'structured-call';
127
+
128
+ interface ProgrammedRunEvent {
129
+ type: string;
130
+ payload?: Record<string, unknown>;
131
+ nodeId?: string;
132
+ sequence: number;
133
+ }
134
+
135
+ async function programRecovery(program: Array<Record<string, unknown>>): Promise<{ status: number }> {
136
+ const res = await driver.post('/v1/host/sample/test/mock-ai/program', { nodeId: RECOVERY_NODE_ID, program });
137
+ return { status: res.status };
138
+ }
139
+
140
+ async function runAndReadEvents(): Promise<ProgrammedRunEvent[] | null> {
141
+ const create = await driver.post('/v1/runs', { workflowId: RECOVERY_FIXTURE });
142
+ if (create.status !== 201) return null;
143
+ const runId = (create.json as { runId: string }).runId;
144
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
145
+ const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
146
+ if (eventsRes.status !== 200) return null;
147
+ return ((eventsRes.json as { events?: ProgrammedRunEvent[] } | undefined)?.events ?? []) as ProgrammedRunEvent[];
148
+ }
149
+
150
+ describe.skipIf(HTTP_SKIP)('envelope-recovery-applied: end-to-end through the envelope-validation pipeline', () => {
151
+ it('when mock LLM emits envelope wrapped in markdown fence, exactly one `envelope.recovery.applied` event fires with `path: "markdown-fence"`', async () => {
152
+ if (!isFixtureAdvertised(RECOVERY_FIXTURE)) return;
153
+ const seed = await programRecovery([
154
+ // Markdown-fenced JSON — dispatchStructured's strict parse fails,
155
+ // tryLenientParse() strips the fence + succeeds via the
156
+ // 'markdown-fence' path.
157
+ { content: '```json\n{"result":"ok"}\n```' },
158
+ ]);
159
+ if (seed.status === 404) return;
160
+ expect(seed.status).toBe(200);
161
+
162
+ const events = await runAndReadEvents();
163
+ if (events === null) return;
164
+ const recoveries = events.filter((e) => e.type === 'envelope.recovery.applied');
165
+ expect(
166
+ recoveries.length,
167
+ driver.describe(
168
+ 'RFCS/0032-envelope-reliability-events.md §B.6',
169
+ 'exactly one envelope.recovery.applied event MUST fire when lenient parsing strips a markdown fence',
170
+ ),
171
+ ).toBe(1);
172
+ expect(
173
+ recoveries[0]?.payload?.path,
174
+ driver.describe(
175
+ 'RFCS/0032-envelope-reliability-events.md §B.6',
176
+ 'path MUST identify the recovery strategy that engaged (markdown-fence here)',
177
+ ),
178
+ ).toBe('markdown-fence');
179
+ });
180
+
181
+ it('recovery does NOT consume a retry attempt — `envelope.retry.attempted` does NOT fire as a consequence of recovery (RFC 0033 §D)', async () => {
182
+ if (!isFixtureAdvertised(RECOVERY_FIXTURE)) return;
183
+ const seed = await programRecovery([
184
+ { content: '```json\n{"result":"ok"}\n```' },
185
+ ]);
186
+ if (seed.status === 404) return;
187
+
188
+ const events = await runAndReadEvents();
189
+ if (events === null) return;
190
+ const retries = events.filter((e) => e.type === 'envelope.retry.attempted');
191
+ expect(
192
+ retries.length,
193
+ driver.describe(
194
+ 'RFCS/0033-envelope-completion-contract.md §D',
195
+ 'recovery (parse fix-up) MUST NOT count against the retry budget — no envelope.retry.attempted may fire',
196
+ ),
197
+ ).toBe(0);
198
+ });
199
+
200
+ it('recovered envelope is subsequently accepted normally; downstream RunEventDoc carries the recovered content', async () => {
201
+ if (!isFixtureAdvertised(RECOVERY_FIXTURE)) return;
202
+ const seed = await programRecovery([
203
+ { content: '```json\n{"result":"recovered-ok"}\n```' },
204
+ ]);
205
+ if (seed.status === 404) return;
206
+
207
+ const events = await runAndReadEvents();
208
+ if (events === null) return;
209
+ const nodeCompleted = events.find((e) => e.type === 'node.completed' && e.nodeId === RECOVERY_NODE_ID);
210
+ expect(
211
+ nodeCompleted,
212
+ driver.describe(
213
+ 'RFCS/0032-envelope-reliability-events.md §B.6',
214
+ 'recovered envelope MUST reach node.completed — recovery does not block downstream acceptance',
215
+ ),
216
+ ).toBeDefined();
217
+ // The dispatching node's output carries the recovered structured
218
+ // data — serialized for substring assertion since the exact shape
219
+ // depends on how the fixture node wraps the dispatch result.
220
+ const completedPayload = JSON.stringify(nodeCompleted?.payload ?? {});
221
+ expect(
222
+ completedPayload.includes('recovered-ok'),
223
+ driver.describe(
224
+ 'RFCS/0032-envelope-reliability-events.md §B.6',
225
+ 'recovered structured data MUST flow to the downstream RunEventDoc unchanged',
226
+ ),
227
+ ).toBe(true);
228
+ });
229
+ });
@@ -0,0 +1,289 @@
1
+ /**
2
+ * envelope-refusal-shape — RFC 0032 §B.3 runtime behavior (MUST tier).
3
+ *
4
+ * Capability-gated on `capabilities.envelopes.reliability.supported: true`
5
+ * AND `events[]` includes `envelope.refusal`. Non-skippable when both flags
6
+ * are advertised — `envelope.refusal` is one of the two MUST-tier events.
7
+ *
8
+ * Also exercises SECURITY invariant `envelope-refusal-no-prompt-leak`: the
9
+ * host's emit-seam refuses payloads that look like they could carry a
10
+ * credentialRef or prompt-content substring. Production emitters MUST redact
11
+ * BEFORE invoking the seam; the seam's refusal is a defense-in-depth CI gate.
12
+ *
13
+ * Drives the host's `POST /v1/host/sample/test/emit-envelope-reliability`
14
+ * seam with synthetic payloads. The seam validates the per-type required
15
+ * fields per `run-event-payloads.schema.json` §envelope* `$defs` and
16
+ * appends to the test event log. Conformance asserts:
17
+ * 1. The seam accepts a well-formed `envelope.refusal` payload.
18
+ * 2. The seam rejects payloads with `refusalText` containing a
19
+ * `secret-canary-*` substring (BYOK leak defense).
20
+ * 3. The seam rejects payloads with a top-level `credentialRef` field.
21
+ * 4. Required field absence (`provider` missing) returns 400.
22
+ *
23
+ * @see RFCS/0032-envelope-reliability-events.md §B.3 + §G
24
+ * @see RFCS/0033-envelope-completion-contract.md §D + §F
25
+ * @see SECURITY/invariants.yaml envelope-refusal-no-prompt-leak
26
+ * @see schemas/run-event-payloads.schema.json §envelopeRefusal
27
+ */
28
+
29
+ import { describe, it, expect } from 'vitest';
30
+ import { driver } from '../lib/driver.js';
31
+
32
+ const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
33
+
34
+ interface DiscoveryDoc {
35
+ capabilities?: {
36
+ envelopes?: {
37
+ reliability?: {
38
+ supported?: unknown;
39
+ events?: unknown;
40
+ };
41
+ };
42
+ };
43
+ }
44
+
45
+ async function readDiscovery(): Promise<DiscoveryDoc | null> {
46
+ try {
47
+ const res = await driver.get('/.well-known/openwop');
48
+ if (res.status !== 200) return null;
49
+ return res.json as DiscoveryDoc;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ async function emit(input: Record<string, unknown>): Promise<{ status: number; body: { event?: { type?: string; payload?: Record<string, unknown> }; error?: { code?: string } } }> {
56
+ const res = await driver.post('/v1/host/sample/test/emit-envelope-reliability', input);
57
+ return {
58
+ status: res.status,
59
+ body: res.json as { event?: { type?: string; payload?: Record<string, unknown> }; error?: { code?: string } },
60
+ };
61
+ }
62
+
63
+ describe.skipIf(HTTP_SKIP)('envelope-refusal-shape: seam emission (RFC 0032 §B.3 MUST)', () => {
64
+ it('accepts a well-formed `envelope.refusal` payload + writes it to the test event log', async () => {
65
+ const d = await readDiscovery();
66
+ if (d === null) return;
67
+ const reliability = d.capabilities?.envelopes?.reliability;
68
+ if (!reliability || reliability.supported !== true) return;
69
+ if (!Array.isArray(reliability.events) || !(reliability.events as unknown[]).includes('envelope.refusal')) return;
70
+
71
+ const r = await emit({
72
+ runId: 'conformance-refusal-1',
73
+ type: 'envelope.refusal',
74
+ payload: {
75
+ nodeId: 'writer',
76
+ provider: 'anthropic',
77
+ model: 'claude-3-5-sonnet',
78
+ refusalText: 'I cannot proceed with that request because the safety filter triggered on category X.',
79
+ safetyCategory: 'harmful-content',
80
+ },
81
+ nodeId: 'writer',
82
+ });
83
+ if (r.status === 404) return;
84
+ expect(
85
+ r.status,
86
+ driver.describe(
87
+ 'schemas/run-event-payloads.schema.json §envelopeRefusal',
88
+ 'a payload with the required {nodeId, provider, model} fields MUST be accepted by the seam',
89
+ ),
90
+ ).toBe(200);
91
+ expect(r.body.event?.type).toBe('envelope.refusal');
92
+ const payload = r.body.event?.payload ?? {};
93
+ expect(payload.nodeId).toBe('writer');
94
+ expect(payload.provider).toBe('anthropic');
95
+ expect(payload.model).toBe('claude-3-5-sonnet');
96
+ });
97
+
98
+ it('rejects payloads with `refusalText` containing a `secret-canary-*` substring (BYOK leak defense per envelope-refusal-no-prompt-leak)', async () => {
99
+ const r = await emit({
100
+ runId: 'conformance-refusal-leak',
101
+ type: 'envelope.refusal',
102
+ payload: {
103
+ nodeId: 'writer',
104
+ provider: 'anthropic',
105
+ model: 'claude-3-5-sonnet',
106
+ refusalText: 'The model echoed back secret-canary-leak-test verbatim.',
107
+ safetyCategory: 'harmful-content',
108
+ },
109
+ });
110
+ if (r.status === 404) return;
111
+ expect(
112
+ r.status,
113
+ driver.describe(
114
+ 'SECURITY/invariants.yaml §envelope-refusal-no-prompt-leak',
115
+ 'envelope.refusal.refusalText MUST be passed through the host BYOK redaction harness; seam refuses payloads carrying secret-canary-* substrings (defense-in-depth CI gate per RFC 0032 §B.3 + §G)',
116
+ ),
117
+ ).toBe(400);
118
+ expect(r.body.error?.code).toBe('envelope_reliability_credential_leak');
119
+ });
120
+
121
+ it('rejects payloads with a top-level `credentialRef` field', async () => {
122
+ const r = await emit({
123
+ runId: 'conformance-refusal-credref',
124
+ type: 'envelope.refusal',
125
+ payload: {
126
+ nodeId: 'writer',
127
+ provider: 'anthropic',
128
+ model: 'claude-3-5-sonnet',
129
+ credentialRef: 'secret-byok-abc123', // forbidden
130
+ },
131
+ });
132
+ if (r.status === 404) return;
133
+ expect(r.status).toBe(400);
134
+ expect(r.body.error?.code).toBe('envelope_reliability_credential_leak');
135
+ });
136
+
137
+ it('rejects payloads missing required `provider` field', async () => {
138
+ const r = await emit({
139
+ runId: 'conformance-refusal-missing',
140
+ type: 'envelope.refusal',
141
+ payload: {
142
+ nodeId: 'writer',
143
+ // provider intentionally omitted
144
+ model: 'claude-3-5-sonnet',
145
+ },
146
+ });
147
+ if (r.status === 404) return;
148
+ expect(r.status).toBe(400);
149
+ expect(r.body.error?.code).toBe('invalid_argument');
150
+ });
151
+ });
152
+
153
+ describe.skipIf(HTTP_SKIP)('envelope-refusal-shape: advertisement contract (RFC 0032 §C)', () => {
154
+ it('capabilities.envelopes.reliability (when supported: true with non-empty events[]) MUST list both MUST-tier events', async () => {
155
+ const d = await readDiscovery();
156
+ if (d === null) return;
157
+ const reliability = d.capabilities?.envelopes?.reliability;
158
+ if (!reliability || reliability.supported !== true) return;
159
+ // Hosts running the legacy undifferentiated retry loop advertise
160
+ // `events: []` (per the OPENWOP_ENVELOPE_RELIABILITY_END_TO_END=false
161
+ // operator override). The two MUST-tier events still surface through
162
+ // the test seam in that case; the advertisement-shape MUST applies
163
+ // only when events[] is non-empty.
164
+ if (!Array.isArray(reliability.events) || (reliability.events as unknown[]).length === 0) return;
165
+ expect(
166
+ (reliability.events as unknown[]).includes('envelope.refusal'),
167
+ driver.describe(
168
+ 'RFCS/0032-envelope-reliability-events.md §C',
169
+ 'hosts that advertise reliability.supported: true with non-empty events[] MUST include envelope.refusal (one of the two MUST-tier events per RFC 0032 §C normative text)',
170
+ ),
171
+ ).toBe(true);
172
+ expect(
173
+ (reliability.events as unknown[]).includes('envelope.retry.exhausted'),
174
+ driver.describe(
175
+ 'RFCS/0032-envelope-reliability-events.md §C',
176
+ 'hosts that advertise reliability.supported: true with non-empty events[] MUST also include envelope.retry.exhausted (the other MUST-tier event; both MUSTs land together)',
177
+ ),
178
+ ).toBe(true);
179
+ });
180
+ });
181
+
182
+ // End-to-end refusal through dispatchStructured. Drives the conformance
183
+ // `mock` provider with a program returning `stopReason: 'safety'` +
184
+ // `refusalText: '...'`; the host's failure-mode-aware retry router
185
+ // classifies this as refusal (RFC 0032 §B.3), emits exactly one
186
+ // envelope.refusal event, throws envelope_refusal WITHOUT
187
+ // retrying (RFC 0033 §D), and the executor surfaces the error code on
188
+ // the RunSnapshot. SECURITY invariant envelope-refusal-no-prompt-leak
189
+ // asserts that RunSnapshot.error.message does NOT echo the refusalText.
190
+
191
+ import { pollUntilTerminal } from '../lib/polling.js';
192
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
193
+
194
+ const E2E_FIXTURE = 'conformance-envelope-refusal';
195
+ const E2E_NODE_ID = 'structured-call';
196
+
197
+ interface E2eEvent {
198
+ type: string;
199
+ payload?: Record<string, unknown>;
200
+ nodeId?: string;
201
+ sequence: number;
202
+ }
203
+
204
+ async function programMockRefusal(refusalText: string): Promise<{ status: number }> {
205
+ const res = await driver.post('/v1/host/sample/test/mock-ai/program', {
206
+ nodeId: E2E_NODE_ID,
207
+ program: [{ stopReason: 'safety', refusalText }],
208
+ });
209
+ return { status: res.status };
210
+ }
211
+
212
+ async function runE2eAndRead(): Promise<{ events: E2eEvent[]; terminal: unknown } | null> {
213
+ const create = await driver.post('/v1/runs', { workflowId: E2E_FIXTURE });
214
+ if (create.status !== 201) return null;
215
+ const runId = (create.json as { runId: string }).runId;
216
+ const terminal = await pollUntilTerminal(runId, { timeoutMs: 10_000 });
217
+ const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
218
+ if (eventsRes.status !== 200) return null;
219
+ const events = ((eventsRes.json as { events?: E2eEvent[] } | undefined)?.events ?? []) as E2eEvent[];
220
+ return { events, terminal };
221
+ }
222
+
223
+ describe.skipIf(HTTP_SKIP)('envelope-refusal-shape: end-to-end refusal through dispatchStructured', () => {
224
+ it('when mock provider returns stopReason: "safety" with refusalText, host emits exactly one envelope.refusal event AND does NOT retry', async () => {
225
+ if (!isFixtureAdvertised(E2E_FIXTURE)) return;
226
+ const seed = await programMockRefusal('I cannot help with that — safety filter triggered.');
227
+ if (seed.status === 404) return;
228
+ expect(seed.status).toBe(200);
229
+
230
+ const result = await runE2eAndRead();
231
+ if (result === null) return;
232
+ const refusals = result.events.filter((e) => e.type === 'envelope.refusal');
233
+ expect(
234
+ refusals.length,
235
+ driver.describe(
236
+ 'RFCS/0032-envelope-reliability-events.md §B.3',
237
+ 'exactly one envelope.refusal event MUST fire on provider safety-stop',
238
+ ),
239
+ ).toBe(1);
240
+ // RFC 0033 §D normative: refusal MUST NOT retry (circumvention concern).
241
+ const retries = result.events.filter((e) => e.type === 'envelope.retry.attempted');
242
+ expect(
243
+ retries.length,
244
+ driver.describe(
245
+ 'RFCS/0033-envelope-completion-contract.md §D',
246
+ 'host MUST NOT retry after envelope.refusal (refusal is terminal — retrying with prompt mutation creates a circumvention concern)',
247
+ ),
248
+ ).toBe(0);
249
+ });
250
+
251
+ it('node fails with RunSnapshot.error.code = "envelope_refusal" per RFC 0033 §F', async () => {
252
+ if (!isFixtureAdvertised(E2E_FIXTURE)) return;
253
+ const seed = await programMockRefusal('Refusal text for terminal-error-code assertion.');
254
+ if (seed.status === 404) return;
255
+
256
+ const result = await runE2eAndRead();
257
+ if (result === null) return;
258
+ const code = (result.terminal as { error?: { code?: string } }).error?.code;
259
+ expect(
260
+ code,
261
+ driver.describe(
262
+ 'RFCS/0033-envelope-completion-contract.md §F',
263
+ 'refusal-driven failure MUST surface as RunSnapshot.error.code = envelope_refusal (renamed 2026-05-21 from envelope_refused_by_provider per the MyndHyve adoption-feedback amendment)',
264
+ ),
265
+ ).toBe('envelope_refusal');
266
+ });
267
+
268
+ it('RunSnapshot.error.message MUST NOT echo the providers refusal text (SECURITY invariant envelope-refusal-no-prompt-leak)', async () => {
269
+ if (!isFixtureAdvertised(E2E_FIXTURE)) return;
270
+ // A distinctive refusal text so the message-no-echo assertion has
271
+ // a unique substring to scan for. Production refusal texts may
272
+ // contain prompt content; the host MUST keep it off the error
273
+ // message surface (event log only, scrubbed via SR-1).
274
+ const REFUSAL_TEXT = 'REFUSAL-CANARY-A8F3-do-not-echo-this-substring-into-RunSnapshot.error.message';
275
+ const seed = await programMockRefusal(REFUSAL_TEXT);
276
+ if (seed.status === 404) return;
277
+
278
+ const result = await runE2eAndRead();
279
+ if (result === null) return;
280
+ const message = (result.terminal as { error?: { message?: string } }).error?.message ?? '';
281
+ expect(
282
+ message.includes(REFUSAL_TEXT),
283
+ driver.describe(
284
+ 'SECURITY/invariants.yaml §envelope-refusal-no-prompt-leak',
285
+ 'RunSnapshot.error.message MUST NOT echo refusalText — the safety-filter text may contain prompt content; spec-compliant emission carries it only on the envelope.refusal event payload (subject to SR-1 redaction at the eventLog.append boundary)',
286
+ ),
287
+ ).toBe(false);
288
+ });
289
+ });