@openwop/openwop-conformance 1.6.0 → 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 (169) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +74 -1
  4. package/api/openapi.yaml +316 -0
  5. package/coverage.md +16 -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 +12 -0
  10. package/schemas/agent-inventory-response.schema.json +90 -0
  11. package/schemas/ai-envelope.schema.json +28 -0
  12. package/schemas/annotation-create.schema.json +37 -0
  13. package/schemas/annotation.schema.json +56 -0
  14. package/schemas/artifact-type-pack-manifest.schema.json +160 -0
  15. package/schemas/capabilities.schema.json +195 -4
  16. package/schemas/chat-card-pack-manifest.schema.json +158 -0
  17. package/schemas/envelopes/media.audio.schema.json +38 -0
  18. package/schemas/envelopes/media.file.schema.json +37 -0
  19. package/schemas/envelopes/media.image.schema.json +33 -0
  20. package/schemas/heartbeat-evaluated.schema.json +14 -0
  21. package/schemas/heartbeat-state-changed.schema.json +14 -0
  22. package/schemas/node-pack-manifest.schema.json +16 -1
  23. package/schemas/run-event-payloads.schema.json +96 -5
  24. package/schemas/run-event.schema.json +4 -0
  25. package/schemas/workflow-definition.schema.json +5 -0
  26. package/schemas/workspace-file-create.schema.json +20 -0
  27. package/schemas/workspace-file.schema.json +39 -0
  28. package/src/lib/agentLoop.ts +44 -0
  29. package/src/lib/agentRuntime.ts +45 -0
  30. package/src/lib/artifactTypes.ts +96 -0
  31. package/src/lib/cardPacks.ts +52 -0
  32. package/src/lib/discovery-capabilities.ts +50 -0
  33. package/src/lib/distillation.ts +38 -0
  34. package/src/lib/feedback.ts +31 -0
  35. package/src/lib/heartbeat.ts +31 -0
  36. package/src/lib/memoryAttribution.ts +48 -0
  37. package/src/lib/subRunAttestation.ts +35 -0
  38. package/src/lib/toolHooks.ts +33 -0
  39. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
  40. package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
  41. package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
  42. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
  43. package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
  44. package/src/scenarios/ai-envelope-shape.test.ts +14 -18
  45. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
  46. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
  47. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
  48. package/src/scenarios/approval-gate-flow.test.ts +4 -6
  49. package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
  50. package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
  51. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
  52. package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
  53. package/src/scenarios/audit-log-integrity.test.ts +3 -2
  54. package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
  55. package/src/scenarios/auth-mtls.test.ts +2 -1
  56. package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
  57. package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
  58. package/src/scenarios/auth-saml-profile.test.ts +2 -1
  59. package/src/scenarios/auth-scim-profile.test.ts +2 -1
  60. package/src/scenarios/authorization-fail-closed.test.ts +2 -1
  61. package/src/scenarios/authorization-roles-shape.test.ts +2 -1
  62. package/src/scenarios/byok-auth-modes.test.ts +141 -0
  63. package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
  64. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
  65. package/src/scenarios/commitment-fired.test.ts +83 -0
  66. package/src/scenarios/credential-payload-redaction.test.ts +2 -1
  67. package/src/scenarios/credentials-capability-shape.test.ts +2 -1
  68. package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
  69. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
  70. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  71. package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
  72. package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
  73. package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
  74. package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
  75. package/src/scenarios/distillation-shape.test.ts +41 -0
  76. package/src/scenarios/distillation-stable-archive.test.ts +37 -0
  77. package/src/scenarios/distillation-token-budget.test.ts +45 -0
  78. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
  79. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
  80. package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
  81. package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
  82. package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
  83. package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
  84. package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
  85. package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
  86. package/src/scenarios/experimental-tier-shape.test.ts +5 -4
  87. package/src/scenarios/feedback-capability-shape.test.ts +35 -0
  88. package/src/scenarios/feedback-correction-redaction.test.ts +35 -0
  89. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +37 -0
  90. package/src/scenarios/feedback-fork-not-copied.test.ts +40 -0
  91. package/src/scenarios/feedback-on-terminal-run.test.ts +32 -0
  92. package/src/scenarios/feedback-record-and-list.test.ts +32 -0
  93. package/src/scenarios/feedback-unsupported-501.test.ts +32 -0
  94. package/src/scenarios/fs-path-traversal.test.ts +2 -1
  95. package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
  96. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
  97. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
  98. package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
  99. package/src/scenarios/http-client-ssrf.test.ts +10 -13
  100. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
  101. package/src/scenarios/media-url-inline-cap.test.ts +167 -0
  102. package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
  103. package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
  104. package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
  105. package/src/scenarios/memory-attribution-shape.test.ts +28 -0
  106. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
  107. package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
  108. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
  109. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
  110. package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
  111. package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
  112. package/src/scenarios/model-capability-substituted.test.ts +2 -1
  113. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  114. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
  115. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
  116. package/src/scenarios/multi-region-idempotency.test.ts +10 -10
  117. package/src/scenarios/oauth-capability-shape.test.ts +2 -1
  118. package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
  119. package/src/scenarios/pause-resume.test.ts +3 -3
  120. package/src/scenarios/production-backpressure.test.ts +2 -2
  121. package/src/scenarios/production-retention-expiry.test.ts +2 -2
  122. package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
  123. package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
  124. package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
  125. package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
  126. package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
  127. package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
  128. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
  129. package/src/scenarios/prompt-pack-install.test.ts +2 -1
  130. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
  131. package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
  132. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
  133. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
  134. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
  135. package/src/scenarios/prompt-template-shape.test.ts +2 -1
  136. package/src/scenarios/provider-usage.test.ts +2 -1
  137. package/src/scenarios/redaction.test.ts +4 -1
  138. package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
  139. package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
  140. package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
  141. package/src/scenarios/replayDeterminism.test.ts +3 -1
  142. package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
  143. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  144. package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
  145. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  146. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  147. package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
  148. package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
  149. package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
  150. package/src/scenarios/spec-corpus-validity.test.ts +4 -1
  151. package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
  152. package/src/scenarios/subrun-approval-gate.test.ts +35 -0
  153. package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
  154. package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
  155. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
  156. package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
  157. package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
  158. package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
  159. package/src/scenarios/tool-hooks-shape.test.ts +34 -0
  160. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
  161. package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
  162. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
  163. package/src/scenarios/wasm-pack-load.test.ts +2 -2
  164. package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
  165. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
  166. package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
  167. package/src/scenarios/workspace-behavior.test.ts +134 -0
  168. package/src/scenarios/workspace-capability-shape.test.ts +73 -0
  169. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * memory-attribution-replay-stable — RFC 0057 §D. `memory.written` is an
