@openwop/openwop-conformance 1.6.1 → 1.11.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 (200) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +127 -0
  4. package/api/openapi.yaml +518 -1
  5. package/coverage.md +44 -2
  6. package/fixtures/conformance-run-duration-breach.json +33 -0
  7. package/fixtures/oauth-providers/synthetic.json +38 -0
  8. package/fixtures.md +29 -0
  9. package/package.json +1 -1
  10. package/schemas/README.md +22 -0
  11. package/schemas/agent-deployment-transition.schema.json +49 -0
  12. package/schemas/agent-deployment.schema.json +54 -0
  13. package/schemas/agent-eval-suite.schema.json +140 -0
  14. package/schemas/agent-inventory-response.schema.json +115 -0
  15. package/schemas/agent-manifest.schema.json +5 -0
  16. package/schemas/agent-org-chart.schema.json +82 -0
  17. package/schemas/agent-ref.schema.json +12 -2
  18. package/schemas/agent-roster-entry.schema.json +81 -0
  19. package/schemas/agent-roster-response.schema.json +21 -0
  20. package/schemas/ai-envelope.schema.json +28 -0
  21. package/schemas/artifact-type-pack-manifest.schema.json +160 -0
  22. package/schemas/budget-policy.schema.json +18 -0
  23. package/schemas/capabilities.schema.json +448 -4
  24. package/schemas/chat-card-pack-manifest.schema.json +158 -0
  25. package/schemas/credential-provenance.schema.json +18 -0
  26. package/schemas/envelopes/media.audio.schema.json +38 -0
  27. package/schemas/envelopes/media.file.schema.json +37 -0
  28. package/schemas/envelopes/media.image.schema.json +33 -0
  29. package/schemas/eval-summary.schema.json +92 -0
  30. package/schemas/heartbeat-evaluated.schema.json +14 -0
  31. package/schemas/heartbeat-state-changed.schema.json +14 -0
  32. package/schemas/node-pack-manifest.schema.json +33 -1
  33. package/schemas/org-chart-responsibility-view.schema.json +26 -0
  34. package/schemas/run-event-payloads.schema.json +380 -6
  35. package/schemas/run-event.schema.json +23 -0
  36. package/schemas/tool-descriptor.schema.json +63 -0
  37. package/schemas/trigger-subscription.schema.json +26 -0
  38. package/schemas/workflow-definition.schema.json +5 -0
  39. package/schemas/workspace-file-create.schema.json +20 -0
  40. package/schemas/workspace-file.schema.json +39 -0
  41. package/src/lib/agentLoop.ts +44 -0
  42. package/src/lib/agentRoster.ts +76 -0
  43. package/src/lib/agentRuntime.ts +45 -0
  44. package/src/lib/artifactTypes.ts +96 -0
  45. package/src/lib/cardPacks.ts +52 -0
  46. package/src/lib/discovery-capabilities.ts +50 -0
  47. package/src/lib/distillation.ts +38 -0
  48. package/src/lib/feedback.ts +3 -3
  49. package/src/lib/heartbeat.ts +31 -0
  50. package/src/lib/liveRuntime.ts +59 -0
  51. package/src/lib/memoryAttribution.ts +48 -0
  52. package/src/lib/profiles.ts +157 -0
  53. package/src/lib/runtimeRequires.ts +38 -0
  54. package/src/lib/safeFetch.ts +87 -0
  55. package/src/lib/subRunAttestation.ts +35 -0
  56. package/src/lib/toolHooks.ts +33 -0
  57. package/src/scenarios/agent-deployment-shape.test.ts +139 -0
  58. package/src/scenarios/agent-eval-suite-shape.test.ts +167 -0
  59. package/src/scenarios/agent-live-allowlist-enforced.test.ts +53 -0
  60. package/src/scenarios/agent-live-invocation-bracket.test.ts +98 -0
  61. package/src/scenarios/agent-live-runtime-shape.test.ts +98 -0
  62. package/src/scenarios/agent-live-structured-output.test.ts +58 -0
  63. package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
  64. package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
  65. package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
  66. package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
  67. package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
  68. package/src/scenarios/agent-org-chart-shape.test.ts +127 -0
  69. package/src/scenarios/agent-platform-profile.test.ts +158 -0
  70. package/src/scenarios/agent-roster-attribution.test.ts +179 -0
  71. package/src/scenarios/agent-roster-shape.test.ts +146 -0
  72. package/src/scenarios/ai-envelope-shape.test.ts +14 -18
  73. package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
  74. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
  75. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
  76. package/src/scenarios/approval-gate-flow.test.ts +4 -6
  77. package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
  78. package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
  79. package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
  80. package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
  81. package/src/scenarios/audit-log-integrity.test.ts +3 -2
  82. package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
  83. package/src/scenarios/auth-mtls.test.ts +2 -1
  84. package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
  85. package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
  86. package/src/scenarios/auth-saml-profile.test.ts +2 -1
  87. package/src/scenarios/auth-scim-profile.test.ts +2 -1
  88. package/src/scenarios/authorization-fail-closed.test.ts +2 -1
  89. package/src/scenarios/authorization-roles-shape.test.ts +2 -1
  90. package/src/scenarios/budget-policy-shape.test.ts +136 -0
  91. package/src/scenarios/byok-auth-modes.test.ts +141 -0
  92. package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
  93. package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
  94. package/src/scenarios/commitment-fired.test.ts +83 -0
  95. package/src/scenarios/credential-payload-redaction.test.ts +2 -1
  96. package/src/scenarios/credentials-capability-shape.test.ts +2 -1
  97. package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
  98. package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
  99. package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
  100. package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
  101. package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
  102. package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
  103. package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
  104. package/src/scenarios/distillation-shape.test.ts +41 -0
  105. package/src/scenarios/distillation-stable-archive.test.ts +37 -0
  106. package/src/scenarios/distillation-token-budget.test.ts +45 -0
  107. package/src/scenarios/egress-provenance-shape.test.ts +137 -0
  108. package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
  109. package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
  110. package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
  111. package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
  112. package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
  113. package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
  114. package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
  115. package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
  116. package/src/scenarios/experimental-tier-shape.test.ts +5 -4
  117. package/src/scenarios/fs-path-traversal.test.ts +2 -1
  118. package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
  119. package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
  120. package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
  121. package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
  122. package/src/scenarios/http-client-ssrf.test.ts +10 -13
  123. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
  124. package/src/scenarios/media-url-inline-cap.test.ts +167 -0
  125. package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
  126. package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
  127. package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
  128. package/src/scenarios/memory-attribution-shape.test.ts +28 -0
  129. package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
  130. package/src/scenarios/memory-capability-model-shape.test.ts +186 -0
  131. package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
  132. package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
  133. package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
  134. package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
  135. package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
  136. package/src/scenarios/model-capability-substituted.test.ts +2 -1
  137. package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
  138. package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
  139. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
  140. package/src/scenarios/multi-region-idempotency.test.ts +10 -10
  141. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +145 -0
  142. package/src/scenarios/oauth-capability-shape.test.ts +2 -1
  143. package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
  144. package/src/scenarios/pause-resume.test.ts +3 -3
  145. package/src/scenarios/production-backpressure.test.ts +2 -2
  146. package/src/scenarios/production-retention-expiry.test.ts +2 -2
  147. package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
  148. package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
  149. package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
  150. package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
  151. package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
  152. package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
  153. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
  154. package/src/scenarios/prompt-pack-install.test.ts +2 -1
  155. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
  156. package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
  157. package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
  158. package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
  159. package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
  160. package/src/scenarios/prompt-template-shape.test.ts +2 -1
  161. package/src/scenarios/provider-usage.test.ts +2 -1
  162. package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
  163. package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
  164. package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
  165. package/src/scenarios/replayDeterminism.test.ts +3 -1
  166. package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
  167. package/src/scenarios/runtime-requires-install-gate.test.ts +92 -0
  168. package/src/scenarios/runtime-requires-shape.test.ts +134 -0
  169. package/src/scenarios/safefetch-behavior.test.ts +99 -0
  170. package/src/scenarios/safefetch-live-audit.test.ts +175 -0
  171. package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
  172. package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
  173. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
  174. package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
  175. package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
  176. package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
  177. package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
  178. package/src/scenarios/spec-corpus-validity.test.ts +20 -4
  179. package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
  180. package/src/scenarios/subrun-approval-gate.test.ts +35 -0
  181. package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
  182. package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
  183. package/src/scenarios/tool-descriptor-shape.test.ts +133 -0
  184. package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
  185. package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
  186. package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
  187. package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
  188. package/src/scenarios/tool-hooks-shape.test.ts +34 -0
  189. package/src/scenarios/trigger-bridge-shape.test.ts +135 -0
  190. package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
  191. package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
  192. package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
  193. package/src/scenarios/wasm-pack-load.test.ts +2 -2
  194. package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
  195. package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
  196. package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
  197. package/src/scenarios/workspace-behavior.test.ts +134 -0
  198. package/src/scenarios/workspace-capability-shape.test.ts +73 -0
  199. package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
  200. package/src/scenarios/x-openwop-form-pack-manifest.test.ts +155 -0
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Memory capability model — reconciled dimensions + degraded-projection shapes (RFC 0080).
3
+ *
4
+ * Always-on, server-free schema-shape probe. Verifies that:
5
+ * - `capabilities.memory` declares the additive `writable` / `search` / `retention`
6
+ * dimensions (RFC 0080 §A), without disturbing the existing
7
+ * `supported` / `compaction` / `distillation` / `attribution` fields.
8
+ * - the `memory.search` / `memory.retention` sub-blocks validate conforming
9
+ * instances and reject malformed ones (`retention.ttl` non-boolean; an
10
+ * unknown `search.modes` enum value; an unknown property under
11
+ * `additionalProperties:false`).
12
+ * - `agent-inventory-response` declares the `memoryDegraded` (bool) +
13
+ * `degradedMemoryDimensions` (closed enum of the eight §A dimension names)
14
+ * inventory fields (RFC 0080 §C), and rejects an out-of-enum dimension.
15
+ * - the eight §A dimension names are stable (the `degradedMemoryDimensions` enum).
16
+ * - `deriveProfiles` surfaces `openwop-memory` for a read/write + long-term
17
+ * payload and withholds it for a `writable:false` payload (the §D predicate).
18
+ *
19
+ * Behavioral assertions (a live `GET /v1/agents` stamping `memoryDegraded` when an
20
+ * agent's `memoryShape` exceeds the host's reconciled model) are gated on
21
+ * `capabilities.agents.manifestRuntime` + `memory` and land in
22
+ * `memory-degraded-projection.test.ts` (deferred per RFC 0080 §Conformance — the
23
+ * degraded projection soft-skips until a reference host computes it). This scenario
24
+ * asserts the wire contract, not host behavior.
25
+ *
26
+ * Spec references:
27
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md (§"Memory capability model")
28
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/profiles.md (§`openwop-memory`)
29
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0080-agent-memory-capability-reconciliation.md
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { readFileSync } from 'node:fs';
34
+ import { join } from 'node:path';
35
+ import Ajv2020 from 'ajv/dist/2020.js';
36
+ import addFormats from 'ajv-formats';
37
+ import { SCHEMAS_DIR } from '../lib/paths.js';
38
+ import { deriveProfiles } from '../lib/profiles.js';
39
+
40
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
41
+
42
+ function loadSchema(name: string): Record<string, unknown> {
43
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
44
+ }
45
+
46
+ /** The canonical eight RFC 0080 §A dimension names, in table order. */
47
+ const DIMENSIONS = [
48
+ 'read',
49
+ 'write',
50
+ 'search',
51
+ 'long-term-durability',
52
+ 'compaction',
53
+ 'attribution',
54
+ 'replay-snapshot',
55
+ 'retention',
56
+ ] as const;
57
+
58
+ describe('memory-capability-model-shape: reconciled dimensions (RFC 0080 §A, server-free)', () => {
59
+ const caps = loadSchema('capabilities.schema.json');
60
+ const memory = (caps.properties as Record<string, { properties?: Record<string, unknown> }>).memory;
61
+
62
+ it('capabilities.memory declares the additive writable / search / retention dimensions', () => {
63
+ for (const dim of ['writable', 'search', 'retention']) {
64
+ expect(
65
+ memory?.properties?.[dim],
66
+ why('agent-memory.md §"Memory capability model"', `capabilities.memory.${dim} MUST be declared (RFC 0080 §A)`),
67
+ ).toBeDefined();
68
+ }
69
+ });
70
+
71
+ it('the pre-existing memory fields are untouched (additive, no relocation)', () => {
72
+ for (const dim of ['supported', 'compaction', 'distillation', 'attribution']) {
73
+ expect(
74
+ memory?.properties?.[dim],
75
+ why('COMPATIBILITY.md §2.1', `capabilities.memory.${dim} MUST remain (RFC 0080 is additive)`),
76
+ ).toBeDefined();
77
+ }
78
+ });
79
+
80
+ it('memory.search / memory.retention validate conforming instances and reject malformed ones', () => {
81
+ const ajv = addFormats(new Ajv2020({ strict: false }));
82
+ // Wrap the extracted sub-block in a standalone schema (no external $refs in the block).
83
+ const validate = ajv.compile({
84
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
85
+ type: 'object',
86
+ additionalProperties: false,
87
+ properties: { memory },
88
+ } as Record<string, unknown>);
89
+
90
+ expect(
91
+ validate({ memory: { supported: true, writable: false, search: { supported: true, modes: ['semantic', 'filter'] }, retention: { ttl: true, forget: true } } }),
92
+ why('capabilities.md §memory', 'a full reconciled-memory advertisement MUST validate'),
93
+ ).toBe(true);
94
+
95
+ expect(
96
+ validate({ memory: { retention: { ttl: 'yes' } } }),
97
+ why('RFC 0080 §A', 'retention.ttl MUST be boolean'),
98
+ ).toBe(false);
99
+
100
+ expect(
101
+ validate({ memory: { search: { supported: true, modes: ['fuzzy'] } } }),
102
+ why('RFC 0080 §A', 'search.modes MUST be the closed enum [semantic, filter]'),
103
+ ).toBe(false);
104
+
105
+ expect(
106
+ validate({ memory: { search: { supported: true, unknownField: 1 } } }),
107
+ why('RFC 0080 §A', 'memory.search MUST be additionalProperties:false'),
108
+ ).toBe(false);
109
+ });
110
+ });
111
+
112
+ describe('memory-capability-model-shape: degraded projection (RFC 0080 §C, server-free)', () => {
113
+ const inventory = loadSchema('agent-inventory-response.schema.json');
114
+
115
+ it('agent-inventory-response declares memoryDegraded + degradedMemoryDimensions', () => {
116
+ const entry = ((inventory.$defs as Record<string, { properties?: Record<string, unknown> }>)
117
+ .AgentInventoryEntry).properties;
118
+ expect(
119
+ entry?.memoryDegraded,
120
+ why('agent-memory.md §C-1', 'memoryDegraded MUST be declared on the inventory entry'),
121
+ ).toBeDefined();
122
+ expect(
123
+ entry?.degradedMemoryDimensions,
124
+ why('agent-memory.md §C-1', 'degradedMemoryDimensions MUST be declared on the inventory entry'),
125
+ ).toBeDefined();
126
+ });
127
+
128
+ it('degradedMemoryDimensions enumerates exactly the eight §A dimension names', () => {
129
+ const entry = ((inventory.$defs as Record<string, { properties?: Record<string, { items?: { enum?: string[] } }> }>)
130
+ .AgentInventoryEntry).properties;
131
+ const enumVals = entry?.degradedMemoryDimensions?.items?.enum ?? [];
132
+ expect(
133
+ [...enumVals].sort(),
134
+ why('agent-memory.md §A', 'the degraded-dimension enum MUST be the eight reconciled dimensions'),
135
+ ).toEqual([...DIMENSIONS].sort());
136
+ });
137
+
138
+ it('the inventory schema round-trips a degraded entry and rejects an out-of-enum dimension', () => {
139
+ const ajv = addFormats(new Ajv2020({ strict: false }));
140
+ const validate = ajv.compile(inventory);
141
+ const base = {
142
+ agentId: 'a', persona: 'A', label: 'A', modelClass: 'standard',
143
+ packName: 'p', packVersion: '1.0.0', toolAllowlist: [], hasHandoffSchemas: false,
144
+ };
145
+ expect(
146
+ validate({ total: 1, agents: [{ ...base, memoryDegraded: true, degradedMemoryDimensions: ['write', 'long-term-durability'] }] }),
147
+ why('agent-memory.md §C-1', 'a degraded inventory entry MUST validate'),
148
+ ).toBe(true);
149
+ expect(
150
+ validate({ total: 1, agents: [{ ...base, memoryDegraded: true, degradedMemoryDimensions: ['telepathy'] }] }),
151
+ why('agent-memory.md §C-1', 'an out-of-enum degraded dimension MUST be rejected'),
152
+ ).toBe(false);
153
+ });
154
+ });
155
+
156
+ describe('memory-capability-model-shape: openwop-memory derivation (RFC 0080 §D, server-free)', () => {
157
+ it('deriveProfiles surfaces openwop-memory for a read/write + long-term host', () => {
158
+ const c = {
159
+ protocolVersion: '1.0',
160
+ supportedEnvelopes: ['clarification.request'],
161
+ schemaVersions: {},
162
+ limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
163
+ memory: { supported: true },
164
+ agents: { memoryBackends: ['long-term'] },
165
+ } as Record<string, unknown>;
166
+ expect(
167
+ deriveProfiles(c).includes('openwop-memory'),
168
+ why('profiles.md §openwop-memory', 'a read/write + long-term host MUST derive openwop-memory'),
169
+ ).toBe(true);
170
+ });
171
+
172
+ it('deriveProfiles withholds openwop-memory from a read-only (writable:false) host', () => {
173
+ const c = {
174
+ protocolVersion: '1.0',
175
+ supportedEnvelopes: ['clarification.request'],
176
+ schemaVersions: {},
177
+ limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
178
+ memory: { supported: true, writable: false },
179
+ agents: { memoryBackends: ['long-term'] },
180
+ } as Record<string, unknown>;
181
+ expect(
182
+ deriveProfiles(c).includes('openwop-memory'),
183
+ why('profiles.md §openwop-memory', 'a read-only host MUST NOT derive openwop-memory'),
184
+ ).toBe(false);
185
+ });
186
+ });
@@ -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