@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,145 @@
1
+ /**
2
+ * oauth-authorization-code-roundtrip — RFC 0047 §C (the authorization-code grant
3
+ * end-to-end) + §C.2 / `credential-payload-redaction`.
4
+ *
5
+ * Closes the RFC 0047 Tier-2 gap: `oauth-capability-shape` proves the discovery
6
+ * block is well-formed and `oauth-connector-redaction` proves an already-acquired
7
+ * token doesn't leak — but nothing exercised the actual authorization-code DANCE
8
+ * (redirect → callback → token exchange) against a known provider. This scenario
9
+ * drives that roundtrip against ONE canonical synthetic provider whose endpoints a
10
+ * conformance test double serves, so a host can prove the grant without a live IdP.
11
+ *
12
+ * The synthetic provider + its canned exchange are defined in
13
+ * `fixtures/oauth-providers/synthetic.json`; the constants below mirror it (kept
14
+ * inline so the scenario runs from the published tarball without fixture-path
15
+ * resolution, exactly like `oauth-connector-redaction`'s TOKEN_CANARY).
16
+ *
17
+ * Capability-gated: skips unless the host advertises
18
+ * `capabilities.oauth.supported = true` AND lists `authorization_code` in
19
+ * `capabilities.oauth.grants`. Behavioral probe drives the optional host seam
20
+ * `POST /v1/host/sample/oauth/authorize-code-roundtrip`; a 404 (seam not wired)
21
+ * is a soft-skip — this is a Tier-2 host-pending scenario.
22
+ *
23
+ * Asserts, when the seam is present:
24
+ * 1. The roundtrip succeeds and returns a credential REFERENCE (the token was
25
+ * acquired + persisted as a host.credentials entry), never the token itself.
26
+ * 2. `connector.authorized` carries `{ provider, credentialRef, scopes }` and
27
+ * none of the token / refresh / code / state / redirectUri / codeVerifier.
28
+ * 3. RFC 0047 §C — the authorization code, redirect URI, state, and PKCE
29
+ * verifier MUST NOT appear on ANY run-visible surface; §C.2 — neither MUST
30
+ * the access/refresh token (the canaries are absent from the whole response).
31
+ *
32
+ * @see RFCS/0047-host-oauth-connector-flows.md §C
33
+ * @see conformance/fixtures/oauth-providers/synthetic.json
34
+ * @see SECURITY/invariants.yaml id: credential-payload-redaction
35
+ */
36
+
37
+ import { describe, it, expect } from 'vitest';
38
+ import { driver } from '../lib/driver.js';
39
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
40
+
41
+ interface DiscoveryOAuth {
42
+ supported?: boolean;
43
+ grants?: string[];
44
+ }
45
+
46
+ // Mirrors fixtures/oauth-providers/synthetic.json — keep in sync.
47
+ const SYNTHETIC = {
48
+ provider: 'synthetic',
49
+ authUrl: 'https://oauth.synthetic.openwop.test/authorize',
50
+ tokenUrl: 'https://oauth.synthetic.openwop.test/token',
51
+ scopes: ['openwop.read', 'openwop.write'],
52
+ authorizationCode: 'openwop-synthetic-auth-code-1f4b9e',
53
+ state: 'openwop-synthetic-state-7c2a8d',
54
+ redirectUri: 'https://host.example/openwop/oauth/callback',
55
+ codeVerifier: 'openwop-synthetic-pkce-verifier-3e9f1b2c5a7d4e8f0a1b2c3d4e5f6a7b',
56
+ accessTokenCanary: 'OPENWOP_OAUTH_TOKEN_CANARY_9d4c1f7a',
57
+ refreshTokenCanary: 'OPENWOP_OAUTH_REFRESH_CANARY_2b8e6a3f',
58
+ } as const;
59
+
60
+ // Values that MUST NOT appear on any run-visible surface (RFC 0047 §C + §C.2).
61
+ const SECRET_VALUES: readonly string[] = [
62
+ SYNTHETIC.accessTokenCanary,
63
+ SYNTHETIC.refreshTokenCanary,
64
+ SYNTHETIC.authorizationCode,
65
+ SYNTHETIC.state,
66
+ SYNTHETIC.codeVerifier,
67
+ ];
68
+
69
+ async function readOAuth(): Promise<DiscoveryOAuth | null> {
70
+ const res = await driver.get('/.well-known/openwop');
71
+ return capabilityFamily<DiscoveryOAuth>(res.json, 'oauth') ?? null;
72
+ }
73
+
74
+ describe('oauth-authorization-code-roundtrip: the grant dance (RFC 0047 §C)', () => {
75
+ it('acquires a token via authorization_code and returns a reference, never the token', async () => {
76
+ const oauth = await readOAuth();
77
+ if (!oauth?.supported) return; // capability-gated
78
+ if (!Array.isArray(oauth.grants) || !oauth.grants.includes('authorization_code')) return; // grant-gated
79
+
80
+ // Seam contract: the host performs the full authorization-code roundtrip
81
+ // against the synthetic provider's authUrl/tokenUrl, persists the acquired
82
+ // token as a host.credentials entry, and returns the run-observable surfaces
83
+ // (events incl. connector.authorized + snapshot + any debug bundle) plus the
84
+ // resulting credentialRef.
85
+ const res = await driver.post('/v1/host/sample/oauth/authorize-code-roundtrip', {
86
+ provider: SYNTHETIC.provider,
87
+ authUrl: SYNTHETIC.authUrl,
88
+ tokenUrl: SYNTHETIC.tokenUrl,
89
+ scopes: SYNTHETIC.scopes,
90
+ authorizationCode: SYNTHETIC.authorizationCode,
91
+ state: SYNTHETIC.state,
92
+ redirectUri: SYNTHETIC.redirectUri,
93
+ codeVerifier: SYNTHETIC.codeVerifier,
94
+ accessTokenCanary: SYNTHETIC.accessTokenCanary,
95
+ refreshTokenCanary: SYNTHETIC.refreshTokenCanary,
96
+ });
97
+ // A host that hasn't wired the seam soft-skips (Tier-2, host-pending).
98
+ if (res.status === 404) return;
99
+
100
+ expect(
101
+ res.status,
102
+ driver.describe(
103
+ 'RFC 0047 §C',
104
+ 'the authorize-code-roundtrip seam MUST perform the authorization_code grant against the synthetic provider and return the run observable surfaces',
105
+ ),
106
+ ).toBeLessThan(400);
107
+
108
+ const body = (res.json ?? {}) as { credentialRef?: unknown };
109
+ expect(
110
+ typeof body.credentialRef === 'string' && body.credentialRef.length > 0,
111
+ driver.describe(
112
+ 'RFC 0047 §C',
113
+ 'a successful roundtrip MUST resolve to a credential REFERENCE (token persisted as a host.credentials entry), not the raw token',
114
+ ),
115
+ ).toBe(true);
116
+
117
+ // §C + §C.2 — no secret material anywhere in the observable response.
118
+ const serialized = JSON.stringify(res.json ?? {});
119
+ for (const secret of SECRET_VALUES) {
120
+ expect(
121
+ serialized.includes(secret),
122
+ driver.describe(
123
+ 'RFC 0047 §C / SECURITY/invariants.yaml credential-payload-redaction',
124
+ `the authorization code, state, PKCE verifier, and acquired token material MUST NOT appear on any run-visible surface — leaked: ${secret.slice(0, 16)}…`,
125
+ ),
126
+ ).toBe(false);
127
+ }
128
+
129
+ // §C — connector.authorized carries the reference + scopes, never the token.
130
+ const events = (res.json as { events?: Array<{ type?: string; payload?: Record<string, unknown> }> })?.events;
131
+ if (Array.isArray(events)) {
132
+ const authorized = events.find((e) => e?.type === 'connector.authorized');
133
+ if (authorized?.payload) {
134
+ const keys = Object.keys(authorized.payload);
135
+ expect(
136
+ keys.includes('credentialRef') && !keys.includes('access_token') && !keys.includes('refresh_token'),
137
+ driver.describe(
138
+ 'RFC 0047 §C',
139
+ 'connector.authorized MUST carry { provider, credentialRef, scopes } and MUST NOT carry token material',
140
+ ),
141
+ ).toBe(true);
142
+ }
143
+ }
144
+ });
145
+ });
@@ -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