@openwop/openwop-conformance 1.6.1 → 1.10.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 (159) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +57 -0
  4. package/api/openapi.yaml +250 -0
  5. package/coverage.md +14 -0
  6. package/fixtures/conformance-run-duration-breach.json +33 -0
  7. package/fixtures.md +19 -0
  8. package/package.json +1 -1
  9. package/schemas/README.md +10 -0
  10. package/schemas/agent-inventory-response.schema.json +90 -0
  11. package/schemas/ai-envelope.schema.json +28 -0
  12. package/schemas/artifact-type-pack-manifest.schema.json +160 -0
  13. package/schemas/capabilities.schema.json +171 -4
  14. package/schemas/chat-card-pack-manifest.schema.json +158 -0
  15. package/schemas/envelopes/media.audio.schema.json +38 -0
  16. package/schemas/envelopes/media.file.schema.json +37 -0
  17. package/schemas/envelopes/media.image.schema.json +33 -0
  18. package/schemas/heartbeat-evaluated.schema.json +14 -0
  19. package/schemas/heartbeat-state-changed.schema.json +14 -0
  20. package/schemas/node-pack-manifest.schema.json +16 -1
  21. package/schemas/run-event-payloads.schema.json +96 -5
  22. package/schemas/run-event.schema.json +4 -0
  23. package/schemas/workflow-definition.schema.json +5 -0
  24. package/schemas/workspace-file-create.schema.json +20 -0
  25. package/schemas/workspace-file.schema.json +39 -0
  26. package/src/lib/agentLoop.ts +44 -0
  27. package/src/lib/agentRuntime.ts +45 -0
  28. package/src/lib/artifactTypes.ts +96 -0
  29. package/src/lib/cardPacks.ts +52 -0
  30. package/src/lib/discovery-capabilities.ts +50 -0
  31. package/src/lib/distillation.ts +38 -0
  32. package/src/lib/feedback.ts +3 -3
  33. package/src/lib/heartbeat.ts +31 -0
  34. package/src/lib/memoryAttribution.ts +48 -0
  35. package/src/lib/subRunAttestation.ts +35 -0
  36. package/src/lib/toolHooks.ts +33 -0
  37. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
  38. package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
  39. package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
  40. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
  41. package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
  42. package/src/scenarios/ai-envelope-shape.test.ts +14 -18
  43. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
  44. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
  45. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
  46. package/src/scenarios/approval-gate-flow.test.ts +4 -6
  47. package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
  48. package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
  49. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
  50. package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
  51. package/src/scenarios/audit-log-integrity.test.ts +3 -2
  52. package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
  53. package/src/scenarios/auth-mtls.test.ts +2 -1
  54. package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
  55. package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
  56. package/src/scenarios/auth-saml-profile.test.ts +2 -1
  57. package/src/scenarios/auth-scim-profile.test.ts +2 -1
  58. package/src/scenarios/authorization-fail-closed.test.ts +2 -1
  59. package/src/scenarios/authorization-roles-shape.test.ts +2 -1
  60. package/src/scenarios/byok-auth-modes.test.ts +141 -0
  61. package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
  62. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
  63. package/src/scenarios/commitment-fired.test.ts +83 -0
  64. package/src/scenarios/credential-payload-redaction.test.ts +2 -1
  65. package/src/scenarios/credentials-capability-shape.test.ts +2 -1
  66. package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
  67. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
  68. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  69. package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
  70. package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
  71. package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
  72. package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
  73. package/src/scenarios/distillation-shape.test.ts +41 -0
  74. package/src/scenarios/distillation-stable-archive.test.ts +37 -0
  75. package/src/scenarios/distillation-token-budget.test.ts +45 -0
  76. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
  77. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
  78. package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
  79. package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
  80. package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
  81. package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
  82. package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
  83. package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
  84. package/src/scenarios/experimental-tier-shape.test.ts +5 -4
  85. package/src/scenarios/fs-path-traversal.test.ts +2 -1
  86. package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
  87. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
  88. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
  89. package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
  90. package/src/scenarios/http-client-ssrf.test.ts +10 -13
  91. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
  92. package/src/scenarios/media-url-inline-cap.test.ts +167 -0
  93. package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
  94. package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
  95. package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
  96. package/src/scenarios/memory-attribution-shape.test.ts +28 -0
  97. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
  98. package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
  99. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
  100. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
  101. package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
  102. package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
  103. package/src/scenarios/model-capability-substituted.test.ts +2 -1
  104. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  105. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
  106. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
  107. package/src/scenarios/multi-region-idempotency.test.ts +10 -10
  108. package/src/scenarios/oauth-capability-shape.test.ts +2 -1
  109. package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
  110. package/src/scenarios/pause-resume.test.ts +3 -3
  111. package/src/scenarios/production-backpressure.test.ts +2 -2
  112. package/src/scenarios/production-retention-expiry.test.ts +2 -2
  113. package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
  114. package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
  115. package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
  116. package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
  117. package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
  118. package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
  119. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
  120. package/src/scenarios/prompt-pack-install.test.ts +2 -1
  121. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
  122. package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
  123. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
  124. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
  125. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
  126. package/src/scenarios/prompt-template-shape.test.ts +2 -1
  127. package/src/scenarios/provider-usage.test.ts +2 -1
  128. package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
  129. package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
  130. package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
  131. package/src/scenarios/replayDeterminism.test.ts +3 -1
  132. package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
  133. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  134. package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
  135. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  136. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  137. package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
  138. package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
  139. package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
  140. package/src/scenarios/spec-corpus-validity.test.ts +1 -1
  141. package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
  142. package/src/scenarios/subrun-approval-gate.test.ts +35 -0
  143. package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
  144. package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
  145. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
  146. package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
  147. package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
  148. package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
  149. package/src/scenarios/tool-hooks-shape.test.ts +34 -0
  150. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
  151. package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
  152. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
  153. package/src/scenarios/wasm-pack-load.test.ts +2 -2
  154. package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
  155. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
  156. package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
  157. package/src/scenarios/workspace-behavior.test.ts +134 -0
  158. package/src/scenarios/workspace-capability-shape.test.ts +73 -0
  159. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Chat card pack manifest validation — `chat-card-packs.md` §"Manifest format"
