@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.
- package/CHANGELOG.md +90 -0
- package/README.md +2 -2
- package/api/redocly.yaml +15 -0
- package/coverage.md +27 -14
- package/fixtures/conformance-agent-low-confidence.json +7 -4
- package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
- package/fixtures/conformance-agent-reasoning-streaming.json +37 -0
- package/fixtures/conformance-agent-reasoning.json +23 -4
- package/fixtures/conformance-dispatch-cancellable-child.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
- package/fixtures/conformance-dispatch-deterministic-fail-child.json +30 -0
- package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
- package/fixtures/conformance-dispatch-input-mapping-no-default.json +49 -0
- package/fixtures/conformance-dispatch-input-mapping.json +49 -0
- package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
- package/fixtures/conformance-dispatch-output-mapping.json +49 -0
- package/fixtures/conformance-dispatch-per-worker-override.json +59 -0
- package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
- package/fixtures/conformance-subworkflow-input-mapping-no-default.json +33 -0
- package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
- package/fixtures.md +18 -2
- package/package.json +1 -1
- package/schemas/README.md +7 -0
- package/schemas/agent-ref.schema.json +1 -1
- package/schemas/ai-envelope.schema.json +106 -0
- package/schemas/capabilities.schema.json +264 -0
- package/schemas/core-conformance-mock-agent-config.schema.json +152 -0
- package/schemas/dispatch-config.schema.json +26 -0
- package/schemas/envelopes/clarification.request.schema.json +43 -0
- package/schemas/envelopes/error.schema.json +26 -0
- package/schemas/envelopes/schema.request.schema.json +22 -0
- package/schemas/envelopes/schema.response.schema.json +22 -0
- package/schemas/node-pack-manifest.schema.json +5 -0
- package/schemas/pack-lockfile.schema.json +16 -0
- package/schemas/run-event-payloads.schema.json +35 -1
- package/schemas/run-event.schema.json +2 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
- package/src/lib/driver.ts +15 -0
- package/src/lib/env.ts +51 -0
- package/src/lib/event-log-query.ts +62 -0
- package/src/lib/fixtures.ts +38 -1
- package/src/lib/host-toggle.ts +54 -0
- package/src/lib/multi-agent-capabilities.ts +10 -0
- package/src/lib/otel-scrape.ts +59 -0
- package/src/lib/webhook-receiver.ts +137 -0
- package/src/lib/workflow-chain-expansion.ts +213 -0
- package/src/scenarios/agentPackCatalog.test.ts +216 -0
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
- package/src/scenarios/agentReasoningEvents.test.ts +58 -7
- package/src/scenarios/agentReasoningStreaming.test.ts +193 -0
- package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
- package/src/scenarios/ai-envelope-shape.test.ts +362 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +261 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +268 -0
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +284 -0
- package/src/scenarios/aiEnvelope.redaction.test.ts +253 -0
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +226 -0
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +194 -0
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +267 -0
- package/src/scenarios/append-ordering.test.ts +44 -0
- package/src/scenarios/artifact-auth.test.ts +58 -0
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/blob-presign-expiry.test.ts +99 -0
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +73 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +129 -0
- package/src/scenarios/dispatch-input-mapping.test.ts +163 -0
- package/src/scenarios/dispatch-output-mapping.test.ts +155 -0
- package/src/scenarios/fixtures-gating.test.ts +139 -1
- package/src/scenarios/fs-path-traversal.test.ts +124 -0
- package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
- package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
- package/src/scenarios/kv-atomic-increment.test.ts +74 -0
- package/src/scenarios/kv-cas.test.ts +75 -0
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
- package/src/scenarios/kv-ttl-expiry.test.ts +78 -0
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
- package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +19 -0
- package/src/scenarios/pack-registry-publish.test.ts +231 -51
- package/src/scenarios/pause-resume.test.ts +43 -0
- package/src/scenarios/provider-usage.test.ts +185 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +121 -0
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +88 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +166 -25
- package/src/scenarios/search-bm25-roundtrip.test.ts +92 -0
- package/src/scenarios/spec-corpus-validity.test.ts +17 -1
- package/src/scenarios/sql-injection-rejection.test.ts +84 -0
- package/src/scenarios/sql-transaction-atomicity.test.ts +95 -0
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +103 -0
- package/src/scenarios/subworkflow-input-mapping.test.ts +170 -0
- package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
- package/src/scenarios/table-cursor-pagination.test.ts +85 -0
- package/src/scenarios/table-schema-enforcement.test.ts +84 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +88 -0
- package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
- package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
- package/src/scenarios/workflow-chain-host-expansion.test.ts +202 -0
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiEnvelope.redaction — 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. Behavioral assertions stay `it.todo()` until a
|
|
6
|
+
* reference host wires the envelope accept path through the BYOK redaction
|
|
7
|
+
* harness.
|
|
8
|
+
*
|
|
9
|
+
* Summary: AI Envelopes MUST route through the same BYOK redaction harness
|
|
10
|
+
* applied to a fresh `MemoryEntry.put` per `agent-memory.md` §"SR-1
|
|
11
|
+
* secret-redaction invariant". The fact that the LLM was instructed not to
|
|
12
|
+
* emit secrets is NOT evidence to skip redaction — the model can hallucinate
|
|
13
|
+
* secret-shaped substrings from prompt context, in-context examples, or tool
|
|
14
|
+
* results. Redacted material MUST NOT appear in resulting `RunEventDoc`s,
|
|
15
|
+
* OTel span attributes, debug-bundle exports, or error envelopes returned to
|
|
16
|
+
* the client. The pass runs AFTER validation and BEFORE dedup/handler routing
|
|
17
|
+
* in the production-flow ordering.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/v1/ai-envelope.md §"Redaction (SR-1 carry-forward)"
|
|
20
|
+
* @see spec/v1/agent-memory.md §"SR-1 secret-redaction invariant"
|
|
21
|
+
* @see SECURITY/invariants.yaml#envelope-redaction-sr-1-carry-forward
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
|
|
27
|
+
interface DiscoveryDoc {
|
|
28
|
+
capabilities?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function isBYOKAdvertised(): Promise<boolean> {
|
|
32
|
+
const res = await driver.get('/.well-known/openwop');
|
|
33
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
34
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
35
|
+
const secrets = top && typeof top === 'object' ? top['secrets'] : undefined;
|
|
36
|
+
return Boolean(secrets && typeof secrets === 'object');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('aiEnvelope.redaction: advertisement shape (FINAL v1.1)', () => {
|
|
40
|
+
it('hosts advertising envelopeContracts AND secrets honor SR-1 carry-forward', async () => {
|
|
41
|
+
if (!(await isBYOKAdvertised())) return; // BYOK not advertised — skip
|
|
42
|
+
// The contract is invariant-based, not capability-flag-based. The
|
|
43
|
+
// advertisement-shape check is just "the host claims a BYOK surface";
|
|
44
|
+
// behavioral assertions below exercise the redaction invariant.
|
|
45
|
+
expect(true).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Behavioral assertions through the workflow-engine sample's env-gated
|
|
50
|
+
// `POST /v1/host/sample/envelope/accept` seam. The seam threads a
|
|
51
|
+
// `byokCanaries: string[]` option into AcceptOptions so the acceptor
|
|
52
|
+
// can apply the §"Redaction (SR-1 carry-forward)" pass on accepted
|
|
53
|
+
// envelopes, returning `redactedPayload` in the outcome. Each test
|
|
54
|
+
// soft-skips on HTTP 404 (host doesn't expose the seam).
|
|
55
|
+
async function accept(envelope: unknown, opts: Record<string, unknown> = {}): Promise<{ status: number; body: { status?: string; reason?: string; redactedPayload?: unknown; redactionCount?: number; details?: unknown[] } }> {
|
|
56
|
+
const res = await driver.post('/v1/host/sample/envelope/accept', { envelope, ...opts });
|
|
57
|
+
return { status: res.status, body: res.json as { status?: string; reason?: string; redactedPayload?: unknown; redactionCount?: number; details?: unknown[] } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const baseMeta = { source: 'ai-generation' as const, ts: '2026-05-18T10:00:00Z' };
|
|
61
|
+
const CANARY_VALUE = 'sk-canary-DO-NOT-LEAK-9d7f3a2b';
|
|
62
|
+
const CANARY_SECRET_ID = 'byok-secret-test-fixture-001';
|
|
63
|
+
const CANARIES = [{ value: CANARY_VALUE, secretId: CANARY_SECRET_ID }];
|
|
64
|
+
const CANONICAL_MARKER = `[REDACTED:${CANARY_SECRET_ID}]`;
|
|
65
|
+
|
|
66
|
+
describe('aiEnvelope.redaction: behavioral acceptor-level scrub (FINAL v1.1)', () => {
|
|
67
|
+
it('canary in payload → substituted with canonical SR-1 [REDACTED:<secretId>] marker per agent-memory.md:66', async () => {
|
|
68
|
+
const r = await accept(
|
|
69
|
+
{
|
|
70
|
+
type: 'error',
|
|
71
|
+
schemaVersion: 1,
|
|
72
|
+
envelopeId: 'env-red-1',
|
|
73
|
+
correlationId: 'r:n:0:red1',
|
|
74
|
+
payload: { code: 'leak_demo', message: `tool result included ${CANARY_VALUE} verbatim` },
|
|
75
|
+
meta: baseMeta,
|
|
76
|
+
},
|
|
77
|
+
{ byokCanaries: CANARIES },
|
|
78
|
+
);
|
|
79
|
+
if (r.status === 404) return;
|
|
80
|
+
expect(r.body.status).toBe('accepted');
|
|
81
|
+
expect(r.body.redactionCount, 'redactionCount MUST be > 0 when canary appears').toBeGreaterThan(0);
|
|
82
|
+
expect(
|
|
83
|
+
JSON.stringify(r.body.redactedPayload).includes(CANARY_VALUE),
|
|
84
|
+
driver.describe('ai-envelope.md §"Redaction (SR-1 carry-forward)"', 'canary plaintext MUST be absent from the redacted view'),
|
|
85
|
+
).toBe(false);
|
|
86
|
+
expect(
|
|
87
|
+
JSON.stringify(r.body.redactedPayload),
|
|
88
|
+
driver.describe('agent-memory.md §SR-1 line 66', 'persisted entry MUST carry [REDACTED:<secretId>] in place of the plaintext'),
|
|
89
|
+
).toContain(CANONICAL_MARKER);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('canary across nested object fields → all occurrences scrubbed with canonical marker', async () => {
|
|
93
|
+
const r = await accept(
|
|
94
|
+
{
|
|
95
|
+
type: 'clarification.request',
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
envelopeId: 'env-red-nested',
|
|
98
|
+
correlationId: 'r:n:0:rednested',
|
|
99
|
+
payload: {
|
|
100
|
+
questions: [
|
|
101
|
+
{ id: 'q1', question: `What is ${CANARY_VALUE}?` },
|
|
102
|
+
{ id: 'q2', question: 'unrelated', context: { trace: `${CANARY_VALUE}/${CANARY_VALUE}` } },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
meta: baseMeta,
|
|
106
|
+
},
|
|
107
|
+
{ byokCanaries: CANARIES },
|
|
108
|
+
);
|
|
109
|
+
if (r.status === 404) return;
|
|
110
|
+
expect(r.body.status).toBe('accepted');
|
|
111
|
+
expect(
|
|
112
|
+
JSON.stringify(r.body.redactedPayload).includes(CANARY_VALUE),
|
|
113
|
+
'no canary plaintext remnant anywhere in the redacted view (recursive scrub)',
|
|
114
|
+
).toBe(false);
|
|
115
|
+
// q1's question (1 occurrence), q2's context.trace (2 occurrences) = total 3
|
|
116
|
+
expect(r.body.redactionCount).toBe(3);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('multiple canaries → each substituted with its own secretId marker', async () => {
|
|
120
|
+
const C1 = { value: 'sk-canary-alpha-xxxx', secretId: 'secret-alpha' };
|
|
121
|
+
const C2 = { value: 'sk-canary-beta-yyyy', secretId: 'secret-beta' };
|
|
122
|
+
const r = await accept(
|
|
123
|
+
{
|
|
124
|
+
type: 'error',
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
envelopeId: 'env-red-multi',
|
|
127
|
+
correlationId: 'r:n:0:redmulti',
|
|
128
|
+
payload: { code: 'multi_leak', message: `first=${C1.value}, second=${C2.value}` },
|
|
129
|
+
meta: baseMeta,
|
|
130
|
+
},
|
|
131
|
+
{ byokCanaries: [C1, C2] },
|
|
132
|
+
);
|
|
133
|
+
if (r.status === 404) return;
|
|
134
|
+
expect(r.body.status).toBe('accepted');
|
|
135
|
+
const view = JSON.stringify(r.body.redactedPayload);
|
|
136
|
+
expect(view.includes(C1.value)).toBe(false);
|
|
137
|
+
expect(view.includes(C2.value)).toBe(false);
|
|
138
|
+
expect(
|
|
139
|
+
view.includes(`[REDACTED:${C1.secretId}]`) && view.includes(`[REDACTED:${C2.secretId}]`),
|
|
140
|
+
driver.describe('agent-memory.md §SR-1', 'each canary MUST be substituted with its OWN [REDACTED:<secretId>] marker'),
|
|
141
|
+
).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('redaction runs AFTER schema validation: payload with [REDACTED:...]-shaped substrings still validates', async () => {
|
|
145
|
+
// The error-kind payload schema requires { code, message }. A pre-redacted
|
|
146
|
+
// marker in the message MUST NOT trip validation.
|
|
147
|
+
const r = await accept(
|
|
148
|
+
{
|
|
149
|
+
type: 'error',
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
envelopeId: 'env-red-shape',
|
|
152
|
+
correlationId: 'r:n:0:redshape',
|
|
153
|
+
payload: { code: 'demo', message: 'already had [REDACTED:secret-prior] before we saw it' },
|
|
154
|
+
meta: baseMeta,
|
|
155
|
+
},
|
|
156
|
+
{ byokCanaries: CANARIES }, // canary NOT in payload; substitution count expected 0
|
|
157
|
+
);
|
|
158
|
+
if (r.status === 404) return;
|
|
159
|
+
expect(
|
|
160
|
+
r.body.status,
|
|
161
|
+
driver.describe('ai-envelope.md §"Redaction (SR-1 carry-forward)"', 'redaction MUST run AFTER schema validation; pre-existing markers do not affect validation'),
|
|
162
|
+
).toBe('accepted');
|
|
163
|
+
// No canary present → redactionCount absent or 0
|
|
164
|
+
expect(r.body.redactionCount ?? 0).toBe(0);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('canary in invalid envelope (validation refusal) → error response MUST NOT echo the canary plaintext', async () => {
|
|
168
|
+
// ISO 8601 violation triggers an `invalid` outcome BEFORE the redaction
|
|
169
|
+
// pass runs. The acceptor's validation-detail extractor MUST NOT echo
|
|
170
|
+
// the payload contents into the error response.
|
|
171
|
+
const r = await accept(
|
|
172
|
+
{
|
|
173
|
+
type: 'error',
|
|
174
|
+
schemaVersion: 1,
|
|
175
|
+
envelopeId: 'env-red-leak',
|
|
176
|
+
correlationId: 'r:n:0:redleak',
|
|
177
|
+
payload: { code: 'demo', message: `secret value is ${CANARY_VALUE}` },
|
|
178
|
+
meta: { ...baseMeta, ts: 'tomorrow' }, // bad ts → invalid
|
|
179
|
+
},
|
|
180
|
+
{ byokCanaries: CANARIES },
|
|
181
|
+
);
|
|
182
|
+
if (r.status === 404) return;
|
|
183
|
+
expect(r.body.status).toBe('invalid');
|
|
184
|
+
const bodyString = JSON.stringify(r.body);
|
|
185
|
+
expect(
|
|
186
|
+
bodyString.includes(CANARY_VALUE),
|
|
187
|
+
driver.describe(
|
|
188
|
+
'SECURITY/threat-model-secret-leakage.md §SR-1',
|
|
189
|
+
'error response on validation refusal MUST NOT echo BYOK canary plaintext',
|
|
190
|
+
),
|
|
191
|
+
).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// E.2 OTel scrape + E.3 debug-bundle seams.
|
|
196
|
+
import { queryTestSpans, exportDebugBundle, isOtelSeamAvailable } from '../lib/otel-scrape.js';
|
|
197
|
+
import { resetTestSeam } from '../lib/event-log-query.js';
|
|
198
|
+
|
|
199
|
+
describe('aiEnvelope.redaction: OTel + debug-bundle scrape (E.2 + E.3)', () => {
|
|
200
|
+
it('redacted canary plaintext MUST be absent from OTel envelope_* span attributes', async () => {
|
|
201
|
+
if (!(await isOtelSeamAvailable())) return;
|
|
202
|
+
const runId = `r-red-otel-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
203
|
+
await accept(
|
|
204
|
+
{
|
|
205
|
+
type: 'error',
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
envelopeId: 'env-red-otel-1',
|
|
208
|
+
correlationId: `${runId}:n:0:red-otel`,
|
|
209
|
+
payload: { code: 'leak_demo', message: `tool result included ${CANARY_VALUE} verbatim` },
|
|
210
|
+
meta: baseMeta,
|
|
211
|
+
},
|
|
212
|
+
{ byokCanaries: CANARIES, projectTo: { runId, nodeId: 'n' } },
|
|
213
|
+
);
|
|
214
|
+
const spans = await queryTestSpans({ runId });
|
|
215
|
+
if (!spans.ok) return;
|
|
216
|
+
const allAttrs = spans.data.flatMap((s) => Object.values(s.attributes).map((v) => String(v)));
|
|
217
|
+
expect(
|
|
218
|
+
allAttrs.some((v) => v.includes(CANARY_VALUE)),
|
|
219
|
+
driver.describe(
|
|
220
|
+
'SECURITY/threat-model-secret-leakage.md §SR-1',
|
|
221
|
+
'BYOK canary plaintext MUST NOT appear in any OTel envelope_* span attribute',
|
|
222
|
+
),
|
|
223
|
+
).toBe(false);
|
|
224
|
+
await resetTestSeam();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('redacted canary plaintext MUST be absent from debug-bundle export', async () => {
|
|
228
|
+
if (!(await isOtelSeamAvailable())) return;
|
|
229
|
+
const runId = `r-red-bundle-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
230
|
+
await accept(
|
|
231
|
+
{
|
|
232
|
+
type: 'clarification.request',
|
|
233
|
+
schemaVersion: 1,
|
|
234
|
+
envelopeId: 'env-red-bundle-1',
|
|
235
|
+
correlationId: `${runId}:n:0:red-bundle`,
|
|
236
|
+
payload: { questions: [{ id: 'q1', question: `embed ${CANARY_VALUE} here` }] },
|
|
237
|
+
meta: baseMeta,
|
|
238
|
+
},
|
|
239
|
+
{ byokCanaries: CANARIES, projectTo: { runId, nodeId: 'n' } },
|
|
240
|
+
);
|
|
241
|
+
const bundle = await exportDebugBundle(runId);
|
|
242
|
+
if (!bundle.ok) return;
|
|
243
|
+
const serialized = JSON.stringify(bundle.data);
|
|
244
|
+
expect(
|
|
245
|
+
serialized.includes(CANARY_VALUE),
|
|
246
|
+
driver.describe(
|
|
247
|
+
'SECURITY/threat-model-secret-leakage.md §SR-1',
|
|
248
|
+
'BYOK canary plaintext MUST NOT appear in the debug-bundle export (events + spans)',
|
|
249
|
+
),
|
|
250
|
+
).toBe(false);
|
|
251
|
+
await resetTestSeam();
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiEnvelope.schemaDrift — 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 envelopeContracts and the optional
|
|
7
|
+
* `envelopeStrictness` knob; behavioral assertions stay `it.todo()` until
|
|
8
|
+
* a reference host wires the accept path.
|
|
9
|
+
*
|
|
10
|
+
* Summary: an LLM emits an envelope whose `schemaVersion` is lower than the
|
|
11
|
+
* host's advertised floor for that kind (`Capabilities.schemaVersions[kind]`).
|
|
12
|
+
* Under `envelopeStrictness: "warn"` (default) the engine MUST attempt
|
|
13
|
+
* validation against the advertised version and log `envelope_schema_version_drift`.
|
|
14
|
+
* Under `envelopeStrictness: "strict"` the engine MUST refuse with
|
|
15
|
+
* `unknown_schema_version`. When the emitted `schemaVersion` is HIGHER than
|
|
16
|
+
* advertised, the engine MUST refuse regardless of strictness.
|
|
17
|
+
*
|
|
18
|
+
* @see spec/v1/ai-envelope.md §"Schema discipline"
|
|
19
|
+
* @see spec/v1/ai-envelope.md §"Capability handshake integration"
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from 'vitest';
|
|
23
|
+
import { driver } from '../lib/driver.js';
|
|
24
|
+
|
|
25
|
+
interface DiscoveryDoc {
|
|
26
|
+
capabilities?: Record<string, unknown>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function isEnvelopeContractsAdvertised(): Promise<boolean> {
|
|
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
|
+
return Boolean(block && block['advertised'] === true);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('aiEnvelope.schemaDrift: advertisement shape (FINAL v1.1)', () => {
|
|
38
|
+
it('capabilities.envelopeStrictness is either absent (treated as "warn") or "warn" | "strict"', async () => {
|
|
39
|
+
const res = await driver.get('/.well-known/openwop');
|
|
40
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
41
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
42
|
+
const val = top && typeof top === 'object' ? top['envelopeStrictness'] : undefined;
|
|
43
|
+
if (val === undefined) return; // absent → treated as 'warn'; skip
|
|
44
|
+
expect(
|
|
45
|
+
val === 'warn' || val === 'strict',
|
|
46
|
+
driver.describe(
|
|
47
|
+
'ai-envelope.md §"Capability handshake integration"',
|
|
48
|
+
'envelopeStrictness MUST be the literal string "warn" or "strict" when present',
|
|
49
|
+
),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('schemaVersions is non-empty when envelopeContracts.advertised: true', async () => {
|
|
54
|
+
if (!(await isEnvelopeContractsAdvertised())) return; // not opted in — skip
|
|
55
|
+
const res = await driver.get('/.well-known/openwop');
|
|
56
|
+
const body = res.json as { schemaVersions?: Record<string, number>; capabilities?: { schemaVersions?: Record<string, number> } } | undefined;
|
|
57
|
+
const versions = body?.schemaVersions ?? body?.capabilities?.schemaVersions ?? {};
|
|
58
|
+
expect(
|
|
59
|
+
Object.keys(versions).length > 0,
|
|
60
|
+
driver.describe(
|
|
61
|
+
'ai-envelope.md §"Schema version advertisement"',
|
|
62
|
+
'schemaVersions MUST be non-empty when envelopeContracts.advertised is true',
|
|
63
|
+
),
|
|
64
|
+
).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Behavioral assertions through the workflow-engine sample's env-gated
|
|
69
|
+
// `POST /v1/host/sample/envelope/accept` seam. The seam threads
|
|
70
|
+
// `schemaVersionFloor` + `envelopeStrictness` into AcceptOptions so the
|
|
71
|
+
// pure-function acceptor can apply the §"Schema discipline" gate.
|
|
72
|
+
// Each test soft-skips on HTTP 404 (host doesn't expose the seam).
|
|
73
|
+
async function accept(envelope: unknown, opts: Record<string, unknown> = {}): Promise<{ status: number; body: { status?: string; reason?: string; details?: unknown[] } }> {
|
|
74
|
+
const res = await driver.post('/v1/host/sample/envelope/accept', { envelope, ...opts });
|
|
75
|
+
return { status: res.status, body: res.json as { status?: string; reason?: string; details?: unknown[] } };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const baseMeta = { source: 'ai-generation' as const, ts: '2026-05-18T10:00:00Z' };
|
|
79
|
+
|
|
80
|
+
describe('aiEnvelope.schemaDrift: behavioral strictness gate (FINAL v1.1)', () => {
|
|
81
|
+
it('schemaVersion below advertised floor under strictness:"warn" → accepted (warn-and-continue)', async () => {
|
|
82
|
+
const r = await accept(
|
|
83
|
+
{
|
|
84
|
+
type: 'clarification.request',
|
|
85
|
+
schemaVersion: 0, // below the v1 floor
|
|
86
|
+
envelopeId: 'env-drift-warn',
|
|
87
|
+
correlationId: 'r:n:0:driftwarn',
|
|
88
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
89
|
+
meta: baseMeta,
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
schemaVersionFloor: { 'clarification.request': 1 },
|
|
93
|
+
envelopeStrictness: 'warn',
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
if (r.status === 404) return;
|
|
97
|
+
expect(
|
|
98
|
+
r.body.status,
|
|
99
|
+
driver.describe(
|
|
100
|
+
'ai-envelope.md §"Schema discipline"',
|
|
101
|
+
'below-floor schemaVersion under strictness:warn MUST be accepted (drift projected at engine level)',
|
|
102
|
+
),
|
|
103
|
+
).toBe('accepted');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('schemaVersion below advertised floor under strictness:"strict" → invalid unknown_schema_version', async () => {
|
|
107
|
+
const r = await accept(
|
|
108
|
+
{
|
|
109
|
+
type: 'clarification.request',
|
|
110
|
+
schemaVersion: 0,
|
|
111
|
+
envelopeId: 'env-drift-strict',
|
|
112
|
+
correlationId: 'r:n:0:driftstrict',
|
|
113
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
114
|
+
meta: baseMeta,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
schemaVersionFloor: { 'clarification.request': 1 },
|
|
118
|
+
envelopeStrictness: 'strict',
|
|
119
|
+
},
|
|
120
|
+
);
|
|
121
|
+
if (r.status === 404) return;
|
|
122
|
+
expect(
|
|
123
|
+
r.body.status,
|
|
124
|
+
driver.describe(
|
|
125
|
+
'ai-envelope.md §"Schema discipline"',
|
|
126
|
+
'below-floor schemaVersion under strictness:strict MUST refuse with unknown_schema_version',
|
|
127
|
+
),
|
|
128
|
+
).toBe('invalid');
|
|
129
|
+
expect(r.body.reason).toContain('unknown_schema_version');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('schemaVersion ABOVE advertised floor → invalid regardless of strictness (host doesn\'t know future version)', async () => {
|
|
133
|
+
for (const strictness of ['warn', 'strict'] as const) {
|
|
134
|
+
const r = await accept(
|
|
135
|
+
{
|
|
136
|
+
type: 'clarification.request',
|
|
137
|
+
schemaVersion: 99,
|
|
138
|
+
envelopeId: `env-drift-above-${strictness}`,
|
|
139
|
+
correlationId: `r:n:0:driftabove-${strictness}`,
|
|
140
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
141
|
+
meta: baseMeta,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
schemaVersionFloor: { 'clarification.request': 1 },
|
|
145
|
+
envelopeStrictness: strictness,
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
if (r.status === 404) return;
|
|
149
|
+
expect(
|
|
150
|
+
r.body.status,
|
|
151
|
+
driver.describe(
|
|
152
|
+
'ai-envelope.md §"Schema discipline"',
|
|
153
|
+
`above-floor schemaVersion MUST refuse regardless of strictness (got ${strictness})`,
|
|
154
|
+
),
|
|
155
|
+
).toBe('invalid');
|
|
156
|
+
expect(r.body.reason).toContain('unknown_schema_version');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('refused above-floor envelope carries instancePath /schemaVersion in details', async () => {
|
|
161
|
+
const r = await accept(
|
|
162
|
+
{
|
|
163
|
+
type: 'error',
|
|
164
|
+
schemaVersion: 5,
|
|
165
|
+
envelopeId: 'env-drift-details',
|
|
166
|
+
correlationId: 'r:n:0:driftdetails',
|
|
167
|
+
payload: { code: 'x', message: 'y' },
|
|
168
|
+
meta: baseMeta,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
schemaVersionFloor: { error: 1 },
|
|
172
|
+
envelopeStrictness: 'warn', // above-floor → invalid regardless
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
if (r.status === 404) return;
|
|
176
|
+
expect(r.body.status).toBe('invalid');
|
|
177
|
+
expect(Array.isArray(r.body.details)).toBe(true);
|
|
178
|
+
const paths = (r.body.details ?? []).map((d: unknown) => (d as { instancePath?: string }).instancePath);
|
|
179
|
+
expect(
|
|
180
|
+
paths.includes('/schemaVersion'),
|
|
181
|
+
driver.describe(
|
|
182
|
+
'ai-envelope.md §"Schema discipline"',
|
|
183
|
+
'schema-drift refusal MUST cite /schemaVersion as the violating field',
|
|
184
|
+
),
|
|
185
|
+
).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// E.2 OTel scrape seam.
|
|
190
|
+
import { queryTestSpans, isOtelSeamAvailable } from '../lib/otel-scrape.js';
|
|
191
|
+
import { resetTestSeam } from '../lib/event-log-query.js';
|
|
192
|
+
|
|
193
|
+
describe('aiEnvelope.schemaDrift: OTel drift attribute projection (E.2)', () => {
|
|
194
|
+
it('below-floor + strictness:warn → OTel span MUST carry envelope_schema_version_drift attribute', async () => {
|
|
195
|
+
if (!(await isOtelSeamAvailable())) return;
|
|
196
|
+
const runId = `r-drift-otel-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
197
|
+
const r = await accept(
|
|
198
|
+
{
|
|
199
|
+
type: 'clarification.request',
|
|
200
|
+
schemaVersion: 0, // below the v1 floor
|
|
201
|
+
envelopeId: 'env-drift-otel-1',
|
|
202
|
+
correlationId: `${runId}:n:0:drift-otel`,
|
|
203
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
204
|
+
meta: baseMeta,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
schemaVersionFloor: { 'clarification.request': 1 },
|
|
208
|
+
envelopeStrictness: 'warn',
|
|
209
|
+
projectTo: { runId, nodeId: 'n' },
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
if (r.status === 404) return;
|
|
213
|
+
expect(r.body.status).toBe('accepted');
|
|
214
|
+
|
|
215
|
+
const spans = await queryTestSpans({ runId });
|
|
216
|
+
if (!spans.ok) return;
|
|
217
|
+
expect(
|
|
218
|
+
spans.data.some((s) => s.attributes.envelope_schema_version_drift === true),
|
|
219
|
+
driver.describe(
|
|
220
|
+
'ai-envelope.md §"Schema discipline"',
|
|
221
|
+
'below-floor accept under strictness:warn MUST project envelope_schema_version_drift attribute on the OTel span',
|
|
222
|
+
),
|
|
223
|
+
).toBe(true);
|
|
224
|
+
await resetTestSeam();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aiEnvelope.trustBoundaryPropagation — 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. Behavioral assertions stay `it.todo()` until a
|
|
6
|
+
* reference host wires the MCP-tool-result → envelope → RunEventDoc trust path.
|
|
7
|
+
*
|
|
8
|
+
* Summary: when a node consumes content from an untrusted source (MCP tool
|
|
9
|
+
* result per `mcp-integration.md`, A2A inbound message per `a2a-integration.md`),
|
|
10
|
+
* any envelope it subsequently emits whose payload incorporates that content
|
|
11
|
+
* MUST carry `meta.contentTrust: "untrusted"`. The engine MUST propagate this
|
|
12
|
+
* onto every `RunEventDoc` emitted as a consequence (`RunEventDoc.contentTrust
|
|
13
|
+
* = "untrusted"`). Downstream LLM nodes re-consuming these events MUST treat
|
|
14
|
+
* the content as untrusted per `SECURITY/threat-model-prompt-injection.md`.
|
|
15
|
+
* Approval gates MUST refuse to advance on `untrusted` envelopes with refusal
|
|
16
|
+
* code `untrusted_content_blocks_approval`.
|
|
17
|
+
*
|
|
18
|
+
* @see spec/v1/ai-envelope.md §"Trust boundary"
|
|
19
|
+
* @see spec/v1/mcp-integration.md §"Trust boundary"
|
|
20
|
+
* @see spec/v1/a2a-integration.md §"Trust boundary"
|
|
21
|
+
* @see SECURITY/threat-model-prompt-injection.md
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
|
|
27
|
+
interface DiscoveryDoc {
|
|
28
|
+
capabilities?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readMcpTrustBoundary(): Promise<string | null> {
|
|
32
|
+
const res = await driver.get('/.well-known/openwop');
|
|
33
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
34
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
35
|
+
const mcp = top && typeof top === 'object' ? (top['mcpClient'] as Record<string, unknown> | undefined) : undefined;
|
|
36
|
+
if (!mcp || typeof mcp !== 'object') return null;
|
|
37
|
+
const tb = mcp['trustBoundary'];
|
|
38
|
+
return typeof tb === 'string' ? tb : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('aiEnvelope.trustBoundaryPropagation: advertisement shape (FINAL v1.1)', () => {
|
|
42
|
+
it('hosts advertising mcpClient declare trustBoundary as "untrusted"', async () => {
|
|
43
|
+
const tb = await readMcpTrustBoundary();
|
|
44
|
+
if (tb === null) return; // host doesn't advertise mcpClient — skip
|
|
45
|
+
expect(
|
|
46
|
+
tb,
|
|
47
|
+
driver.describe(
|
|
48
|
+
'mcp-integration.md §"Trust boundary"',
|
|
49
|
+
'mcpClient.trustBoundary MUST be "untrusted" — MCP tool results are always untrusted input',
|
|
50
|
+
),
|
|
51
|
+
).toBe('untrusted');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
async function accept(envelope: unknown, opts: Record<string, unknown> = {}): Promise<{ status: number; body: { status?: string; normalizedMeta?: { contentTrust?: string } } }> {
|
|
56
|
+
const res = await driver.post('/v1/host/sample/envelope/accept', { envelope, ...opts });
|
|
57
|
+
return { status: res.status, body: res.json as { status?: string; normalizedMeta?: { contentTrust?: string } } };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const baseMeta = { source: 'ai-generation' as const, ts: '2026-05-18T10:00:00Z' };
|
|
61
|
+
|
|
62
|
+
describe('aiEnvelope.trustBoundaryPropagation: behavioral normalization (FINAL v1.1)', () => {
|
|
63
|
+
it('envelope with meta.contentTrust:"untrusted" → normalizedMeta.contentTrust:"untrusted"', async () => {
|
|
64
|
+
const r = await accept({
|
|
65
|
+
type: 'clarification.request',
|
|
66
|
+
schemaVersion: 1,
|
|
67
|
+
envelopeId: 'env-tb-1',
|
|
68
|
+
correlationId: 'r:n:0:tb1',
|
|
69
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
70
|
+
meta: { ...baseMeta, contentTrust: 'untrusted' },
|
|
71
|
+
});
|
|
72
|
+
if (r.status === 404) return;
|
|
73
|
+
expect(r.body.status).toBe('accepted');
|
|
74
|
+
expect(
|
|
75
|
+
r.body.normalizedMeta?.contentTrust,
|
|
76
|
+
driver.describe(
|
|
77
|
+
'ai-envelope.md §"Trust boundary"',
|
|
78
|
+
'envelope-supplied contentTrust:"untrusted" MUST propagate to normalizedMeta',
|
|
79
|
+
),
|
|
80
|
+
).toBe('untrusted');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('envelope with no meta.contentTrust + runTrustBoundary:"untrusted" → normalizedMeta.contentTrust:"untrusted" (run-level propagation)', async () => {
|
|
84
|
+
const r = await accept(
|
|
85
|
+
{
|
|
86
|
+
type: 'clarification.request',
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
envelopeId: 'env-tb-2',
|
|
89
|
+
correlationId: 'r:n:0:tb2',
|
|
90
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
91
|
+
meta: baseMeta,
|
|
92
|
+
},
|
|
93
|
+
{ runTrustBoundary: 'untrusted' },
|
|
94
|
+
);
|
|
95
|
+
if (r.status === 404) return;
|
|
96
|
+
expect(r.body.normalizedMeta?.contentTrust).toBe('untrusted');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('envelope-supplied contentTrust takes precedence over runTrustBoundary (per-emission decision)', async () => {
|
|
100
|
+
const r = await accept(
|
|
101
|
+
{
|
|
102
|
+
type: 'clarification.request',
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
envelopeId: 'env-tb-3',
|
|
105
|
+
correlationId: 'r:n:0:tb3',
|
|
106
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
107
|
+
meta: { ...baseMeta, contentTrust: 'trusted' },
|
|
108
|
+
},
|
|
109
|
+
{ runTrustBoundary: 'untrusted' }, // explicit conflict — envelope wins
|
|
110
|
+
);
|
|
111
|
+
if (r.status === 404) return;
|
|
112
|
+
expect(
|
|
113
|
+
r.body.normalizedMeta?.contentTrust,
|
|
114
|
+
driver.describe(
|
|
115
|
+
'ai-envelope.md §"Trust boundary"',
|
|
116
|
+
'per-emission contentTrust MUST take precedence — trusted envelope emitted after MCP tool result does NOT inherit untrusted',
|
|
117
|
+
),
|
|
118
|
+
).toBe('trusted');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('no contentTrust + no runTrustBoundary → default "trusted"', async () => {
|
|
122
|
+
const r = await accept({
|
|
123
|
+
type: 'clarification.request',
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
envelopeId: 'env-tb-default',
|
|
126
|
+
correlationId: 'r:n:0:tbdef',
|
|
127
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
128
|
+
meta: baseMeta,
|
|
129
|
+
});
|
|
130
|
+
if (r.status === 404) return;
|
|
131
|
+
expect(r.body.normalizedMeta?.contentTrust).toBe('trusted');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// E.1 engine-projection via the test-only event-log seam.
|
|
136
|
+
import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
|
|
137
|
+
|
|
138
|
+
describe('aiEnvelope.trustBoundaryPropagation: engine projection via event-log seam', () => {
|
|
139
|
+
it('normalizedMeta.contentTrust:"untrusted" MUST project onto RunEventDoc.contentTrust', async () => {
|
|
140
|
+
if (!(await isEventLogSeamAvailable())) return;
|
|
141
|
+
const runId = `r-tb-proj-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
142
|
+
await accept(
|
|
143
|
+
{
|
|
144
|
+
type: 'clarification.request',
|
|
145
|
+
schemaVersion: 1,
|
|
146
|
+
envelopeId: 'env-tb-proj-1',
|
|
147
|
+
correlationId: `${runId}:n:0:tb-proj`,
|
|
148
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
149
|
+
meta: { ...baseMeta, contentTrust: 'untrusted' },
|
|
150
|
+
},
|
|
151
|
+
{ projectTo: { runId, nodeId: 'n' } },
|
|
152
|
+
);
|
|
153
|
+
const events = await queryTestEvents(runId, { type: 'interrupt.requested' });
|
|
154
|
+
if (!events.ok || events.events.length === 0) return;
|
|
155
|
+
expect(
|
|
156
|
+
events.events[0]!.contentTrust,
|
|
157
|
+
driver.describe(
|
|
158
|
+
'ai-envelope.md §"Trust boundary"',
|
|
159
|
+
'engine MUST project normalizedMeta.contentTrust:"untrusted" onto every consequent RunEventDoc.contentTrust',
|
|
160
|
+
),
|
|
161
|
+
).toBe('untrusted');
|
|
162
|
+
await resetTestSeam();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('trusted envelope projects RunEventDoc.contentTrust:"trusted" (default + explicit both verified)', async () => {
|
|
166
|
+
if (!(await isEventLogSeamAvailable())) return;
|
|
167
|
+
const runId = `r-tb-trusted-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
168
|
+
await accept(
|
|
169
|
+
{
|
|
170
|
+
type: 'clarification.request',
|
|
171
|
+
schemaVersion: 1,
|
|
172
|
+
envelopeId: 'env-tb-proj-trusted',
|
|
173
|
+
correlationId: `${runId}:n:0:tb-trusted`,
|
|
174
|
+
payload: { questions: [{ id: 'q1', question: 'why?' }] },
|
|
175
|
+
meta: baseMeta, // no contentTrust → default 'trusted'
|
|
176
|
+
},
|
|
177
|
+
{ projectTo: { runId, nodeId: 'n' } },
|
|
178
|
+
);
|
|
179
|
+
const events = await queryTestEvents(runId, { type: 'interrupt.requested' });
|
|
180
|
+
if (!events.ok || events.events.length === 0) return;
|
|
181
|
+
expect(events.events[0]!.contentTrust).toBe('trusted');
|
|
182
|
+
await resetTestSeam();
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('aiEnvelope.trustBoundaryPropagation: approval-gate refusal placeholder', () => {
|
|
187
|
+
// Approval-gate refusal (`untrusted_content_blocks_approval`) requires
|
|
188
|
+
// wiring the acceptor's normalizedMeta onto the engine's approval-gate
|
|
189
|
+
// resume handler. Tracked under Thread E.4 of the test-coverage plan
|
|
190
|
+
// (approval-gate refusal seam); the projection seam alone can't drive
|
|
191
|
+
// a resume-with-untrusted assertion.
|
|
192
|
+
it.todo('approval gate refuses to advance on untrusted envelope with untrusted_content_blocks_approval (needs approval-gate resume seam)');
|
|
193
|
+
it.todo('downstream LLM node re-consuming untrusted RunEventDoc applies <UNTRUSTED> wrap per prompt-injection invariant (needs node-execution seam)');
|
|
194
|
+
});
|