3
+ * immutable recorded fact: a `replay`-mode fork MUST NOT mint a new
4
+ * `memoryId` for a write the source run already recorded. This asserts the
5
+ * "MUST NOT regenerate" half — every `memory.written` on a replayed run
6
+ * reuses a `memoryId` the source run recorded (a compliant host that
7
+ * suppresses re-mint on replay satisfies this vacuously with zero events).
8
+ *
9
+ * Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
10
+ * when unadvertised, when the seeded run wrote no memory, or when the host
11
+ * doesn't support `:fork` in `replay` mode.
12
+ *
13
+ * @see RFCS/0057-memory-write-attribution-event.md §D
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+ import { pollUntilTerminal } from '../lib/polling.js';
19
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
20
+
21
+ function memoryIdOf(payload: Record<string, unknown> | undefined): string | null {
22
+ const id = (payload ?? {})['memoryId'];
23
+ return typeof id === 'string' ? id : null;
24
+ }
25
+
26
+ describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
27
+ it('a replay-mode fork introduces no memory.written with a new memoryId', async () => {
28
+ const cap = await readMemoryAttributionCap();
29
+ if (!emitsWriteEvents(cap)) return;
30
+ const runId = await seedRun('mem-attr-replay');
31
+ if (!runId) return;
32
+ try {
33
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
34
+ } catch {
35
+ return;
36
+ }
37
+ const original = await memoryWrittenEvents(runId);
38
+ if (original.length === 0) return; // run wrote no memory — nothing to test
39
+ const recordedIds = new Set(original.map((e) => memoryIdOf(e.payload)).filter((x): x is string => x !== null));
40
+
41
+ const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'replay' });
42
+ if (fork.status !== 200 && fork.status !== 201) return; // replay fork unsupported — soft-skip
43
+ const forkId = (fork.json as { runId?: string } | undefined)?.runId;
44
+ if (!forkId) return;
45
+ try {
46
+ await pollUntilTerminal(forkId, { timeoutMs: 10_000 });
47
+ } catch {
48
+ /* still assert on whatever the replay emitted */
49
+ }
50
+
51
+ const replayed = await memoryWrittenEvents(forkId);
52
+ for (const e of replayed) {
53
+ const id = memoryIdOf(e.payload);
54
+ expect(
55
+ id !== null && recordedIds.has(id),
56
+ driver.describe('RFC 0057 §D', 'a replay MUST NOT regenerate memoryId — every replayed memory.written reuses a recorded id'),
57
+ ).toBe(true);
58
+ }
59
+ });
60
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * memory-attribution-shape — RFC 0057 §A. The `capabilities.memory.attribution`
3
+ * advertisement block is either absent or a well-formed object.
4
+ *
5
+ * Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
6
+ * in the sibling memory-attribution-*.test.ts scenarios, gated on
7
+ * `capabilities.memory.attribution.emitsWriteEvents`.
8
+ *
9
+ * @see RFCS/0057-memory-write-attribution-event.md §A
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import { driver } from '../lib/driver.js';
14
+ import { readMemoryAttributionCap } from '../lib/memoryAttribution.js';
15
+
16
+ describe('memory-attribution-shape: advertisement (RFC 0057 §A)', () => {
17
+ it('capabilities.memory.attribution is absent or a well-formed object', async () => {
18
+ const cap = await readMemoryAttributionCap();
19
+ if (cap === null) return; // not advertised — valid
20
+ expect(
21
+ cap.supported,
22
+ driver.describe('capabilities.schema.json §memory.attribution', 'memory.attribution.supported MUST be the literal true when the block is present'),
23
+ ).toBe(true);
24
+ if (cap.emitsWriteEvents !== undefined) {
25
+ expect(typeof cap.emitsWriteEvents).toBe('boolean');
26
+ }
27
+ });
28
+ });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * memory-attribution-tenant-scoped — RFC 0057 §C + SECURITY/invariants.yaml
3
+ * `memory-attribution-tenant-scoped`. A run's `memory.written` events appear
4
+ * only on that run's stream (mirrors CTI-1). The full cross-tenant proof
5
+ * (tenant B cannot read tenant A's run stream) needs a multi-tenant auth seam
6
+ * not standardized for this surface — that half soft-skips, mirroring
7
+ * `feedback-cross-tenant-isolation`.
8
+ *
9
+ * Gated on `capabilities.memory.attribution.emitsWriteEvents`.
10
+ *
11
+ * @see RFCS/0057-memory-write-attribution-event.md §C
12
+ * @see SECURITY/invariants.yaml — memory-attribution-tenant-scoped
13
+ */
14
+
15
+ import { describe, it, expect } from 'vitest';
16
+ import { driver } from '../lib/driver.js';
17
+ import { pollUntilTerminal } from '../lib/polling.js';
18
+ import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
19
+
20
+ describe('memory-attribution-tenant-scoped (RFC 0057 §C)', () => {
21
+ it("a run's memory.written events appear only on that run's stream", async () => {
22
+ const cap = await readMemoryAttributionCap();
23
+ if (!emitsWriteEvents(cap)) return;
24
+ const runId = await seedRun('mem-attr-cti');
25
+ if (!runId) return;
26
+ try {
27
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
28
+ } catch {
29
+ return;
30
+ }
31
+ const events = await memoryWrittenEvents(runId);
32
+ if (events.length === 0) return;
33
+ // Every memory.written we read came from THIS run's /events stream; if the
34
+ // host echoes a runId in the event it MUST be this run's (no cross-run leak).
35
+ for (const e of events) {
36
+ if (typeof e.runId === 'string') {
37
+ expect(
38
+ e.runId,
39
+ driver.describe('RFC 0057 §C', "a memory.written event MUST belong to its own run's stream (CTI-1)"),
40
+ ).toBe(runId);
41
+ }
42
+ }
43
+ });
44
+ });
@@ -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 MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-event_longTerm';
31
32
 
@@ -35,7 +36,7 @@ interface MemoryCaps {
35
36
 
36
37
  async function isCompactionAdvertised(): Promise<boolean> {
37
38
  const disco = await driver.get('/.well-known/openwop');
38
- const memory = (disco.json as { capabilities?: { memory?: MemoryCaps } }).capabilities?.memory;
39
+ const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
39
40
  return memory?.compaction?.supported === true;
40
41
  }
41
42
 
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { describe, it, expect } from 'vitest';
22
22
  import { driver } from '../lib/driver.js';
23
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
23
24
 
24
25
  const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-tag_longTerm';
25
26
  const COMPACTED_FROM_RE = /^compacted-from:[^\s:][^\s]*$/;
@@ -34,7 +35,7 @@ interface MemoryListResponse {
34
35
 
35
36
  async function isCompactionAdvertised(): Promise<boolean> {
36
37
  const disco = await driver.get('/.well-known/openwop');
37
- const memory = (disco.json as { capabilities?: { memory?: MemoryCaps } }).capabilities?.memory;
38
+ const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
38
39
  return memory?.compaction?.supported === true;
39
40
  }
40
41
 
@@ -28,6 +28,7 @@
28
28
 
29
29
  import { describe, it, expect } from 'vitest';
30
30
  import { driver } from '../lib/driver.js';
31
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
31
32
 
32
33
  const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-sr1_longTerm';
33
34
 
@@ -37,7 +38,7 @@ interface MemoryCaps {
37
38
 
38
39
  async function isCompactionAdvertised(): Promise<boolean> {
39
40
  const disco = await driver.get('/.well-known/openwop');
40
- const memory = (disco.json as { capabilities?: { memory?: MemoryCaps } }).capabilities?.memory;
41
+ const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
41
42
  return memory?.compaction?.supported === true;
42
43
  }
43
44
 
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Background memory consolidation — idempotence + SR-1 carry-forward
3
+ * (RFC 0068, `Draft`).
4
+ *
5
+ * Gated on `capabilities.agents.memoryConsolidation.supported`. Drives the
6
+ * documented host seam `POST /v1/host/sample/memory/consolidate` (staged
7
+ * per the RFC 0027 §G precedent — soft-skips on 404/501 until a reference
8
+ * host wires it). Asserts:
9
+ * - a consolidation pass emits `agent.memory.consolidated` with
10
+ * `outputCount <= inputCount` (RFC 0068 §D);
11
+ * - a second pass over the unchanged corpus is a no-op
12
+ * (`inputCount == outputCount`) — the idempotence MUST that bounds
13
+ * runaway consolidation;
14
+ * - SR-1 carry-forward — a redacted secret in a source entry stays
15
+ * redacted in a consolidated entry.
16
+ *
17
+ * Hosts that omit the capability skip cleanly.
18
+ *
19
+ * Spec references:
20
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
21
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
22
+ */
23
+
24
+ import { describe, it, expect } from 'vitest';
25
+ import { driver } from '../lib/driver.js';
26
+
27
+ interface ConsolidationCaps {
28
+ agents?: { memoryConsolidation?: { supported?: boolean } };
29
+ }
30
+
31
+ interface ConsolidateResult {
32
+ event?: { inputCount?: number; outputCount?: number };
33
+ secretLeaked?: boolean;
34
+ }
35
+
36
+ async function consolidationSupported(): Promise<boolean> {
37
+ const res = await driver.get('/.well-known/openwop', { authenticated: false });
38
+ if (res.status !== 200) return false;
39
+ return Boolean((res.json as ConsolidationCaps).agents?.memoryConsolidation?.supported);
40
+ }
41
+
42
+ describe('memory-consolidation-idempotent: pass contract (RFC 0068 §D, capability-gated)', () => {
43
+ it('a consolidation pass reduces or holds entry count and is idempotent on a stable corpus', async () => {
44
+ if (!(await consolidationSupported())) return; // capability absent — gated skip
45
+
46
+ const first = await driver.post('/v1/host/sample/memory/consolidate', {
47
+ memoryRef: 'mem://conformance/consolidation',
48
+ includeSecretCanary: true,
49
+ });
50
+ if (first.status === 404 || first.status === 501) return; // seam not wired — soft-skip
51
+
52
+ expect(first.status, driver.describe('RFC 0068 §D', 'an advertised consolidation seam MUST succeed')).toBe(200);
53
+ const r1 = first.json as ConsolidateResult;
54
+ const in1 = r1.event?.inputCount ?? 0;
55
+ const out1 = r1.event?.outputCount ?? 0;
56
+ expect(out1, driver.describe('RFC 0068 §D.1', 'outputCount MUST be <= inputCount for a merge/dedup pass')).toBeLessThanOrEqual(in1);
57
+
58
+ // §D.2 — a second pass over the unchanged corpus is a no-op.
59
+ const second = await driver.post('/v1/host/sample/memory/consolidate', {
60
+ memoryRef: 'mem://conformance/consolidation',
61
+ });
62
+ if (second.status === 404 || second.status === 501) return;
63
+ const r2 = second.json as ConsolidateResult;
64
+ expect(
65
+ r2.event?.inputCount,
66
+ driver.describe('RFC 0068 §D.2', 'a second pass over an unchanged corpus MUST be a no-op (inputCount == outputCount)'),
67
+ ).toBe(r2.event?.outputCount);
68
+
69
+ // §D.3 — SR-1 carry-forward: a redacted secret stays redacted in the consolidated entry.
70
+ if (typeof r1.secretLeaked === 'boolean') {
71
+ expect(
72
+ r1.secretLeaked,
73
+ driver.describe('RFC 0068 §D.3 / agent-memory.md §SR-1', 'a redacted secret MUST NOT re-appear in a consolidated entry'),
74
+ ).toBe(false);
75
+ }
76
+ });
77
+ });
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Memory-consolidation + commitment event shapes (RFC 0068, `Draft`).
3
+ *
4
+ * Always-on, server-free schema-shape probe. Verifies that:
5
+ * - `capabilities.agents.memoryConsolidation` + `agents.commitments`
6
+ * sub-blocks are declared on the capabilities schema.
7
+ * - the `agent.memory.consolidated` + `commitment.fired` payload $defs
8
+ * validate conforming payloads and reject malformed ones (a
9
+ * `commitment.fired` missing `memoryRef` is rejected — a commitment
10
+ * with no memory provenance is not an *inferred* commitment).
11
+ * - both event names appear in the RunEventType enum.
12
+ *
13
+ * Distinct from RFC 0062 distillation (`memory.compacted`): consolidation
14
+ * reconciles long-term memory; this scenario asserts the new event
15
+ * contract, not host behavior.
16
+ *
17
+ * Spec references:
18
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
19
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Inferred commitments"
20
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { readFileSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import Ajv2020 from 'ajv/dist/2020.js';
27
+ import addFormats from 'ajv-formats';
28
+ import { SCHEMAS_DIR } from '../lib/paths.js';
29
+
30
+ /** Server-free assertion-message helper (mirrors driver.describe's "spec — requirement" shape without requiring OPENWOP_BASE_URL). */
31
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
32
+
33
+ function loadSchema(name: string): Record<string, unknown> {
34
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
35
+ }
36
+
37
+ describe('memory-consolidation-shape: capability advertisement (RFC 0068, server-free)', () => {
38
+ it('the capabilities schema declares agents.memoryConsolidation + agents.commitments', () => {
39
+ const caps = loadSchema('capabilities.schema.json');
40
+ const agents = (caps.properties as Record<string, { properties?: Record<string, unknown> }>).agents;
41
+ expect(
42
+ agents?.properties?.memoryConsolidation,
43
+ why('capabilities.md §agents', 'agents.memoryConsolidation MUST be declared'),
44
+ ).toBeDefined();
45
+ expect(
46
+ agents?.properties?.commitments,
47
+ why('capabilities.md §agents', 'agents.commitments MUST be declared'),
48
+ ).toBeDefined();
49
+ });
50
+ });
51
+
52
+ describe('memory-consolidation-shape: event payloads (RFC 0068, server-free)', () => {
53
+ const payloads = loadSchema('run-event-payloads.schema.json');
54
+ const ajv = new Ajv2020({ strict: false, allErrors: true });
55
+ addFormats(ajv);
56
+ ajv.addSchema(payloads, 'payloads');
57
+
58
+ const consolidated = ajv.getSchema('payloads#/$defs/agentMemoryConsolidated');
59
+ const fired = ajv.getSchema('payloads#/$defs/commitmentFired');
60
+
61
+ it('agent.memory.consolidated validates a content-free pass summary', () => {
62
+ expect(consolidated, 'the agentMemoryConsolidated $def MUST exist').toBeTruthy();
63
+ expect(
64
+ consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 240, outputCount: 201, trigger: 'host-managed' }),
65
+ why('RFC 0068 §B', 'a conforming agent.memory.consolidated payload MUST validate'),
66
+ ).toBe(true);
67
+ // Negative: outputCount as string fails the integer type.
68
+ expect(consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 1, outputCount: 'x' })).toBe(false);
69
+ });
70
+
71
+ it('commitment.fired validates a content-free fire record and requires memoryRef', () => {
72
+ expect(fired, 'the commitmentFired $def MUST exist').toBeTruthy();
73
+ expect(
74
+ fired!({ commitmentId: 'cmt-1', memoryRef: 'mem://a/agent-1', condition: 'predicate', enqueuedRunId: 'run-1' }),
75
+ why('RFC 0068 §C', 'a conforming commitment.fired payload MUST validate'),
76
+ ).toBe(true);
77
+ // Negative: missing memoryRef — a commitment with no provenance breaks CTI-1 binding.
78
+ expect(
79
+ fired!({ commitmentId: 'cmt-1', condition: 'time' }),
80
+ why('RFC 0068 §C', 'commitment.fired without memoryRef MUST be rejected'),
81
+ ).toBe(false);
82
+ });
83
+
84
+ it('both event names appear in the RunEventType enum', () => {
85
+ const runEvent = loadSchema('run-event.schema.json');
86
+ const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
87
+ expect(enumVals).toContain('agent.memory.consolidated');
88
+ expect(enumVals).toContain('commitment.fired');
89
+ });
90
+ });
@@ -17,6 +17,7 @@
17
17
 
18
18
  import { describe, it, expect } from 'vitest';
19
19
  import { driver } from '../lib/driver.js';
20
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
20
21
 
21
22
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
22
23
 
@@ -64,7 +65,7 @@ describe.skipIf(HTTP_SKIP)('model-capability-substituted: advertisement shape (R
64
65
  it('capabilities.modelCapabilities (when present) conforms to RFC 0031 §E', async () => {
65
66
  const d = await readDiscovery();
66
67
  if (d === null) return;
67
- const mc = d.capabilities?.modelCapabilities;
68
+ const mc = capabilityFamily(d, 'modelCapabilities');
68
69
  if (mc === undefined) return;
69
70
  expect(
70
71
  typeof mc.supported,
@@ -50,6 +50,7 @@ import { describe, it, expect } from 'vitest';
50
50
  import { driver } from '../lib/driver.js';
51
51
  import { isFixtureAdvertised } from '../lib/fixtures.js';
52
52
  import { pollUntilTerminal } from '../lib/polling.js';
53
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
53
54
 
54
55
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
55
56
  const FIXTURE = 'conformance-multi-agent-confidence-escalation';
@@ -84,7 +85,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-confidence-escalation: capability shape
84
85
  it('confidenceEscalationFloor (when advertised) MUST be in [0.5, 1.0]', async () => {
85
86
  const d = await readDiscovery();
86
87
  if (d === null) return;
87
- const em = d.capabilities?.multiAgent?.executionModel;
88
+ const em = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
88
89
  if (em === undefined) return;
89
90
  const floor = em.confidenceEscalationFloor;
90
91
  if (floor === undefined) return;
@@ -101,8 +102,8 @@ describe.skipIf(HTTP_SKIP)('multi-agent-confidence-escalation: capability shape
101
102
  describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral (RFC 0039 §A)', () => {
102
103
  it('happy-path: low-confidence decision → confidence-escalated event + clarification interrupt + zero dispatch events', async () => {
103
104
  const d = await readDiscovery();
104
- const supported = d?.capabilities?.multiAgent?.executionModel?.supported === true;
105
- const versionRaw = d?.capabilities?.multiAgent?.executionModel?.version;
105
+ const supported = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.supported === true;
106
+ const versionRaw = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version;
106
107
  const version = typeof versionRaw === 'number' ? versionRaw : 0;
107
108
  if (!supported || version < 2) return; // soft-skip — `version: 1` hosts pass via this absence
108
109
 
@@ -125,7 +126,7 @@ describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral
125
126
  // status — the host's own interrupt.md mapping determines the suffix).
126
127
  // When the host does NOT advertise the field, fall back to the canonical
127
128
  // either-status check.
128
- const advertisedKind = d?.capabilities?.multiAgent?.executionModel?.confidenceEscalationInterruptKind;
129
+ const advertisedKind = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.confidenceEscalationInterruptKind;
129
130
  const isVendorKind = typeof advertisedKind === 'string' && /^x-host-[a-z][a-z0-9-]*-[a-z][a-z0-9-]*$/.test(advertisedKind);
130
131
  const isCanonicalKind = advertisedKind === 'clarification' || advertisedKind === 'approval';
131
132
 
@@ -10,7 +10,7 @@
10
10
  * Asserts (Phase 1 — execution-loop + handoff state machine per spec/v1/multi-agent-execution.md):
11
11
  *
12
12
  * 1. Advertisement shape: when capabilities.multiAgent.executionModel.supported
13
- * is present, version MUST be integer in [1, 4]; supported MUST be boolean.
13
+ * is present, version MUST be integer in [1, 5]; supported MUST be boolean.
14
14
  *
15
15
  * 2. Behavioral (gated on supported: true + fixture availability): a
16
16
  * supervisor → next-worker → child-completed run emits the 4 expected
@@ -34,6 +34,7 @@ import { describe, it, expect } from 'vitest';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { pollUntilTerminal } from '../lib/polling.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
39
40
 
@@ -62,7 +63,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-handoff-state-machine: advertisement sha
62
63
  it('capabilities.multiAgent.executionModel (when present) conforms to RFC 0037 §C', async () => {
63
64
  const d = await readDiscovery();
64
65
  if (d === null) return; // discovery unavailable — skip
65
- const executionModel = d.capabilities?.multiAgent?.executionModel;
66
+ const executionModel = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
66
67
  if (executionModel === undefined) return; // host doesn't advertise — soft-skip
67
68
  expect(
68
69
  typeof executionModel.supported,
@@ -80,10 +81,10 @@ describe.skipIf(HTTP_SKIP)('multi-agent-handoff-state-machine: advertisement sha
80
81
  ).toBe('number');
81
82
  const v = executionModel.version as number;
82
83
  expect(
83
- Number.isInteger(v) && v >= 1 && v <= 4,
84
+ Number.isInteger(v) && v >= 1 && v <= 5,
84
85
  driver.describe(
85
86
  'RFCS/0037-multi-agent-execution-model.md §C',
86
- 'version MUST be an integer in [1, 4] (1 = Phase 1 only; Phases 2-4 lift the ceiling additively)',
87
+ 'version MUST be an integer in [1, 5] (1 = Phase 1 only; Phases 2-5 lift the ceiling additively — Phase 5 = RFC 0061 stateful agent-loop lifecycle, matching `capabilities.schema.json` §multiAgent.executionModel.version maximum)',
87
88
  ),
88
89
  ).toBe(true);
89
90
  });
@@ -104,7 +105,7 @@ const BEHAVIORAL_SKIP = HTTP_SKIP || !isFixtureAdvertised(PARENT_FIXTURE) || !is
104
105
  describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-handoff-state-machine: behavioral 4-event causation chain (RFC 0037 §"Handoff state machine")', () => {
105
106
  it('happy-path: dispatch.began → dispatch.succeeded → child.completed → output.harvested fire in causation order', async () => {
106
107
  const d = await readDiscovery();
107
- const advertised = d?.capabilities?.multiAgent?.executionModel?.supported === true;
108
+ const advertised = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.supported === true;
108
109
  if (!advertised) return; // soft-skip — host honest about not implementing
109
110
 
110
111
  const create = await driver.post('/v1/runs', { workflowId: PARENT_FIXTURE });
@@ -48,6 +48,7 @@
48
48
 
49
49
  import { describe, it, expect } from 'vitest';
50
50
  import { driver } from '../lib/driver.js';
51
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
51
52
 
52
53
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
53
54
 
@@ -81,7 +82,7 @@ describe.skipIf(HTTP_SKIP)('multi-agent-memory-lifecycle: advertisement shape (R
81
82
  ctx.skip();
82
83
  return;
83
84
  }
84
- const ccmc = d.capabilities?.multiAgent?.executionModel?.crossChildMemoryConcurrency;
85
+ const ccmc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossChildMemoryConcurrency;
85
86
  if (ccmc === undefined) {
86
87
  ctx.skip(); // optional advertisement — host hasn't opted in
87
88
  return;
@@ -135,8 +136,8 @@ describe.skipIf(HTTP_SKIP)('multi-agent-memory-lifecycle: behavioral (RFC 0039
135
136
  ctx.skip();
136
137
  return;
137
138
  }
138
- const v = d.capabilities?.multiAgent?.executionModel?.version;
139
- const memorySupported = d.capabilities?.memory?.supported;
139
+ const v = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version;
140
+ const memorySupported = capabilityFamily<{ supported?: unknown }>(d, 'memory')?.supported;
140
141
  const phase2OrLater = typeof v === 'number' && v >= 2;
141
142
  const expiredRunId = process.env.OPENWOP_TEST_EXPIRED_REPLAY_RUN_ID;
142
143
  if (!phase2OrLater || memorySupported !== true || !expiredRunId) {
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { describe, it, expect } from 'vitest';
22
22
  import { driver } from '../lib/driver.js';
23
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
23
24
 
24
25
  const ALLOWED = new Set(['single-region', 'best-effort', 'strict']);
25
26
  const REQUIRED_METRICS_WHEN_MULTI_REGION = [
@@ -40,9 +41,7 @@ interface ObservabilityCaps {
40
41
  describe('multi-region-idempotency: capability shape', () => {
41
42
  it('idempotency.crossRegion (when advertised) MUST be one of the closed enum', async () => {
42
43
  const disco = await driver.get('/.well-known/openwop');
43
- const idem =
44
- (disco.json as { capabilities?: { idempotency?: IdempotencyCaps } }).capabilities
45
- ?.idempotency;
44
+ const idem = capabilityFamily<IdempotencyCaps>(disco.json, 'idempotency');
46
45
 
47
46
  if (!idem || idem.crossRegion === undefined) {
48
47
  // eslint-disable-next-line no-console
@@ -67,16 +66,16 @@ describe('multi-region-idempotency: capability shape', () => {
67
66
 
68
67
  it('multi-region hosts SHOULD expose the cross-region conflict counter per §"Operator surface"', async () => {
69
68
  const disco = await driver.get('/.well-known/openwop');
70
- const caps = (disco.json as { capabilities?: { idempotency?: IdempotencyCaps; observability?: ObservabilityCaps } })
71
- .capabilities;
72
- const crossRegion = caps?.idempotency?.crossRegion;
69
+ const idem = capabilityFamily<IdempotencyCaps>(disco.json, 'idempotency');
70
+ const observability = capabilityFamily<ObservabilityCaps>(disco.json, 'observability');
71
+ const crossRegion = idem?.crossRegion;
73
72
 
74
73
  if (crossRegion !== 'best-effort' && crossRegion !== 'strict') {
75
74
  // Single-region hosts have no conflicts to count — skip.
76
75
  return;
77
76
  }
78
77
 
79
- const advertised = new Set(caps?.observability?.metrics?.names ?? []);
78
+ const advertised = new Set(observability?.metrics?.names ?? []);
80
79
  for (const name of REQUIRED_METRICS_WHEN_MULTI_REGION) {
81
80
  expect(advertised.has(name), driver.describe(
82
81
  'idempotency.md §"Operator surface"',
@@ -103,9 +102,10 @@ interface MultiRegionCaps {
103
102
  describe('multi-region-idempotency: granular multiRegion advertisement shape (RFC 0036 §A)', () => {
104
103
  it('capabilities.idempotency.multiRegion (when present) conforms to RFC 0036 §A', async () => {
105
104
  const disco = await driver.get('/.well-known/openwop');
106
- const idem =
107
- (disco.json as { capabilities?: { idempotency?: IdempotencyCaps & { multiRegion?: MultiRegionCaps } } })
108
- .capabilities?.idempotency;
105
+ const idem = capabilityFamily<IdempotencyCaps & { multiRegion?: MultiRegionCaps }>(
106
+ disco.json,
107
+ 'idempotency',
108
+ );
109
109
  const mr = idem?.multiRegion;
110
110
  if (mr === undefined) return; // host doesn't advertise the granular block — soft-skip
111
111
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
20
  import { driver } from '../lib/driver.js';
21
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
21
22
 
22
23
  interface DiscoveryOAuthProvider {
23
24
  id?: string;
@@ -47,7 +48,7 @@ const VALID_GRANTS: ReadonlySet<string> = new Set([
47
48
  async function readOAuth(): Promise<DiscoveryOAuth | null> {
48
49
  const res = await driver.get('/.well-known/openwop');
49
50
  const body = res.json as DiscoveryDoc | undefined;
50
- return body?.capabilities?.oauth ?? null;
51
+ return capabilityFamily(body, 'oauth') ?? null;
51
52
  }
52
53
 
53
54
  describe('oauth-capability-shape: advertisement shape (RFC 0047 §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
  interface DiscoveryOAuth {
31
32
  supported?: boolean;
@@ -42,7 +43,7 @@ const TOKEN_CANARY = 'OPENWOP_OAUTH_CANARY_b7d3e1a9c2';
42
43
  async function readOAuth(): Promise<DiscoveryOAuth | null> {
43
44
  const res = await driver.get('/.well-known/openwop');
44
45
  const body = res.json as DiscoveryDoc | undefined;
45
- return body?.capabilities?.oauth ?? null;
46
+ return capabilityFamily(body, 'oauth') ?? null;
46
47
  }
47
48
 
48
49
  describe('oauth-connector-redaction: advertisement shape (RFC 0047 §A)', () => {
@@ -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
  import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
23
24
  import { isFixtureAdvertised } from '../lib/fixtures.js';
24
25
 
@@ -235,9 +236,8 @@ describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities
235
236
  it('every drainPolicy advertised by the host is accepted on :pause', async () => {
236
237
  const disco = await driver.get('/.well-known/openwop');
237
238
  const drainPolicies =
238
- (disco.json as {
239
- capabilities?: { runs?: { pauseResume?: { drainPolicies?: string[] } } };
240
- }).capabilities?.runs?.pauseResume?.drainPolicies ?? [];
239
+ capabilityFamily<{ pauseResume?: { drainPolicies?: string[] } }>(disco.json, 'runs')
240
+ ?.pauseResume?.drainPolicies ?? [];
241
241
  if (drainPolicies.length === 0) {
242
242
  // eslint-disable-next-line no-console
243
243
  console.warn('[pause-resume] host advertises no drainPolicies; skipping policy-discrimination subtest');
@@ -40,6 +40,7 @@ import { driver } from '../lib/driver.js';
40
40
  import { loadEnv } from '../lib/env.js';
41
41
  import { behaviorGate } from '../lib/behavior-gate.js';
42
42
  import { isFixtureAdvertised } from '../lib/fixtures.js';
43
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
43
44
 
44
45
  interface BackpressureCaps {
45
46
  supported?: boolean;
@@ -54,8 +55,7 @@ interface ProductionCaps {
54
55
 
55
56
  async function readProductionCaps(): Promise<ProductionCaps | undefined> {
56
57
  const disco = await driver.get('/.well-known/openwop');
57
- return (disco.json as { capabilities?: { production?: ProductionCaps } })
58
- .capabilities?.production;
58
+ return capabilityFamily<ProductionCaps>(disco.json, 'production');
59
59
  }
60
60
 
61
61
  function isProfileAdvertised(prod: ProductionCaps | undefined): boolean {
@@ -31,6 +31,7 @@
31
31
  import { describe, it, expect } from 'vitest';
32
32
  import { driver } from '../lib/driver.js';
33
33
  import { behaviorGate } from '../lib/behavior-gate.js';
34
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
34
35
 
35
36
  interface RetentionCaps {
36
37
  supported?: boolean;
@@ -45,8 +46,7 @@ interface ProductionCaps {
45
46
 
46
47
  async function readProductionCaps(): Promise<ProductionCaps | undefined> {
47
48
  const disco = await driver.get('/.well-known/openwop');
48
- return (disco.json as { capabilities?: { production?: ProductionCaps } })
49
- .capabilities?.production;
49
+ return capabilityFamily<ProductionCaps>(disco.json, 'production');
50
50
  }
51
51
 
52
52
  function isProfileAdvertised(prod: ProductionCaps | undefined): boolean {