@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
@@ -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 {
@@ -34,6 +34,7 @@ import { driver } from '../lib/driver.js';
34
34
  import { pollUntilTerminal } from '../lib/polling.js';
35
35
  import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  const WORKFLOW_ID = 'conformance-prompt-all-four-kinds';
39
40
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -64,7 +65,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
64
65
  }
65
66
 
66
67
  function promptsSupported(d: DiscoveryDoc | null): boolean {
67
- return d?.capabilities?.prompts?.supported === true;
68
+ return capabilityFamily(d, 'prompts')?.supported === true;
68
69
  }
69
70
 
70
71
  async function readAllEvents(runId: string): Promise<RunEventDoc[]> {
@@ -33,6 +33,7 @@
33
33
  import { describe, it, expect } from 'vitest';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { behaviorGate } from '../lib/behavior-gate.js';
36
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
36
37
 
37
38
  interface DiscoveryDoc {
38
39
  capabilities?: {
@@ -63,7 +64,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
63
64
  }
64
65
 
65
66
  function promptsSupportFull(d: DiscoveryDoc | null): boolean {
66
- const p = d?.capabilities?.prompts;
67
+ const p = capabilityFamily(d, 'prompts');
67
68
  if (!p) return false;
68
69
  return p.supported === true && p.observability === 'full';
69
70
  }
@@ -32,6 +32,7 @@
32
32
  import { describe, it, expect } from 'vitest';
33
33
  import { driver } from '../lib/driver.js';
34
34
  import { behaviorGate } from '../lib/behavior-gate.js';
35
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
36
 
36
37
  interface DiscoveryDoc {
37
38
  capabilities?: {
@@ -59,7 +60,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
59
60
  }
60
61
 
61
62
  function promptsSupportFull(d: DiscoveryDoc | null): boolean {
62
- const p = d?.capabilities?.prompts;
63
+ const p = capabilityFamily(d, 'prompts');
63
64
  if (!p) return false;
64
65
  return p.supported === true && p.observability === 'full';
65
66
  }
@@ -34,6 +34,7 @@ import { driver } from '../lib/driver.js';
34
34
  import { pollUntilTerminal } from '../lib/polling.js';
35
35
  import { isFixtureAdvertised } from '../lib/fixtures.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  const WORKFLOW_ID = 'conformance-prompt-end-to-end';
39
40
  const SKIP_NO_FIXTURE = !isFixtureAdvertised(WORKFLOW_ID);
@@ -64,7 +65,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
64
65
  }
65
66
 
66
67
  function promptsSupported(d: DiscoveryDoc | null): boolean {
67
- return d?.capabilities?.prompts?.supported === true;
68
+ return capabilityFamily(d, 'prompts')?.supported === true;
68
69
  }
69
70
 
70
71
  /** Drain the run's event log via polling. The fixture is tiny so all
@@ -34,6 +34,7 @@ import { join } from 'node:path';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { SCHEMAS_DIR } from '../lib/paths.js';
36
36
  import { behaviorGate } from '../lib/behavior-gate.js';
37
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
37
38
 
38
39
  interface DiscoveryDoc {
39
40
  capabilities?: {
@@ -65,7 +66,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
65
66
  }
66
67
 
67
68
  function endpointsSupported(d: DiscoveryDoc | null): boolean {
68
- return d?.capabilities?.prompts?.endpointsSupported === true;
69
+ return capabilityFamily(d, 'prompts')?.endpointsSupported === true;
69
70
  }
70
71
 
71
72
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -35,6 +35,7 @@
35
35
  import { describe, it, expect } from 'vitest';
36
36
  import { driver } from '../lib/driver.js';
37
37
  import { behaviorGate } from '../lib/behavior-gate.js';
38
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
38
39
 
39
40
  interface DiscoveryDoc {
40
41
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function mutableSupport(d: DiscoveryDoc | null): boolean {
63
- const p = d?.capabilities?.prompts;
64
+ const p = capabilityFamily(d, 'prompts');
64
65
  return p?.endpointsSupported === true && p?.mutableLibrary === true;
65
66
  }
66
67
 
@@ -41,6 +41,7 @@
41
41
  import { describe, it, expect } from 'vitest';
42
42
  import { randomUUID } from 'node:crypto';
43
43
  import { driver } from '../lib/driver.js';
44
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
44
45
 
45
46
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
46
47
 
@@ -71,7 +72,7 @@ describe.skipIf(HTTP_SKIP)(
71
72
  ctx.skip();
72
73
  return;
73
74
  }
74
- const mutableLibrary = d.capabilities?.prompts?.mutableLibrary;
75
+ const mutableLibrary = capabilityFamily(d, 'prompts')?.mutableLibrary;
75
76
  if (mutableLibrary !== true) {
76
77
  ctx.skip();
77
78
  return;
@@ -46,6 +46,7 @@
46
46
  import { describe, it, expect } from 'vitest';
47
47
  import { driver } from '../lib/driver.js';
48
48
  import { behaviorGate } from '../lib/behavior-gate.js';
49
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
49
50
 
50
51
  interface DiscoveryDoc {
51
52
  capabilities?: {
@@ -79,7 +80,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
79
80
  }
80
81
 
81
82
  function endpointsSupported(d: DiscoveryDoc | null): boolean {
82
- return d?.capabilities?.prompts?.endpointsSupported === true;
83
+ return capabilityFamily(d, 'prompts')?.endpointsSupported === true;
83
84
  }
84
85
 
85
86
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -67,6 +67,7 @@
67
67
  import { describe, it, expect } from 'vitest';
68
68
  import { randomUUID } from 'node:crypto';
69
69
  import { driver } from '../lib/driver.js';
70
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
70
71
 
71
72
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
72
73
 
@@ -101,7 +102,7 @@ describe.skipIf(HTTP_SKIP)(
101
102
  ctx.skip();
102
103
  return;
103
104
  }
104
- const promptsSupported = d.capabilities?.prompts?.supported;
105
+ const promptsSupported = capabilityFamily(d, 'prompts')?.supported;
105
106
  if (promptsSupported !== true) {
106
107
  ctx.skip();
107
108
  return;
@@ -23,6 +23,7 @@
23
23
  import { describe, it, expect } from 'vitest';
24
24
  import { driver } from '../lib/driver.js';
25
25
  import { behaviorGate } from '../lib/behavior-gate.js';
26
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
26
27
 
27
28
  interface DiscoveryDoc {
28
29
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function endpointsSupported(d: DiscoveryDoc | null): boolean {
63
- return d?.capabilities?.prompts?.endpointsSupported === true;
64
+ return capabilityFamily(d, 'prompts')?.endpointsSupported === true;
64
65
  }
65
66
 
66
67
  /** Pick a template that has at least one input-source variable
@@ -30,6 +30,7 @@
30
30
  import { describe, it, expect } from 'vitest';
31
31
  import { driver } from '../lib/driver.js';
32
32
  import { behaviorGate } from '../lib/behavior-gate.js';
33
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
33
34
 
34
35
  interface DiscoveryDoc {
35
36
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function promptsAgentBindings(d: DiscoveryDoc | null): boolean {
63
- const p = d?.capabilities?.prompts;
64
+ const p = capabilityFamily(d, 'prompts');
64
65
  if (!p) return false;
65
66
  return p.supported === true && p.agentBindings === true;
66
67
  }
@@ -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 DiscoveryDoc {
36
37
  capabilities?: {
@@ -60,7 +61,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
60
61
  }
61
62
 
62
63
  function promptsSupported(d: DiscoveryDoc | null): boolean {
63
- return d?.capabilities?.prompts?.supported === true;
64
+ return capabilityFamily(d, 'prompts')?.supported === true;
64
65
  }
65
66
 
66
67
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -29,6 +29,7 @@
29
29
  import { describe, it, expect } from 'vitest';
30
30
  import { driver } from '../lib/driver.js';
31
31
  import { behaviorGate } from '../lib/behavior-gate.js';
32
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
32
33
 
33
34
  interface DiscoveryDoc {
34
35
  capabilities?: {
@@ -58,7 +59,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
58
59
  }
59
60
 
60
61
  function promptsSupported(d: DiscoveryDoc | null): boolean {
61
- return d?.capabilities?.prompts?.supported === true;
62
+ return capabilityFamily(d, 'prompts')?.supported === true;
62
63
  }
63
64
 
64
65
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -33,6 +33,7 @@ import { readFileSync } from 'node:fs';
33
33
  import { join } from 'node:path';
34
34
  import { driver } from '../lib/driver.js';
35
35
  import { SCHEMAS_DIR } from '../lib/paths.js';
36
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
36
37
 
37
38
  const PROMPT_KIND_VALUES = ['system', 'user', 'few-shot', 'schema-hint'] as const;
38
39
 
@@ -326,7 +327,7 @@ describe.skipIf(HTTP_SKIP)('prompt-template-shape: capabilities.prompts advertis
326
327
  it('capabilities.prompts (when present) carries the optional shape per RFC 0027 §D', async () => {
327
328
  const d = await readDiscovery();
328
329
  if (d === null) return;
329
- const prompts = d.capabilities?.prompts;
330
+ const prompts = capabilityFamily(d, 'prompts');
330
331
  if (prompts === undefined) return; // optional block; host MAY omit
331
332
  expect(
332
333
  typeof prompts.supported,
@@ -26,6 +26,7 @@ import { join } from 'node:path';
26
26
  import { driver } from '../lib/driver.js';
27
27
  import { SCHEMAS_DIR } from '../lib/paths.js';
28
28
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
29
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
29
30
 
30
31
  interface DiscoveryDoc {
31
32
  capabilities?: {
@@ -36,7 +37,7 @@ interface DiscoveryDoc {
36
37
  async function readProviderUsageCap(): Promise<{ supported?: boolean; costEstimates?: boolean; currency?: string } | null> {
37
38
  const res = await driver.get('/.well-known/openwop');
38
39
  const body = res.json as DiscoveryDoc | undefined;
39
- const cap = body?.capabilities?.providerUsage;
40
+ const cap = capabilityFamily(body, 'providerUsage');
40
41
  return cap && typeof cap === 'object' ? cap : null;
41
42
  }
42
43
 
@@ -46,6 +46,7 @@
46
46
 
47
47
  import { describe, it, expect } from 'vitest';
48
48
  import { driver } from '../lib/driver.js';
49
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
49
50
 
50
51
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
51
52
 
@@ -79,7 +80,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: advertisement shape (R
79
80
  ctx.skip();
80
81
  return;
81
82
  }
82
- const rd = d.capabilities?.multiAgent?.executionModel?.replayDeterminism;
83
+ const rd = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.replayDeterminism;
83
84
  if (rd === undefined) {
84
85
  ctx.skip(); // optional advertisement — host hasn't opted in
85
86
  return;
@@ -94,7 +95,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: advertisement shape (R
94
95
  ).toBe('boolean');
95
96
 
96
97
  if (rd.supported === true) {
97
- const version = d.capabilities?.multiAgent?.executionModel?.version as number | undefined;
98
+ const version = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version as number | undefined;
98
99
  expect(
99
100
  typeof version === 'number' && version >= 4,
100
101
  driver.describe(
@@ -170,7 +171,7 @@ describe.skipIf(HTTP_SKIP)('replay-divergence-at-refusal: behavioral (RFC 0041
170
171
 
171
172
  async function gateOnPhase4(ctx: { skip: () => void }): Promise<boolean> {
172
173
  const d = await readDiscovery();
173
- const rd = d?.capabilities?.multiAgent?.executionModel?.replayDeterminism;
174
+ const rd = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.replayDeterminism;
174
175
  if (rd?.supported !== true || rd?.refusalDivergenceEmission !== true) {
175
176
  ctx.skip();
176
177
  return false;
@@ -110,7 +110,9 @@ function structuralShape(
110
110
  return events.map((e) => ({
111
111
  type: e.type,
112
112
  nodeId: e.nodeId ?? null,
113
- data: e.data ?? null,
113
+ // Canonical `payload` (run-event.schema.json) with the legacy `data`
114
+ // field as a fallback for hosts that haven't migrated their envelope.
115
+ data: e.payload ?? e.data ?? null,
114
116
  }));
115
117
  }
116
118
 
@@ -53,6 +53,7 @@
53
53
  import { describe, it, expect } from 'vitest';
54
54
  import { driver } from '../lib/driver.js';
55
55
  import { expectedCacheKey, callCacheKeySeam as callSeam } from '../lib/llm-cache-key-recipe.js';
56
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
56
57
 
57
58
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
58
59
 
@@ -167,7 +168,7 @@ describe.skipIf(HTTP_SKIP)('replay-llm-cache-key-portable: non-recipe-field inva
167
168
  describe.skipIf(HTTP_SKIP)('replay-llm-cache-key-portable: Phase 4 advertisement alignment (RFC 0041 §D)', () => {
168
169
  it('hosts advertising version: 4 MUST advertise replayDeterminism.llmCacheKeyRecipe', async (ctx) => {
169
170
  const d = await readDiscovery();
170
- const em = d?.capabilities?.multiAgent?.executionModel;
171
+ const em = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel;
171
172
  const version = em?.version;
172
173
  if (typeof version !== 'number' || version < 4) {
173
174
  ctx.skip(); // pre-Phase-4 or no multiAgent advertisement
@@ -60,7 +60,9 @@ function structuralShape(events: readonly RawEvent[]): Array<{ type: unknown; no
60
60
  return events.map((e) => ({
61
61
  type: e.type,
62
62
  nodeId: e.nodeId ?? null,
63
- data: e.data ?? null,
63
+ // Canonical `payload` (run-event.schema.json) with the legacy `data`
64
+ // field as a fallback for hosts that haven't migrated their envelope.
65
+ data: e.payload ?? e.data ?? null,
64
66
  }));
65
67
  }
66
68