@openwop/openwop-conformance 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -2
  3. package/coverage.md +26 -14
  4. package/fixtures/conformance-agent-low-confidence.json +7 -4
  5. package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
  6. package/fixtures/conformance-agent-reasoning.json +23 -4
  7. package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
  8. package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
  9. package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
  10. package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
  11. package/fixtures/conformance-dispatch-input-mapping.json +49 -0
  12. package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
  13. package/fixtures/conformance-dispatch-output-mapping.json +49 -0
  14. package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
  15. package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
  16. package/fixtures.md +12 -2
  17. package/package.json +1 -1
  18. package/schemas/README.md +7 -0
  19. package/schemas/agent-ref.schema.json +1 -1
  20. package/schemas/ai-envelope.schema.json +106 -0
  21. package/schemas/capabilities.schema.json +248 -0
  22. package/schemas/core-conformance-mock-agent-config.schema.json +147 -0
  23. package/schemas/dispatch-config.schema.json +26 -0
  24. package/schemas/envelopes/clarification.request.schema.json +43 -0
  25. package/schemas/envelopes/error.schema.json +26 -0
  26. package/schemas/envelopes/schema.request.schema.json +22 -0
  27. package/schemas/envelopes/schema.response.schema.json +22 -0
  28. package/schemas/node-pack-manifest.schema.json +5 -0
  29. package/schemas/pack-lockfile.schema.json +16 -0
  30. package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
  31. package/src/lib/webhook-receiver.ts +137 -0
  32. package/src/lib/workflow-chain-expansion.ts +213 -0
  33. package/src/scenarios/agentPackCatalog.test.ts +216 -0
  34. package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
  35. package/src/scenarios/agentReasoningEvents.test.ts +58 -7
  36. package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
  37. package/src/scenarios/ai-envelope-shape.test.ts +362 -0
  38. package/src/scenarios/aiEnvelope.capBreached.test.ts +173 -0
  39. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
  40. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
  41. package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
  42. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
  43. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
  44. package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -0
  45. package/src/scenarios/append-ordering.test.ts +44 -0
  46. package/src/scenarios/artifact-auth.test.ts +58 -0
  47. package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
  48. package/src/scenarios/blob-presign-expiry.test.ts +66 -0
  49. package/src/scenarios/blob-roundtrip.test.ts +48 -0
  50. package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
  51. package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
  52. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
  53. package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
  54. package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
  55. package/src/scenarios/fs-path-traversal.test.ts +124 -0
  56. package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
  57. package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
  58. package/src/scenarios/kv-atomic-increment.test.ts +74 -0
  59. package/src/scenarios/kv-cas.test.ts +75 -0
  60. package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
  61. package/src/scenarios/kv-ttl-expiry.test.ts +47 -0
  62. package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
  63. package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
  64. package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
  65. package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
  66. package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
  67. package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
  68. package/src/scenarios/pause-resume.test.ts +43 -0
  69. package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
  70. package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
  71. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
  72. package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
  73. package/src/scenarios/spec-corpus-validity.test.ts +17 -1
  74. package/src/scenarios/sql-injection-rejection.test.ts +84 -0
  75. package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
  76. package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
  77. package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
  78. package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
  79. package/src/scenarios/table-cursor-pagination.test.ts +47 -0
  80. package/src/scenarios/table-schema-enforcement.test.ts +47 -0
  81. package/src/scenarios/vector-knn-roundtrip.test.ts +48 -0
  82. package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
  83. package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
  84. package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
  85. package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
  86. package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