3
+ * + `schemas/chat-card-pack-manifest.schema.json` (RFC 0071 Phase 2).
4
+ *
5
+ * Server-free schema-validation scenario for `kind: "card"` packs:
6
+ * 1. Positive: a valid card manifest validates.
7
+ * 2. Negative — kind/contents mismatch: cards[] + a foreign artifactTypes[]
8
+ * is rejected (additionalProperties -> pack_kind_invalid at the registry).
9
+ * 3. Negative — empty cards[] (minItems).
10
+ * 4. Negative — invalid cardTypeId (uppercase scope -> pattern).
11
+ * 5. Negative — a card missing prompt (required).
12
+ * 6. Negative — a non-portable inputs[].type that is neither in the closed
13
+ * enum nor a vendor-prefixed extension (`canvas-reference` -> pattern).
14
+ * 7. Positive — a vendor.*-prefixed inputs[].type extension is tolerated.
15
+ *
16
+ * Behavioral execution (`chat-card-pack-execution.test.ts` — prompt routed
17
+ * through ctx.aiEnvelope.generate, output validated against the linked
18
+ * outputArtifactType, untrusted-input trust-tag propagation) is the Phase-2
19
+ * `Active` gate (R2) and lands with a host advertising `host.chat.cardPacks`.
20
+ *
21
+ * @see spec/v1/chat-card-packs.md
22
+ * @see schemas/chat-card-pack-manifest.schema.json
23
+ * @see RFCS/0071-artifact-type-and-chat-card-packs.md
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { readFileSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import Ajv2020 from 'ajv/dist/2020.js';
30
+ import addFormats from 'ajv-formats';
31
+ import type { ErrorObject } from 'ajv';
32
+ import { SCHEMAS_DIR } from '../lib/paths.js';
33
+
34
+ const SCHEMA_PATH = join(SCHEMAS_DIR, 'chat-card-pack-manifest.schema.json');
35
+
36
+ function validManifest() {
37
+ return {
38
+ kind: 'card',
39
+ name: 'vendor.acme.cad-cards',
40
+ version: '1.0.0',
41
+ engines: { openwop: '>=1.1' },
42
+ cards: [
43
+ {
44
+ cardTypeId: 'vendor.acme.cad.model.create',
45
+ prompt: {
46
+ template: 'Design a model for: {{spec}}',
47
+ placeholderMapping: { spec: 'inputs.spec' },
48
+ temperature: 0.2,
49
+ },
50
+ inputs: [{ id: 'spec', type: 'text', label: 'Part spec', required: true }],
51
+ outputArtifactType: 'vendor.acme.cad.model',
52
+ outputSchemaRef: 'schemas/cad-model.schema.json',
53
+ },
54
+ ],
55
+ };
56
+ }
57
+
58
+ describe('category: chat-card-pack manifest validation', () => {
59
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
60
+ addFormats(ajv);
61
+ const validate = ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')));
62
+
63
+ const failsWith = (manifest: unknown, keyword: string): ErrorObject[] => {
64
+ expect(validate(manifest)).toBe(false);
65
+ return (validate.errors ?? []).filter((e) => e.keyword === keyword);
66
+ };
67
+
68
+ it('positive: a valid chat card pack manifest validates cleanly', () => {
69
+ expect(
70
+ validate(validManifest()),
71
+ `chat-card-packs.md §"Manifest format": a well-formed kind:"card" manifest MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
72
+ ).toBe(true);
73
+ });
74
+
75
+ it('negative: a manifest mixing cards[] and artifactTypes[] is rejected', () => {
76
+ const manifest = { ...validManifest(), artifactTypes: [{ artifactTypeId: 'vendor.acme.x', schemaRef: 'x.json' }] };
77
+ const errs = failsWith(manifest, 'additionalProperties');
78
+ expect(
79
+ errs.some((e) => (e.params as { additionalProperty?: string }).additionalProperty === 'artifactTypes'),
80
+ 'chat-card-packs.md §"Pack kind": one kind per pack (additionalProperties:false)',
81
+ ).toBe(true);
82
+ });
83
+
84
+ it('negative: an empty cards[] is rejected', () => {
85
+ expect(failsWith({ ...validManifest(), cards: [] }, 'minItems').length).toBeGreaterThan(0);
86
+ });
87
+
88
+ it('negative: an uppercase-scope cardTypeId is rejected', () => {
89
+ const m = validManifest();
90
+ m.cards[0]!.cardTypeId = 'Vendor.Acme.Card';
91
+ expect(failsWith(m, 'pattern').length).toBeGreaterThan(0);
92
+ });
93
+
94
+ it('negative: a card missing prompt is rejected', () => {
95
+ const m = validManifest();
96
+ delete (m.cards[0] as { prompt?: unknown }).prompt;
97
+ expect(failsWith(m, 'required').length).toBeGreaterThan(0);
98
+ });
99
+
100
+ it('negative: a non-portable inputs[].type (canvas-reference) is rejected', () => {
101
+ const m = validManifest();
102
+ m.cards[0]!.inputs[0]!.type = 'canvas-reference';
103
+ expect(
104
+ failsWith(m, 'pattern').length,
105
+ 'chat-card-packs.md §"Input fields": type is the closed portable enum OR a vendor.*/x- extension',
106
+ ).toBeGreaterThan(0);
107
+ });
108
+
109
+ it('positive: a vendor.*-prefixed inputs[].type extension is tolerated', () => {
110
+ const m = validManifest();
111
+ m.cards[0]!.inputs[0]!.type = 'vendor.myndhyve.canvas-ref';
112
+ expect(
113
+ validate(m),
114
+ 'chat-card-packs.md §"Input fields": a vendor.<org>.<kind> input type extension MUST validate (other hosts ignore it)',
115
+ ).toBe(true);
116
+ });
117
+
118
+ it('positive: the full portable inputs[].type subset validates (G9, incl. multiselect + file)', () => {
119
+ for (const t of ['text', 'longtext', 'number', 'boolean', 'select', 'multiselect', 'file', 'artifact-ref']) {
120
+ const m = validManifest();
121
+ m.cards[0]!.inputs[0]!.type = t;
122
+ expect(
123
+ validate(m),
124
+ `chat-card-packs.md §"Input fields": portable inputs[].type "${t}" MUST validate (G9 resolved 2026-05-27)`,
125
+ ).toBe(true);
126
+ }
127
+ });
128
+ });
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Inferred standing commitment — fire-once + content-free (RFC 0068, `Draft`).
3
+ *
4
+ * Gated on `capabilities.agents.commitments.supported`. Drives the
5
+ * documented host seam `POST /v1/host/sample/commitment/fire` (staged per
6
+ * the RFC 0027 §G precedent — soft-skips on 404/501 until a reference host
7
+ * wires it). Asserts:
8
+ * - a fired commitment emits a content-free `commitment.fired` carrying
9
+ * `commitmentId` + `memoryRef` provenance + `condition` (RFC 0068 §C);
10
+ * - the event MUST NOT carry the inferred intention text (no-content);
11
+ * - the commitment fires at most once per satisfied condition.
12
+ *
13
+ * Hosts that omit the capability skip cleanly.
14
+ *
15
+ * Spec references:
16
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Inferred commitments"
17
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { driver } from '../lib/driver.js';
22
+
23
+ interface CommitmentCaps {
24
+ agents?: { commitments?: { supported?: boolean } };
25
+ }
26
+
27
+ interface FireResult {
28
+ event?: {
29
+ commitmentId?: string;
30
+ memoryRef?: string;
31
+ condition?: string;
32
+ [k: string]: unknown;
33
+ };
34
+ fireCount?: number;
35
+ /** The plaintext intention the host inferred — used only to assert it does NOT appear on the event. */
36
+ intentionCanary?: string;
37
+ }
38
+
39
+ async function commitmentsSupported(): Promise<boolean> {
40
+ const res = await driver.get('/.well-known/openwop', { authenticated: false });
41
+ if (res.status !== 200) return false;
42
+ return Boolean((res.json as CommitmentCaps).agents?.commitments?.supported);
43
+ }
44
+
45
+ describe('commitment-fired: fire contract (RFC 0068 §C, capability-gated)', () => {
46
+ it('a fired commitment emits a content-free event with memory provenance, exactly once', async () => {
47
+ if (!(await commitmentsSupported())) return; // capability absent — gated skip
48
+
49
+ const res = await driver.post('/v1/host/sample/commitment/fire', {
50
+ memoryRef: 'mem://conformance/commitments',
51
+ condition: 'predicate',
52
+ includeIntentionCanary: true,
53
+ });
54
+ if (res.status === 404 || res.status === 501) return; // seam not wired — soft-skip
55
+
56
+ expect(res.status, driver.describe('RFC 0068 §C', 'an advertised commitment seam MUST succeed')).toBe(200);
57
+ const r = res.json as FireResult;
58
+
59
+ // §C — required identifiers.
60
+ expect(r.event?.commitmentId, driver.describe('RFC 0068 §C', 'commitment.fired MUST carry commitmentId')).toBeTruthy();
61
+ expect(
62
+ r.event?.memoryRef,
63
+ driver.describe('RFC 0068 §C.1', 'commitment.fired MUST carry the source memoryRef (CTI-1 provenance)'),
64
+ ).toBeTruthy();
65
+
66
+ // §C.3 — content-free: the inferred intention text MUST NOT appear on the event.
67
+ if (typeof r.intentionCanary === 'string' && r.intentionCanary.length > 0) {
68
+ const serialized = JSON.stringify(r.event ?? {});
69
+ expect(
70
+ serialized.includes(r.intentionCanary),
71
+ driver.describe('RFC 0068 §C.3', 'the inferred intention text MUST NOT appear on the commitment.fired payload'),
72
+ ).toBe(false);
73
+ }
74
+
75
+ // §C.2 — fire-once-per-condition (when the seam reports a count).
76
+ if (typeof r.fireCount === 'number') {
77
+ expect(
78
+ r.fireCount,
79
+ driver.describe('RFC 0068 §C.2', 'a commitment MUST fire at most once per satisfied condition'),
80
+ ).toBeLessThanOrEqual(1);
81
+ }
82
+ });
83
+ });
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
28
  import { driver } from '../lib/driver.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  interface DiscoveryCredentials {
31
32
  supported?: boolean;
@@ -43,7 +44,7 @@ const CANARY = 'OPENWOP_CRED_CANARY_4f1c8a2e9b';
43
44
  async function readCredentials(): Promise<DiscoveryCredentials | null> {
44
45
  const res = await driver.get('/.well-known/openwop');
45
46
  const body = res.json as DiscoveryDoc | undefined;
46
- return body?.capabilities?.credentials ?? null;
47
+ return capabilityFamily(body, 'credentials') ?? null;
47
48
  }
48
49
 
49
50
  describe('credential-payload-redaction: advertisement shape (RFC 0046 §A)', () => {
@@ -21,6 +21,7 @@
21
21
 
22
22
  import { describe, it, expect } from 'vitest';
23
23
  import { driver } from '../lib/driver.js';
24
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
24
25
 
25
26
  interface DiscoveryCredentials {
26
27
  supported?: boolean;
@@ -42,7 +43,7 @@ const VALID_ROTATION: ReadonlySet<string> = new Set(['none', 'two-key-overlap'])
42
43
  async function readCredentials(): Promise<DiscoveryCredentials | null> {
43
44
  const res = await driver.get('/.well-known/openwop');
44
45
  const body = res.json as DiscoveryDoc | undefined;
45
- return body?.capabilities?.credentials ?? null;
46
+ return capabilityFamily(body, 'credentials') ?? null;
46
47
  }
47
48
 
48
49
  describe('credentials-capability-shape: advertisement shape (RFC 0046 §A)', () => {
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
28
  import { driver } from '../lib/driver.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
31
32
  const ORDERING_MODELS = new Set(['lamport', 'vector-clock', 'global-sequencer']);
@@ -55,7 +56,7 @@ describe.skipIf(HTTP_SKIP)('cross-engine-append-ordering: advertisement shape (R
55
56
  it('capabilities.eventLog.crossEngineOrdering (when present) conforms to RFC 0036 §B', async () => {
56
57
  const d = await readDiscovery();
57
58
  if (d === null) return;
58
- const ceo = d.capabilities?.eventLog?.crossEngineOrdering;
59
+ const ceo = capabilityFamily<{ crossEngineOrdering?: { supported?: unknown; orderingModel?: unknown } }>(d, 'eventLog')?.crossEngineOrdering;
59
60
  if (ceo === undefined) return; // host doesn't advertise — soft-skip
60
61
 
61
62
  expect(
@@ -35,6 +35,7 @@
35
35
 
36
36
  import { describe, it, expect } from 'vitest';
37
37
  import { driver } from '../lib/driver.js';
38
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
38
39
 
39
40
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
40
41
 
@@ -65,7 +66,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
65
66
  describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040 §C)', () => {
66
67
  it('hosts advertising ancestryEndpointSupported MUST serve GET /v1/runs/{runId}/ancestry with the documented shape on a top-level run', async (ctx) => {
67
68
  const d = await readDiscovery();
68
- const chc = d?.capabilities?.multiAgent?.executionModel?.crossHostCausation;
69
+ const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
69
70
  if (chc?.ancestryEndpointSupported !== true) {
70
71
  ctx.skip();
71
72
  return;
@@ -112,7 +113,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040
112
113
 
113
114
  it('hosts advertising crossHostCausation.supported but NOT ancestryEndpointSupported MUST return 404 from the ancestry endpoint', async (ctx) => {
114
115
  const d = await readDiscovery();
115
- const chc = d?.capabilities?.multiAgent?.executionModel?.crossHostCausation;
116
+ const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
116
117
  if (chc?.supported !== true) {
117
118
  ctx.skip();
118
119
  return;
@@ -27,6 +27,7 @@
27
27
 
28
28
  import { describe, it, expect } from 'vitest';
29
29
  import { driver } from '../lib/driver.js';
30
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
30
31
 
31
32
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
32
33
 
@@ -63,7 +64,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-causation-shape: advertisement shape (RFC
63
64
  ctx.skip();
64
65
  return;
65
66
  }
66
- const chc = d.capabilities?.multiAgent?.executionModel?.crossHostCausation;
67
+ const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
67
68
  if (chc === undefined) {
68
69
  ctx.skip(); // host doesn't advertise — soft-skip
69
70
  return;
@@ -78,7 +79,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-causation-shape: advertisement shape (RFC
78
79
  ).toBe('boolean');
79
80
 
80
81
  if (chc.supported === true) {
81
- const version = d.capabilities?.multiAgent?.executionModel?.version as number | undefined;
82
+ const version = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version as number | undefined;
82
83
  expect(
83
84
  typeof version === 'number' && version >= 3,
84
85
  driver.describe(
@@ -19,6 +19,7 @@
19
19
 
20
20
  import { describe, it, expect } from 'vitest';
21
21
  import { driver } from '../lib/driver.js';
22
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
22
23
 
23
24
  interface DiscoveryDeadLetter {
24
25
  supported?: boolean;
@@ -32,7 +33,7 @@ interface DiscoveryDoc {
32
33
  async function readDeadLetter(): Promise<DiscoveryDeadLetter | null> {
33
34
  const res = await driver.get('/.well-known/openwop');
34
35
  const body = res.json as DiscoveryDoc | undefined;
35
- return body?.capabilities?.deadLetter ?? null;
36
+ return capabilityFamily(body, 'deadLetter') ?? null;
36
37
  }
37
38
 
38
39
  describe('deadletter-capability-shape: advertisement shape (RFC 0053 §A)', () => {
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { describe, it, expect } from 'vitest';
25
25
  import { driver } from '../lib/driver.js';
26
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
26
27
 
27
28
  interface DiscoveryDoc {
28
29
  capabilities?: { deadLetter?: { supported?: boolean } };
@@ -30,7 +31,7 @@ interface DiscoveryDoc {
30
31
 
31
32
  async function deadLetterSupported(): Promise<boolean> {
32
33
  const res = await driver.get('/.well-known/openwop');
33
- return (res.json as DiscoveryDoc | undefined)?.capabilities?.deadLetter?.supported === true;
34
+ return capabilityFamily((res.json as DiscoveryDoc | undefined), 'deadLetter')?.supported === true;
34
35
  }
35
36
 
36
37
  describe('deadletter-retry-exhaustion: retry exhaustion → dead-lettered + fork-eligible (RFC 0053 §C)', () => {
@@ -0,0 +1,35 @@
1
+ /**
2
+ * distillation-index-roundtrip — RFC 0062 §B(5). After distillation the
3
+ * memory-index workspace file (`MEMORY-INDEX.json`, RFC 0059) is retrievable and
4
+ * the run reported updating the index (rides `workspace.updated`, not a bespoke
5
+ * index event).
6
+ *
7
+ * Gated on `capabilities.memory.distillation.supported` + `indexEmitted` + the
8
+ * host memory-distillation seam; soft-skips when any is absent. (The seam echoes
9
+ * the index file, so this scenario does not separately require the workspace
10
+ * read endpoint to be wired.)
11
+ *
12
+ * @see RFCS/0062-scheduled-memory-distillation.md §B
13
+ * @see RFCS/0059-agent-workspace.md — the durable layer the index rides
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+ import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
19
+
20
+ describe('distillation-index-roundtrip (RFC 0062 §B)', () => {
21
+ it('an indexEmitted run updates a retrievable memory-index manifest', async () => {
22
+ const cap = await readDistillationCap();
23
+ if (cap?.supported !== true || cap?.indexEmitted !== true) return;
24
+ const res = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000, indexEmitted: true });
25
+ if (res === null) return; // seam absent — soft-skip
26
+ expect(
27
+ res.body.indexUpdated === true || res.body.event?.distillation?.indexUpdated === true,
28
+ driver.describe('RFC 0062 §B', 'an indexEmitted distillation MUST report updating the memory index'),
29
+ ).toBe(true);
30
+ expect(
31
+ res.body.indexFile !== undefined && res.body.indexFile !== null,
32
+ driver.describe('RFC 0062 §B', 'the MEMORY-INDEX.json manifest MUST be retrievable after distillation'),
33
+ ).toBe(true);
34
+ });
35
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * distillation-secret-carryforward — RFC 0062 §B(3). A redacted secret in
3
+ * source memory stays redacted in the distilled archive — the SR-1 carry-forward
4
+ * invariant (RFC 0012 §D) holds through distillation; the raw value never appears
5
+ * in the archive or the emitted `memory.compacted` event.
6
+ *
7
+ * Gated on `capabilities.memory.distillation.supported` + the host memory-
8
+ * distillation seam; soft-skips when either is absent.
9
+ *
10
+ * @see RFCS/0062-scheduled-memory-distillation.md §B
11
+ * @see spec/v1/agent-memory.md §SR-1 — Secret-Redaction Invariant
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { driver } from '../lib/driver.js';
16
+ import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
17
+
18
+ const CANARY = 'sk-canary-rfc0062-do-not-leak-qrs456';
19
+
20
+ describe('distillation-secret-carryforward (RFC 0062 §B)', () => {
21
+ it('a redacted secret in source memory never appears in the distilled output', async () => {
22
+ if ((await readDistillationCap())?.supported !== true) return;
23
+ const res = await invokeDistill({
24
+ memoryRef: 'conformance-distill',
25
+ tokenBudget: 8000,
26
+ includeSecretCanary: true,
27
+ sources: [{ content: `notes with embedded secret ${CANARY}` }],
28
+ });
29
+ if (res === null) return; // seam absent — soft-skip
30
+ expect(
31
+ JSON.stringify(res.body).includes(CANARY),
32
+ driver.describe('RFC 0062 §B', 'SR-1 carry-forward: a redacted secret MUST NOT re-appear in the archive or memory.compacted event'),
33
+ ).toBe(false);
34
+ });
35
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * distillation-shape — RFC 0062 §A. The `capabilities.memory.distillation`
3
+ * advertisement block is either absent or a well-formed object (with a positive
4
+ * `maxTokenBudget` when present).
5
+ *
6
+ * Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
7
+ * in the sibling distillation-*.test.ts scenarios, gated on `supported` + the
8
+ * host memory-distillation seam.
9
+ *
10
+ * @see RFCS/0062-scheduled-memory-distillation.md §A
11
+ * @see spec/v1/agent-memory.md §"Scheduled distillation"
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { driver } from '../lib/driver.js';
16
+ import { readDistillationCap } from '../lib/distillation.js';
17
+
18
+ describe('distillation-shape: advertisement (RFC 0062 §A)', () => {
19
+ it('capabilities.memory.distillation is absent or a well-formed object', async () => {
20
+ const cap = await readDistillationCap();
21
+ if (cap === null) return; // not advertised — valid
22
+ expect(
23
+ typeof cap.supported,
24
+ driver.describe('capabilities.schema.json §memory.distillation', 'distillation.supported MUST be a boolean when the block is present'),
25
+ ).toBe('boolean');
26
+ if (cap.maxTokenBudget !== undefined) {
27
+ expect(
28
+ typeof cap.maxTokenBudget === 'number' && (cap.maxTokenBudget as number) >= 1,
29
+ driver.describe('capabilities.schema.json §memory.distillation', 'maxTokenBudget MUST be a positive integer when present'),
30
+ ).toBe(true);
31
+ }
32
+ for (const k of ['scheduled', 'indexEmitted'] as const) {
33
+ if (cap[k] !== undefined) {
34
+ expect(
35
+ typeof cap[k],
36
+ driver.describe('capabilities.schema.json §memory.distillation', `distillation.${k} MUST be a boolean when present`),
37
+ ).toBe('boolean');
38
+ }
39
+ }
40
+ });
41
+ });
@@ -0,0 +1,37 @@
1
+ /**
2
+ * distillation-stable-archive — RFC 0062 §B(4). The distilled archive is an
3
+ * immutable, addressable artifact: the same source set + budget MUST yield a
4
+ * byte-stable archive checksum (reproducible + auditable).
5
+ *
6
+ * Gated on `capabilities.memory.distillation.supported` + the host memory-
7
+ * distillation seam; soft-skips when either is absent.
8
+ *
9
+ * @see RFCS/0062-scheduled-memory-distillation.md §B
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
15
+
16
+ describe('distillation-stable-archive (RFC 0062 §B)', () => {
17
+ it('identical sources + budget produce an identical archive checksum', async () => {
18
+ if ((await readDistillationCap())?.supported !== true) return;
19
+ const req = {
20
+ memoryRef: 'conformance-distill',
21
+ tokenBudget: 8000,
22
+ sources: ['s1', 's2', 's3'],
23
+ };
24
+ const a = await invokeDistill(req);
25
+ if (a === null) return; // seam absent — soft-skip
26
+ const b = await invokeDistill(req);
27
+ if (b === null) return;
28
+ expect(
29
+ typeof a.body.archiveChecksum === 'string' && (a.body.archiveChecksum as string).length > 0,
30
+ driver.describe('RFC 0062 §B', 'a distillation run MUST produce a non-empty archive checksum'),
31
+ ).toBe(true);
32
+ expect(
33
+ b.body.archiveChecksum,
34
+ driver.describe('RFC 0062 §B', 'the same source set + budget MUST yield a byte-stable archive'),
35
+ ).toBe(a.body.archiveChecksum);
36
+ });
37
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * distillation-token-budget — RFC 0062 §B. A distillation run stays within its
3
+ * token budget (`memory.compacted.distillation.tokensUsed ≤ tokenBudget`); an
4
+ * un-meetable budget fails with `token_budget_exceeded` and writes no partial
5
+ * archive (atomic).
6
+ *
7
+ * Gated on `capabilities.memory.distillation.supported` + the host memory-
8
+ * distillation seam; soft-skips when either is absent.
9
+ *
10
+ * @see RFCS/0062-scheduled-memory-distillation.md §B
11
+ */
12
+
13
+ import { describe, it, expect } from 'vitest';
14
+ import { driver } from '../lib/driver.js';
15
+ import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
16
+
17
+ describe('distillation-token-budget (RFC 0062 §B)', () => {
18
+ it('within budget tokensUsed ≤ tokenBudget; an un-meetable budget fails atomically', async () => {
19
+ if ((await readDistillationCap())?.supported !== true) return;
20
+
21
+ const ok = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000 });
22
+ if (ok === null) return; // seam absent — soft-skip
23
+ const dist = ok.body.event?.distillation ?? {};
24
+ expect(
25
+ typeof dist.tokenBudget === 'number' && typeof dist.tokensUsed === 'number',
26
+ driver.describe('RFC 0062 §B', 'memory.compacted MUST carry distillation.tokenBudget + tokensUsed on a budgeted run'),
27
+ ).toBe(true);
28
+ expect(
29
+ (dist.tokensUsed as number) <= (dist.tokenBudget as number),
30
+ driver.describe('RFC 0062 §B', 'a successful distillation MUST consume ≤ its tokenBudget'),
31
+ ).toBe(true);
32
+
33
+ // A budget too small to distill the corpus MUST fail closed, no partial archive.
34
+ const tooSmall = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 1 });
35
+ if (tooSmall === null) return;
36
+ expect(
37
+ tooSmall.status >= 400 && tooSmall.body.error === 'token_budget_exceeded',
38
+ driver.describe('RFC 0062 §B', 'an un-meetable budget MUST fail with token_budget_exceeded'),
39
+ ).toBe(true);
40
+ expect(
41
+ tooSmall.body.archiveChecksum,
42
+ driver.describe('RFC 0062 §B', 'a token_budget_exceeded run MUST write no partial archive (atomic)'),
43
+ ).toBeUndefined();
44
+ });
45
+ });
@@ -31,6 +31,7 @@ import { describe, it, expect } from 'vitest';
31
31
  import { driver } from '../lib/driver.js';
32
32
  import { pollUntilTerminal } from '../lib/polling.js';
33
33
  import { isFixtureAdvertised } from '../lib/fixtures.js';
34
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
34
35
 
35
36
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
36
37
  const NODE_ID = 'structured-call';
@@ -91,7 +92,7 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: advert
91
92
  it('capabilities.envelopes.reliability.completion (when present) conforms to RFC 0033 §E', async () => {
92
93
  const d = await readDiscovery();
93
94
  if (d === null) return;
94
- const completion = d.capabilities?.envelopes?.reliability?.completion;
95
+ const completion = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reliability?.completion;
95
96
  if (completion === undefined) return;
96
97
  expect(
97
98
  typeof completion.distinguishesTruncation,
@@ -114,7 +115,7 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: trunca
114
115
  it('truncation: emits envelope.truncated + envelope.retry.attempted with reason: "truncation"', async () => {
115
116
  if (!isFixtureAdvertised(TRUNCATED_FIXTURE)) return;
116
117
  const d = await readDiscovery();
117
- if (d?.capabilities?.envelopes?.reliability?.completion?.distinguishesTruncation !== true) return;
118
+ if (capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reliability?.completion?.distinguishesTruncation !== true) return;
118
119
  const seed = await programMock([
119
120
  { stopReason: 'max_tokens', content: '{"partial' },
120
121
  { stopReason: 'end_turn', content: '{"valid":true}' },
@@ -139,7 +140,7 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: trunca
139
140
  it('truncation: retry budget strictly greater than initial (RFC 0033 §B truncationBudgetMultiplier)', async () => {
140
141
  if (!isFixtureAdvertised(TRUNCATED_FIXTURE)) return;
141
142
  const d = await readDiscovery();
142
- if (d?.capabilities?.envelopes?.reliability?.completion?.distinguishesTruncation !== true) return;
143
+ if (capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reliability?.completion?.distinguishesTruncation !== true) return;
143
144
  const seed = await programMock([
144
145
  { stopReason: 'max_tokens', content: '{"partial' },
145
146
  { stopReason: 'end_turn', content: '{"valid":true}' },
@@ -35,6 +35,7 @@
35
35
 
36
36
  import { describe, it, expect } from 'vitest';
37
37
  import { driver } from '../lib/driver.js';
38
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
38
39
 
39
40
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
40
41
 
@@ -97,8 +98,8 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-secret-redaction: advertisement s
97
98
  it('hosts advertising envelope reasoning + BYOK honor SR-1 carry-forward for the reasoning field', async () => {
98
99
  const d = await readDiscovery();
99
100
  if (d === null) return;
100
- const reasoning = d.capabilities?.envelopes?.reasoning?.supported;
101
- const secrets = d.capabilities?.secrets?.supported;
101
+ const reasoning = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reasoning?.supported;
102
+ const secrets = capabilityFamily<{ supported?: unknown }>(d, 'secrets')?.supported;
102
103
  if (reasoning !== true || secrets !== true) return; // soft-skip when either is absent
103
104
  // The contract is invariant-based, not capability-flag-based — the
104
105
  // advertisement-shape check here just confirms both surfaces are claimed.
@@ -257,7 +258,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-secret-redaction: downstream-proj
257
258
  // RFC 0034 §B: gate on capabilities.observability.testSeams.otelScrape.
258
259
  // Hosts that don't advertise it soft-skip; hosts that DO advertise MUST serve a valid response.
259
260
  const d = await readDiscovery();
260
- const otelScrapeAdvertised = d?.capabilities?.observability?.testSeams?.otelScrape === true;
261
+ const otelScrapeAdvertised = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams?.otelScrape === true;
261
262
  if (!otelScrapeAdvertised) return; // soft-skip — host honest about not implementing per RFC 0034 §A
262
263
 
263
264
  const r = await acceptForRun(
@@ -291,7 +292,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-secret-redaction: downstream-proj
291
292
  it("debug-bundle export MUST NOT include plaintext `secret:`-prefixed substrings from envelope.reasoning", async () => {
292
293
  // RFC 0034 §B: gate on capabilities.observability.testSeams.debugBundleExport.
293
294
  const d = await readDiscovery();
294
- const debugBundleAdvertised = d?.capabilities?.observability?.testSeams?.debugBundleExport === true;
295
+ const debugBundleAdvertised = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams?.debugBundleExport === true;
295
296
  if (!debugBundleAdvertised) return; // soft-skip — host honest about not implementing per RFC 0034 §A
296
297
 
297
298
  const r = await acceptForRun(
@@ -32,6 +32,7 @@ import { readFileSync } from 'node:fs';
32
32
  import { join } from 'node:path';
33
33
  import { driver } from '../lib/driver.js';
34
34
  import { SCHEMAS_DIR } from '../lib/paths.js';
35
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
36
 
36
37
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
37
38
 
@@ -163,7 +164,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-shape: capabilities.envelopes adv
163
164
  it('capabilities.envelopes.reasoning (when present) conforms to RFC 0030 §C', async () => {
164
165
  const d = await readDiscovery();
165
166
  if (d === null) return;
166
- const reasoning = d.capabilities?.envelopes?.reasoning;
167
+ const reasoning = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reasoning;
167
168
  if (reasoning === undefined) return; // optional block; host MAY omit
168
169
  expect(
169
170
  typeof reasoning.supported,
@@ -180,7 +181,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-shape: capabilities.envelopes adv
180
181
  it('capabilities.envelopes.tierOneSubsetCompliance (when present) conforms to RFC 0030 §B', async () => {
181
182
  const d = await readDiscovery();
182
183
  if (d === null) return;
183
- const compliance = d.capabilities?.envelopes?.tierOneSubsetCompliance;
184
+ const compliance = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.tierOneSubsetCompliance;
184
185
  if (compliance === undefined) return; // optional; host MAY omit
185
186
  expect(
186
187
  ['strict', 'warn', 'off'],