@@ -0,0 +1,69 @@
1
+ /**
2
+ * aiEnvelope.correlationReplay — 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 accept path and the cross-process replay seam.
7
+ *
8
+ * Summary: two envelopes in the same run with the same `correlationId` MUST
9
+ * be treated as a re-emission. The second invocation returns the cached
10
+ * `EnvelopeOutcome` synchronously without re-invoking the handler. After
11
+ * process death + recovery, the engine MUST consult the run event log via
12
+ * `causationId = correlationId` and return the cached outcome — the handler
13
+ * runs at most once per `correlationId` per run lifetime. A re-emission with
14
+ * the same `correlationId` but a different `type` MUST be refused with
15
+ * `envelope_correlation_conflict`.
16
+ *
17
+ * @see spec/v1/ai-envelope.md §"Replay determinism"
18
+ * @see spec/v1/interrupt.md §"Replay determinism" (parallel contract)
19
+ */
20
+
21
+ import { describe, it, expect } from 'vitest';
22
+ import { driver } from '../lib/driver.js';
23
+
24
+ interface DiscoveryDoc {
25
+ capabilities?: Record<string, unknown>;
26
+ }
27
+
28
+ async function isEnvelopeContractsAdvertised(): Promise<boolean> {
29
+ const res = await driver.get('/.well-known/openwop');
30
+ const body = res.json as DiscoveryDoc | undefined;
31
+ const top = body?.capabilities as Record<string, unknown> | undefined;
32
+ const block = top && typeof top === 'object' ? (top['envelopeContracts'] as Record<string, unknown> | undefined) : undefined;
33
+ return Boolean(block && block['advertised'] === true);
34
+ }
35
+
36
+ describe('aiEnvelope.correlationReplay: advertisement shape (FINAL v1.1)', () => {
37
+ it('host that advertises envelopeContracts.advertised:true claims the replay-determinism contract', async () => {
38
+ if (!(await isEnvelopeContractsAdvertised())) return; // not opted in — skip
39
+ // The contract has no separate capability flag — advertising
40
+ // envelopeContracts is the claim. The behavioral assertions below
41
+ // exercise the contract; this advertisement-shape test exists so
42
+ // a "no envelope contracts at all" host doesn't appear in failure
43
+ // reports for this scenario.
44
+ expect(true).toBe(true);
45
+ });
46
+ });
47
+
48
+ describe('aiEnvelope.correlationReplay: engine-state placeholders', () => {
49
+ // The 4 assertions below require the engine to maintain a per-run
50
+ // correlationId → cached-outcome map AND project envelope acceptance
51
+ // onto RunEventDocs with `causationId = envelope.correlationId`.
52
+ //
53
+ // The reference workflow-engine sample's `acceptEnvelope` is a pure
54
+ // function (host/envelopeAcceptor.ts) — it validates + categorizes
55
+ // a single envelope without tracking state across calls. Promoting
56
+ // these to behavioral requires either:
57
+ // (a) extending the acceptor with an injected dedup store
58
+ // (per-run correlationId map keyed by runId), OR
59
+ // (b) a higher-level test seam that wires the acceptor into the
60
+ // run lifecycle + event log.
61
+ //
62
+ // (b) is the spec-faithful path (per ai-envelope.md §"Replay
63
+ // determinism" the dedup is engine-level, not acceptor-level).
64
+ // Tracked as host-impl follow-up.
65
+ it.todo('emit envelope twice with same correlationId → second returns cached outcome; no duplicate RunEventDocs');
66
+ it.todo('emit envelope with correlationId C, then with same C and different type → refuse envelope_correlation_conflict');
67
+ it.todo('cross-process replay: process-death after accept; recovered process re-emits same correlationId → cached outcome, no handler re-invocation');
68
+ it.todo('resulting RunEventDoc.causationId equals the envelope.correlationId (causal chain preserved)');
69
+ });
@@ -0,0 +1,73 @@
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
+ describe('aiEnvelope.redaction: BYOK-redaction placeholders', () => {
50
+ // The 6 assertions below require the engine's BYOK redaction pipeline
51
+ // (per SECURITY/threat-model-secret-leakage.md SR-1 carry-forward) to
52
+ // hook into envelope acceptance AND every downstream surface that
53
+ // persists envelope content (RunEventDoc, OTel span attributes,
54
+ // debug-bundle export, error envelope projection).
55
+ //
56
+ // The reference workflow-engine sample's `acceptEnvelope` is pure +
57
+ // doesn't touch payload contents. Redaction lives at a different
58
+ // layer (BYOK secretResolver + event-log sanitizer). Promoting these
59
+ // to behavioral requires either:
60
+ // (a) chaining the acceptor through `stripSecretsFromPersisted`
61
+ // before persisting the recorded view, OR
62
+ // (b) an end-to-end test that plants a BYOK canary in an envelope
63
+ // payload, runs through the full accept → emit → persist → export
64
+ // chain, and asserts the canary is absent on every output.
65
+ //
66
+ // (b) is the spec-faithful path. Tracked as host-impl follow-up.
67
+ it.todo('emit envelope whose payload contains a known BYOK substring → substring absent from emitted RunEventDocs');
68
+ it.todo('redacted substring absent from OTel envelope_* span attributes');
69
+ it.todo('redacted substring absent from debug-bundle export');
70
+ it.todo('redacted substring absent from error envelope on validation refusal (no leak via error path)');
71
+ it.todo('redaction marker is the canonical [REDACTED:<reason>] form, NOT a model-generated <REDACTED> string');
72
+ it.todo('redaction runs AFTER schema validation: a payload with redacted-shaped substrings still validates structurally');
73
+ });
@@ -0,0 +1,87 @@
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
+ describe('aiEnvelope.schemaDrift: engine-strictness placeholders', () => {
69
+ // The 4 assertions below require the engine to read both:
70
+ // (a) `Capabilities.schemaVersions[<kind>]` — the advertised floor
71
+ // version the host implements for the kind, AND
72
+ // (b) `Capabilities.envelopeStrictness` — the run-level knob that
73
+ // decides whether below-floor versions warn or refuse.
74
+ //
75
+ // The reference workflow-engine sample's `acceptEnvelope` validates
76
+ // `schemaVersion` as a top-level structural field but does NOT yet
77
+ // cross-reference it against the host's advertised floor or apply
78
+ // the strictness knob. Promoting these to behavioral requires
79
+ // threading both pieces of state through `AcceptOptions` (or making
80
+ // the acceptor close over a discovery snapshot). Tracked as host-
81
+ // impl follow-up; the OTel span attribute (`envelope_schema_version_drift`)
82
+ // is engine-projection scope.
83
+ it.todo('emit envelope with schemaVersion below advertised floor under strictness:"warn" → warn-and-continue');
84
+ it.todo('emit envelope with schemaVersion below advertised floor under strictness:"strict" → refuse unknown_schema_version');
85
+ it.todo('emit envelope with schemaVersion ABOVE advertised floor → refuse regardless of strictness');
86
+ it.todo('drift logs include envelope_schema_version_drift attribute on the OTel span');
87
+ });
@@ -0,0 +1,143 @@
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
+ describe('aiEnvelope.trustBoundaryPropagation: engine-integration placeholders', () => {
136
+ // These require the engine to project normalizedMeta.contentTrust
137
+ // onto RunEventDoc.contentTrust + enforce the approval-gate refusal
138
+ // path. The pure-function acceptor surfaces normalizedMeta; engine
139
+ // wiring is host-impl scope.
140
+ it.todo('engine projects normalizedMeta.contentTrust onto RunEventDoc.contentTrust');
141
+ it.todo('approval gate refuses to advance on untrusted envelope with untrusted_content_blocks_approval');
142
+ it.todo('downstream LLM node re-consuming untrusted RunEventDoc applies <UNTRUSTED> wrap per prompt-injection invariant');
143
+ });
@@ -0,0 +1,176 @@
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
+ describe('aiEnvelope.universalKinds: engine-integration placeholders', () => {
168
+ // These assert behaviors beyond the pure-function acceptor — they
169
+ // need the engine to lift envelopes into interrupts / re-inject
170
+ // schemas / emit log.appended events. Tracked separately; the
171
+ // acceptor seam above covers the 5 wire-level assertions.
172
+ it.todo('lift clarification.request to kind:"clarification" interrupt per interrupt.md');
173
+ it.todo('schema.request triggers next-turn schema re-injection (host responsibility)');
174
+ it.todo('schema.response counted (or exempt) against limits.envelopesPerTurn per host policy');
175
+ it.todo('error envelope projects to log.appended (level: "error"), NOT node.failed');
176
+ });
@@ -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)}`);