@openwop/openwop-conformance 1.134.0 → 1.135.1

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 (58) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/schemas/CORPUS-STAMP.json +2 -2
  4. package/schemas/capabilities.schema.json +10 -0
  5. package/src/lib/discovery-capabilities.ts +21 -0
  6. package/src/lib/feedback.ts +2 -1
  7. package/src/lib/heartbeat.ts +2 -5
  8. package/src/lib/memoryAttribution.ts +2 -5
  9. package/src/lib/toolHooks.ts +2 -5
  10. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +2 -1
  11. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +2 -1
  12. package/src/scenarios/aiEnvelope.redaction.test.ts +2 -1
  13. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +3 -3
  14. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +2 -1
  15. package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -2
  16. package/src/scenarios/blob-cross-tenant-isolation.test.ts +2 -1
  17. package/src/scenarios/blob-presign-expiry.test.ts +2 -1
  18. package/src/scenarios/blob-roundtrip.test.ts +0 -0
  19. package/src/scenarios/cache-cross-tenant-isolation.test.ts +2 -1
  20. package/src/scenarios/cache-ttl-expiry.test.ts +2 -1
  21. package/src/scenarios/discovery-families-view.test.ts +38 -0
  22. package/src/scenarios/discovery.test.ts +6 -9
  23. package/src/scenarios/kv-atomic-increment.test.ts +2 -1
  24. package/src/scenarios/kv-cas.test.ts +2 -1
  25. package/src/scenarios/kv-cross-tenant-isolation.test.ts +2 -1
  26. package/src/scenarios/kv-ttl-expiry.test.ts +2 -1
  27. package/src/scenarios/mcp-server-elicitation-bridge.test.ts +2 -1
  28. package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +2 -1
  29. package/src/scenarios/mcp-server-resource-roundtrip.test.ts +2 -1
  30. package/src/scenarios/mcp-server-sampling-bridge.test.ts +2 -1
  31. package/src/scenarios/mcp-server-tool-roundtrip.test.ts +2 -1
  32. package/src/scenarios/mcp-server-untrusted-args.test.ts +2 -1
  33. package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -4
  34. package/src/scenarios/media-url-inline-cap.test.ts +3 -5
  35. package/src/scenarios/metric-emission.test.ts +3 -4
  36. package/src/scenarios/node-module-required-capabilities-shape.test.ts +2 -1
  37. package/src/scenarios/otel-emission-grpc.test.ts +3 -6
  38. package/src/scenarios/otel-emission.test.ts +2 -1
  39. package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +2 -1
  40. package/src/scenarios/otel-trace-propagation.test.ts +2 -1
  41. package/src/scenarios/pack-registry-isolation.test.ts +2 -1
  42. package/src/scenarios/pack-registry-publish.test.ts +2 -1
  43. package/src/scenarios/queue-ack-nack-dlq.test.ts +2 -1
  44. package/src/scenarios/queue-cross-tenant-isolation.test.ts +2 -1
  45. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +2 -1
  46. package/src/scenarios/search-bm25-roundtrip.test.ts +2 -1
  47. package/src/scenarios/sql-injection-rejection.test.ts +2 -1
  48. package/src/scenarios/sql-transaction-atomicity.test.ts +2 -1
  49. package/src/scenarios/stream-subscribe-from-beginning.test.ts +2 -1
  50. package/src/scenarios/table-cross-tenant-isolation.test.ts +2 -1
  51. package/src/scenarios/table-cursor-pagination.test.ts +2 -1
  52. package/src/scenarios/table-schema-enforcement.test.ts +2 -1
  53. package/src/scenarios/vector-knn-roundtrip.test.ts +2 -1
  54. package/src/scenarios/versioned-composition-profiles.test.ts +18 -0
  55. package/src/scenarios/webhook-negative.test.ts +3 -3
  56. package/src/scenarios/webhook-sig-algorithm.test.ts +2 -1
  57. package/src/scenarios/webhook-signed-delivery.test.ts +3 -2
  58. package/src/scenarios/workflow-chain-host-expansion.test.ts +3 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.134.0",
3
+ "version": "1.135.1",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
- "suiteVersion": "1.134.0",
4
- "corpusCommit": "fbb82fa8ea94c25358a63fda98bd49fdb528693a"
3
+ "suiteVersion": "1.135.1",
4
+ "corpusCommit": "821921ce3b4a6cbbc20fe1aafe81a7413e10c9d9"
5
5
  }
@@ -3332,6 +3332,16 @@
3332
3332
  "type": "boolean",
3333
3333
  "description": "Host advertises a client-side MCP surface (ctx.mcp.*). See spec/v1/mcp-integration.md."
3334
3334
  },
3335
+ "serverUrls": {
3336
+ "type": "array",
3337
+ "minItems": 1,
3338
+ "uniqueItems": true,
3339
+ "items": {
3340
+ "type": "string",
3341
+ "minLength": 1
3342
+ },
3343
+ "description": "Where the host's MCP server mount(s) answer JSON-RPC \u2014 an absolute URL or a path relative to the discovery origin (S25: a path is joined to the base URL, an absolute URL is used as-is). The v1.0 conformance baseline (`mcp-discoverability.test.ts`, spec/v1/mcp-integration.md \u00a7Conformance + interop) REQUIRES a non-empty `serverUrls` whenever `supported: true`, and since suite 1.134.0 every server-side scenario POSTs to `serverUrls[0]`. Required by the scenario and absent from this schema until 2026-08-17 (S27) \u2014 the second sibling host emitted it and measured the contradiction."
3344
+ },
3335
3345
  "serverMount": {
3336
3346
  "type": "object",
3337
3347
  "description": "Server-side MCP composition (workflow IS an MCP server). When supported, the host mounts an MCP endpoint and routes inbound tools/call, resources/read, prompts/get into workflows. Inbound MUST be treated as `trustBoundary: 'untrusted'`.",
@@ -47,3 +47,24 @@ export async function readCapabilityFamily<T = Record<string, unknown>>(
47
47
  if (res.status !== 200) return undefined;
48
48
  return capabilityFamily<T>(res.json, name);
49
49
  }
50
+
51
+ /**
52
+ * A root-first VIEW of the discovery document's capability families for
53
+ * readers that were written against the deprecated top-level `capabilities`
54
+ * wrapper (RFC 0073: root is the MUST; the wrapper is a v1.x-tolerated
55
+ * mirror that retires at v2). Root families win; a family that exists only in
56
+ * the wrapper is still visible so a legacy host does not lose coverage. Until
57
+ * suite 1.135.0 forty-four readers consulted the wrapper ONLY, which meant a
58
+ * root-only host — the shape RFC 0073 asks for — silently lost their legs
59
+ * (S26, found by the second sibling host, which had to keep its wrapper as a
60
+ * mirror because of it).
61
+ */
62
+ export function discoveryFamilies(doc: unknown): Record<string, unknown> {
63
+ if (!doc || typeof doc !== 'object') return {};
64
+ const root = doc as Record<string, unknown>;
65
+ const wrapper = root['capabilities'];
66
+ const merged: Record<string, unknown> = {};
67
+ if (wrapper && typeof wrapper === 'object' && !Array.isArray(wrapper)) Object.assign(merged, wrapper as Record<string, unknown>);
68
+ for (const [k, v] of Object.entries(root)) if (k !== 'capabilities') merged[k] = v;
69
+ return merged;
70
+ }
@@ -4,6 +4,7 @@
4
4
  * standard `../lib/feedback.js` path.
5
5
  */
6
6
  import { driver } from './driver.js';
7
+ import { discoveryFamilies } from './discovery-capabilities.js';
7
8
  import { isFixtureAdvertised } from './fixtures.js';
8
9
 
9
10
  interface DiscoveryDoc {
@@ -14,7 +15,7 @@ interface DiscoveryDoc {
14
15
  export async function readFeedbackCap(): Promise<Record<string, unknown> | null> {
15
16
  const res = await driver.get('/.well-known/openwop');
16
17
  const body = res.json as DiscoveryDoc | undefined;
17
- const top = body?.capabilities as Record<string, unknown> | undefined;
18
+ const top = discoveryFamilies(body);
18
19
  const fb = top && typeof top === 'object' ? (top as Record<string, unknown>)['feedback'] : undefined;
19
20
  return fb && typeof fb === 'object' ? (fb as Record<string, unknown>) : null;
20
21
  }
@@ -3,15 +3,12 @@
3
3
  * Lives in lib/ (not a *.test.ts) so scenarios import it via `../lib/heartbeat.js`.
4
4
  */
5
5
  import { driver } from './driver.js';
6
-
7
- interface DiscoveryDoc {
8
- capabilities?: Record<string, unknown>;
9
- }
6
+ import { discoveryFamilies } from './discovery-capabilities.js';
10
7
 
11
8
  /** Reads `capabilities.heartbeat` from discovery; null when unadvertised. */
12
9
  export async function readHeartbeatCap(): Promise<Record<string, unknown> | null> {
13
10
  const res = await driver.get('/.well-known/openwop');
14
- const caps = (res.json as DiscoveryDoc | undefined)?.capabilities;
11
+ const caps = discoveryFamilies(res.json);
15
12
  const hb = caps && typeof caps === 'object' ? (caps as Record<string, unknown>)['heartbeat'] : undefined;
16
13
  return hb && typeof hb === 'object' ? (hb as Record<string, unknown>) : null;
17
14
  }
@@ -3,16 +3,13 @@
3
3
  * Lives in lib/ (not a *.test.ts) so scenarios import it via `../lib/memoryAttribution.js`.
4
4
  */
5
5
  import { driver } from './driver.js';
6
+ import { discoveryFamilies } from './discovery-capabilities.js';
6
7
  import { isFixtureAdvertised } from './fixtures.js';
7
8
 
8
- interface DiscoveryDoc {
9
- capabilities?: Record<string, unknown>;
10
- }
11
-
12
9
  /** Reads `capabilities.memory.attribution` from discovery; null when unadvertised. */
13
10
  export async function readMemoryAttributionCap(): Promise<Record<string, unknown> | null> {
14
11
  const res = await driver.get('/.well-known/openwop');
15
- const caps = (res.json as DiscoveryDoc | undefined)?.capabilities;
12
+ const caps = discoveryFamilies(res.json);
16
13
  const mem = caps && typeof caps === 'object' ? (caps as Record<string, unknown>)['memory'] : undefined;
17
14
  const attr = mem && typeof mem === 'object' ? (mem as Record<string, unknown>)['attribution'] : undefined;
18
15
  return attr && typeof attr === 'object' ? (attr as Record<string, unknown>) : null;
@@ -3,15 +3,12 @@
3
3
  * Lives in lib/ (not a *.test.ts) so scenarios import it via `../lib/toolHooks.js`.
4
4
  */
5
5
  import { driver } from './driver.js';
6
-
7
- interface DiscoveryDoc {
8
- capabilities?: Record<string, unknown>;
9
- }
6
+ import { discoveryFamilies } from './discovery-capabilities.js';
10
7
 
11
8
  /** Reads `capabilities.toolHooks` from discovery; null when unadvertised. */
12
9
  export async function readToolHooksCap(): Promise<Record<string, unknown> | null> {
13
10
  const res = await driver.get('/.well-known/openwop');
14
- const caps = (res.json as DiscoveryDoc | undefined)?.capabilities;
11
+ const caps = discoveryFamilies(res.json);
15
12
  const th = caps && typeof caps === 'object' ? (caps as Record<string, unknown>)['toolHooks'] : undefined;
16
13
  return th && typeof th === 'object' ? (th as Record<string, unknown>) : null;
17
14
  }
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { describe, it, expect, afterEach } from 'vitest';
24
24
  import { driver } from '../lib/driver.js';
25
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
25
26
  import { setHostCapability, resetHostCapabilities, isToggleAvailable } from '../lib/host-toggle.js';
26
27
 
27
28
  interface DiscoveryDoc {
@@ -32,7 +33,7 @@ describe('aiEnvelope.contractRefusal: advertisement shape (FINAL v1.1)', () => {
32
33
  it('opted-in hosts advertise envelopeContracts.advertised as a boolean', async () => {
33
34
  const res = await driver.get('/.well-known/openwop');
34
35
  const body = res.json as DiscoveryDoc | undefined;
35
- const top = body?.capabilities as Record<string, unknown> | undefined;
36
+ const top = discoveryFamilies(body);
36
37
  const block = top && typeof top === 'object' ? top['envelopeContracts'] : undefined;
37
38
  if (block === undefined) return; // absent — skip
38
39
  expect(
@@ -22,6 +22,7 @@
22
22
 
23
23
  import { describe, it, expect } from 'vitest';
24
24
  import { driver } from '../lib/driver.js';
25
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
25
26
 
26
27
  interface DiscoveryDoc {
27
28
  capabilities?: Record<string, unknown>;
@@ -30,7 +31,7 @@ interface DiscoveryDoc {
30
31
  async function isEnvelopeContractsAdvertised(): Promise<boolean> {
31
32
  const res = await driver.get('/.well-known/openwop');
32
33
  const body = res.json as DiscoveryDoc | undefined;
33
- const top = body?.capabilities as Record<string, unknown> | undefined;
34
+ const top = discoveryFamilies(body);
34
35
  const block = top && typeof top === 'object' ? (top['envelopeContracts'] as Record<string, unknown> | undefined) : undefined;
35
36
  return Boolean(block && block['advertised'] === true);
36
37
  }
@@ -24,6 +24,7 @@
24
24
 
25
25
  import { describe, it, expect } from 'vitest';
26
26
  import { driver } from '../lib/driver.js';
27
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
27
28
 
28
29
  interface DiscoveryDoc {
29
30
  capabilities?: Record<string, unknown>;
@@ -32,7 +33,7 @@ interface DiscoveryDoc {
32
33
  async function isBYOKAdvertised(): Promise<boolean> {
33
34
  const res = await driver.get('/.well-known/openwop');
34
35
  const body = res.json as DiscoveryDoc | undefined;
35
- const top = body?.capabilities as Record<string, unknown> | undefined;
36
+ const top = discoveryFamilies(body);
36
37
  const secrets = top && typeof top === 'object' ? top['secrets'] : undefined;
37
38
  return Boolean(secrets && typeof secrets === 'object');
38
39
  }
@@ -29,7 +29,7 @@ interface DiscoveryDoc {
29
29
  async function isEnvelopeContractsAdvertised(): Promise<boolean> {
30
30
  const res = await driver.get('/.well-known/openwop');
31
31
  const body = res.json as DiscoveryDoc | undefined;
32
- const top = body?.capabilities as Record<string, unknown> | undefined;
32
+ const top = discoveryFamilies(body);
33
33
  const block = top && typeof top === 'object' ? (top['envelopeContracts'] as Record<string, unknown> | undefined) : undefined;
34
34
  return Boolean(block && block['advertised'] === true);
35
35
  }
@@ -38,7 +38,7 @@ describe('aiEnvelope.schemaDrift: advertisement shape (FINAL v1.1)', () => {
38
38
  it('capabilities.envelopeStrictness is either absent (treated as "warn") or "warn" | "strict"', async () => {
39
39
  const res = await driver.get('/.well-known/openwop');
40
40
  const body = res.json as DiscoveryDoc | undefined;
41
- const top = body?.capabilities as Record<string, unknown> | undefined;
41
+ const top = discoveryFamilies(body);
42
42
  const val = top && typeof top === 'object' ? top['envelopeStrictness'] : undefined;
43
43
  if (val === undefined) return; // absent → treated as 'warn'; skip
44
44
  expect(
@@ -189,7 +189,7 @@ describe('aiEnvelope.schemaDrift: behavioral strictness gate (FINAL v1.1)', () =
189
189
  // E.2 OTel scrape seam.
190
190
  import { queryTestSpans, isOtelSeamAvailable } from '../lib/otel-scrape.js';
191
191
  import { resetTestSeam } from '../lib/event-log-query.js';
192
- import { capabilityFamily } from '../lib/discovery-capabilities.js';
192
+ import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
193
193
 
194
194
  describe('aiEnvelope.schemaDrift: OTel drift attribute projection (E.2)', () => {
195
195
  it('below-floor + strictness:warn → OTel span MUST carry envelope_schema_version_drift attribute', async () => {
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { describe, it, expect } from 'vitest';
25
25
  import { driver } from '../lib/driver.js';
26
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
26
27
 
27
28
  interface DiscoveryDoc {
28
29
  capabilities?: Record<string, unknown>;
@@ -31,7 +32,7 @@ interface DiscoveryDoc {
31
32
  async function readMcpTrustBoundary(): Promise<string | null> {
32
33
  const res = await driver.get('/.well-known/openwop');
33
34
  const body = res.json as DiscoveryDoc | undefined;
34
- const top = body?.capabilities as Record<string, unknown> | undefined;
35
+ const top = discoveryFamilies(body);
35
36
  const mcp = top && typeof top === 'object' ? (top['mcpClient'] as Record<string, unknown> | undefined) : undefined;
36
37
  if (!mcp || typeof mcp !== 'object') return null;
37
38
  const tb = mcp['trustBoundary'];
@@ -29,7 +29,7 @@ const UNIVERSALS = ['clarification.request', 'schema.request', 'schema.response'
29
29
  async function readEnvelopeContracts(): Promise<{ advertised: boolean } | null> {
30
30
  const res = await driver.get('/.well-known/openwop');
31
31
  const body = res.json as DiscoveryDoc | undefined;
32
- const top = body?.capabilities as Record<string, unknown> | undefined;
32
+ const top = discoveryFamilies(body);
33
33
  const block = top && typeof top === 'object' ? (top['envelopeContracts'] as Record<string, unknown> | undefined) : undefined;
34
34
  if (!block || typeof block !== 'object') return null;
35
35
  return { advertised: block['advertised'] === true };
@@ -166,7 +166,7 @@ describe('aiEnvelope.universalKinds: behavioral accept via /v1/host/sample/envel
166
166
 
167
167
  // E.1 engine-projection via the test-only event-log seam.
168
168
  import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
169
- import { capabilityFamily } from '../lib/discovery-capabilities.js';
169
+ import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
170
170
 
171
171
  describe('aiEnvelope.universalKinds: engine projection via event-log seam', () => {
172
172
  it('clarification.request MUST be lifted to interrupt.requested { kind: "clarification" } per interrupt.md', async () => {
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { describe, it, expect } from 'vitest';
11
11
  import { driver } from '../lib/driver.js';
12
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
12
13
 
13
14
  interface DiscoveryDoc {
14
15
  capabilities?: Record<string, unknown>;
@@ -17,7 +18,7 @@ interface DiscoveryDoc {
17
18
  async function readCap(): Promise<Record<string, unknown> | null> {
18
19
  const res = await driver.get('/.well-known/openwop');
19
20
  const body = res.json as DiscoveryDoc | undefined;
20
- const top = body?.capabilities as Record<string, unknown> | undefined;
21
+ const top = discoveryFamilies(body);
21
22
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["blobStorage"] : undefined;
22
23
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
23
24
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
17
  import { driver } from '../lib/driver.js';
18
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
18
19
 
19
20
  interface DiscoveryDoc {
20
21
  capabilities?: Record<string, unknown>;
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
23
24
  async function readCap(): Promise<Record<string, unknown> | null> {
24
25
  const res = await driver.get('/.well-known/openwop');
25
26
  const body = res.json as DiscoveryDoc | undefined;
26
- const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const top = discoveryFamilies(body);
27
28
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["blobStorage"] : undefined;
28
29
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
30
  }
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { describe, it, expect } from 'vitest';
11
11
  import { driver } from '../lib/driver.js';
12
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
12
13
 
13
14
  interface DiscoveryDoc {
14
15
  capabilities?: Record<string, unknown>;
@@ -17,7 +18,7 @@ interface DiscoveryDoc {
17
18
  async function readCap(): Promise<Record<string, unknown> | null> {
18
19
  const res = await driver.get('/.well-known/openwop');
19
20
  const body = res.json as DiscoveryDoc | undefined;
20
- const top = body?.capabilities as Record<string, unknown> | undefined;
21
+ const top = discoveryFamilies(body);
21
22
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["cache"] : undefined;
22
23
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
23
24
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
17
  import { driver } from '../lib/driver.js';
18
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
18
19
 
19
20
  interface DiscoveryDoc {
20
21
  capabilities?: Record<string, unknown>;
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
23
24
  async function readCap(): Promise<Record<string, unknown> | null> {
24
25
  const res = await driver.get('/.well-known/openwop');
25
26
  const body = res.json as DiscoveryDoc | undefined;
26
- const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const top = discoveryFamilies(body);
27
28
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["cache"] : undefined;
28
29
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
30
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * RFC 0073 — root is the normative layout; the top-level `capabilities`
3
+ * wrapper is a v1.x-tolerated mirror. This pins `discoveryFamilies`, the view
4
+ * every reader that was written against the wrapper now goes through (S26,
5
+ * suite 1.135.0): root families win, a wrapper-only family is still visible,
6
+ * and a root-only host loses nothing.
7
+ *
8
+ * Why it exists: forty-four readers consulted the wrapper ONLY, so a host that
9
+ * did what RFC 0073 asks — root families, no wrapper — silently lost their
10
+ * legs, and the second sibling host had to keep its wrapper as a mirror
11
+ * because of it. Server-free, always-on.
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
16
+
17
+ describe('RFC 0073 — discoveryFamilies is root-first with the deprecated wrapper as fallback', () => {
18
+ it('a root-only document is read as-is (the RFC 0073 shape)', () => {
19
+ expect(discoveryFamilies({ protocolVersion: '1.0', kvStorage: { supported: true } })).toEqual({ protocolVersion: '1.0', kvStorage: { supported: true } });
20
+ });
21
+
22
+ it('a wrapper-only family is still visible, and the wrapper key itself is not a family', () => {
23
+ const v = discoveryFamilies({ protocolVersion: '1.0', capabilities: { heartbeat: { supported: true } } });
24
+ expect(v['heartbeat']).toEqual({ supported: true });
25
+ expect('capabilities' in v).toBe(false);
26
+ });
27
+
28
+ it('when both carry a family, the root wins', () => {
29
+ const v = discoveryFamilies({ feedback: { supported: true, root: true }, capabilities: { feedback: { supported: false, root: false } } });
30
+ expect(v['feedback']).toEqual({ supported: true, root: true });
31
+ });
32
+
33
+ it('a non-object document reads as empty', () => {
34
+ expect(discoveryFamilies(null)).toEqual({});
35
+ expect(discoveryFamilies('nope')).toEqual({});
36
+ expect(discoveryFamilies({ capabilities: ['not', 'an', 'object'] })).toEqual({});
37
+ });
38
+ });
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
10
  import { driver } from '../lib/driver.js';
11
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
11
12
  import { behaviorGate } from '../lib/behavior-gate.js';
12
13
 
13
14
  describe('discovery: /.well-known/openwop', () => {
@@ -182,8 +183,7 @@ const AUTH_SCOPED_PROFILE = 'openwop-discovery-auth-scoped';
182
183
 
183
184
  async function readDiscoveryCaps(): Promise<DiscoveryCaps | undefined> {
184
185
  const disco = await driver.get('/.well-known/openwop', { authenticated: false });
185
- return (disco.json as { capabilities?: { discovery?: DiscoveryCaps } }).capabilities
186
- ?.discovery;
186
+ return (discoveryFamilies(disco.json) as { discovery?: DiscoveryCaps }).discovery;
187
187
  }
188
188
 
189
189
  function isAuthScopedAdvertised(disc: DiscoveryCaps | undefined): boolean {
@@ -306,13 +306,10 @@ describe('discovery: auth-scoped is not an authorization oracle', () => {
306
306
  }
307
307
  expect(unauthorized.status).toBe(200);
308
308
 
309
- const primaryCaps = Object.keys(
310
- (primary.json as { capabilities?: Record<string, unknown> })?.capabilities ?? {},
311
- );
312
- const unauthorizedCaps = Object.keys(
313
- (unauthorized.json as { capabilities?: Record<string, unknown> })?.capabilities ??
314
- {},
315
- );
309
+ // Root-first (RFC 0073); the deprecated `capabilities` wrapper is folded in
310
+ // so a root-only host and a wrapper-only host are compared the same way (S26).
311
+ const primaryCaps = Object.keys(discoveryFamilies(primary.json));
312
+ const unauthorizedCaps = Object.keys(discoveryFamilies(unauthorized.json));
316
313
 
317
314
  // Spec annex line 69: "Hosts MUST NOT let scoped discovery become
318
315
  // an authorization oracle. A caller should learn only about
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { describe, it, expect } from 'vitest';
12
12
  import { driver } from '../lib/driver.js';
13
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
13
14
 
14
15
  interface DiscoveryDoc {
15
16
  capabilities?: Record<string, unknown>;
@@ -18,7 +19,7 @@ interface DiscoveryDoc {
18
19
  async function readCap(): Promise<Record<string, unknown> | null> {
19
20
  const res = await driver.get('/.well-known/openwop');
20
21
  const body = res.json as DiscoveryDoc | undefined;
21
- const top = body?.capabilities as Record<string, unknown> | undefined;
22
+ const top = discoveryFamilies(body);
22
23
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
23
24
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
24
25
  }
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { describe, it, expect } from 'vitest';
12
12
  import { driver } from '../lib/driver.js';
13
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
13
14
 
14
15
  interface DiscoveryDoc {
15
16
  capabilities?: Record<string, unknown>;
@@ -18,7 +19,7 @@ interface DiscoveryDoc {
18
19
  async function readCap(): Promise<Record<string, unknown> | null> {
19
20
  const res = await driver.get('/.well-known/openwop');
20
21
  const body = res.json as DiscoveryDoc | undefined;
21
- const top = body?.capabilities as Record<string, unknown> | undefined;
22
+ const top = discoveryFamilies(body);
22
23
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
23
24
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
24
25
  }
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { describe, it, expect } from 'vitest';
20
20
  import { driver } from '../lib/driver.js';
21
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
21
22
 
22
23
  interface DiscoveryDoc {
23
24
  capabilities?: Record<string, unknown>;
@@ -26,7 +27,7 @@ interface DiscoveryDoc {
26
27
  async function readCap(): Promise<Record<string, unknown> | null> {
27
28
  const res = await driver.get('/.well-known/openwop');
28
29
  const body = res.json as DiscoveryDoc | undefined;
29
- const top = body?.capabilities as Record<string, unknown> | undefined;
30
+ const top = discoveryFamilies(body);
30
31
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
31
32
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
32
33
  }
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { describe, it, expect } from 'vitest';
17
17
  import { driver } from '../lib/driver.js';
18
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
18
19
 
19
20
  interface DiscoveryDoc {
20
21
  capabilities?: Record<string, unknown>;
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
23
24
  async function readCap(): Promise<Record<string, unknown> | null> {
24
25
  const res = await driver.get('/.well-known/openwop');
25
26
  const body = res.json as DiscoveryDoc | undefined;
26
- const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const top = discoveryFamilies(body);
27
28
  const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
28
29
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
30
  }
@@ -13,6 +13,7 @@
13
13
 
14
14
  import { describe, it, expect } from 'vitest';
15
15
  import { driver } from '../lib/driver.js';
16
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
16
17
  import { seamAbsent } from '../lib/soft-skip.js';
17
18
  import { mcpServerMount } from '../lib/mcp-mount.js';
18
19
 
@@ -23,7 +24,7 @@ interface DiscoveryDoc {
23
24
  async function readCap(): Promise<Record<string, unknown> | null> {
24
25
  const res = await driver.get('/.well-known/openwop');
25
26
  const body = res.json as DiscoveryDoc | undefined;
26
- const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const top = discoveryFamilies(body);
27
28
  const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
28
29
  const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
29
30
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
@@ -8,6 +8,7 @@
8
8
 
9
9
  import { describe, it, expect } from 'vitest';
10
10
  import { driver } from '../lib/driver.js';
11
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
11
12
  import { seamAbsent } from '../lib/soft-skip.js';
12
13
  import { mcpServerMount } from '../lib/mcp-mount.js';
13
14
 
@@ -18,7 +19,7 @@ interface DiscoveryDoc {
18
19
  async function readCap(): Promise<Record<string, unknown> | null> {
19
20
  const res = await driver.get('/.well-known/openwop');
20
21
  const body = res.json as DiscoveryDoc | undefined;
21
- const top = body?.capabilities as Record<string, unknown> | undefined;
22
+ const top = discoveryFamilies(body);
22
23
  const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
23
24
  const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
24
25
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
@@ -10,6 +10,7 @@
10
10
 
11
11
  import { describe, it, expect } from 'vitest';
12
12
  import { driver } from '../lib/driver.js';
13
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
13
14
  import { seamAbsent } from '../lib/soft-skip.js';
14
15
  import { mcpServerMount } from '../lib/mcp-mount.js';
15
16
 
@@ -20,7 +21,7 @@ interface DiscoveryDoc {
20
21
  async function readCap(): Promise<Record<string, unknown> | null> {
21
22
  const res = await driver.get('/.well-known/openwop');
22
23
  const body = res.json as DiscoveryDoc | undefined;
23
- const top = body?.capabilities as Record<string, unknown> | undefined;
24
+ const top = discoveryFamilies(body);
24
25
  const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
25
26
  const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
26
27
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { describe, it, expect } from 'vitest';
18
18
  import { driver } from '../lib/driver.js';
19
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
19
20
  import { seamAbsent } from '../lib/soft-skip.js';
20
21
  import { mcpServerMount } from '../lib/mcp-mount.js';
21
22
 
@@ -26,7 +27,7 @@ interface DiscoveryDoc {
26
27
  async function readCap(): Promise<Record<string, unknown> | null> {
27
28
  const res = await driver.get('/.well-known/openwop');
28
29
  const body = res.json as DiscoveryDoc | undefined;
29
- const top = body?.capabilities as Record<string, unknown> | undefined;
30
+ const top = discoveryFamilies(body);
30
31
  const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
31
32
  const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
32
33
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { describe, it, expect } from 'vitest';
16
16
  import { driver } from '../lib/driver.js';
17
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
17
18
  import { seamAbsent } from '../lib/soft-skip.js';
18
19
  import { mcpServerMount } from '../lib/mcp-mount.js';
19
20
 
@@ -24,7 +25,7 @@ interface DiscoveryDoc {
24
25
  async function readCap(): Promise<Record<string, unknown> | null> {
25
26
  const res = await driver.get('/.well-known/openwop');
26
27
  const body = res.json as DiscoveryDoc | undefined;
27
- const top = body?.capabilities as Record<string, unknown> | undefined;
28
+ const top = discoveryFamilies(body);
28
29
  const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
29
30
  const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
30
31
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
@@ -12,6 +12,7 @@
12
12
 
13
13
  import { describe, it, expect } from 'vitest';
14
14
  import { driver } from '../lib/driver.js';
15
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
15
16
  import { seamAbsent } from '../lib/soft-skip.js';
16
17
  import { mcpServerMount } from '../lib/mcp-mount.js';
17
18
 
@@ -22,7 +23,7 @@ interface DiscoveryDoc {
22
23
  async function readCap(): Promise<Record<string, unknown> | null> {
23
24
  const res = await driver.get('/.well-known/openwop');
24
25
  const body = res.json as DiscoveryDoc | undefined;
25
- const top = body?.capabilities as Record<string, unknown> | undefined;
26
+ const top = discoveryFamilies(body);
26
27
  const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
27
28
  const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
28
29
  return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
@@ -22,13 +22,12 @@
22
22
  import { describe, it, expect } from 'vitest';
23
23
  import { softSkip } from '../lib/soft-skip.js';
24
24
  import { driver } from '../lib/driver.js';
25
- import { capabilityFamily } from '../lib/discovery-capabilities.js';
25
+ import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
26
26
 
27
27
  async function isMcpClientSupported(): Promise<boolean> {
28
28
  const disco = await driver.get('/.well-known/openwop');
29
- const caps = (disco.json as { capabilities?: { mcpClient?: { supported?: boolean } } })
30
- .capabilities;
31
- return caps?.mcpClient?.supported === true;
29
+ const caps = discoveryFamilies(disco.json) as { mcpClient?: { supported?: boolean } };
30
+ return caps.mcpClient?.supported === true;
32
31
  }
33
32
 
34
33
  describe('mcp-toolcall-redaction: capability advertisement contract', () => {
@@ -32,7 +32,7 @@ import { readFileSync } from 'node:fs';
32
32
  import { join } from 'node:path';
33
33
  import { driver } from '../lib/driver.js';
34
34
  import { SCHEMAS_DIR } from '../lib/paths.js';
35
- import { capabilityFamily } from '../lib/discovery-capabilities.js';
35
+ import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
36
36
 
37
37
  const MEDIA_KINDS = ['media.image', 'media.audio', 'media.file'] as const;
38
38
  const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
@@ -136,10 +136,8 @@ describe.skipIf(HTTP_SKIP)('media-url-inline-cap: advertisement shape (RFC 0055
136
136
  // that emits a media.* envelope into a run (so no media payload appears).
137
137
  const disc = await driver.get('/.well-known/openwop');
138
138
  if (disc.status !== 200) return;
139
- const caps = (disc.json as {
140
- capabilities?: { aiProviders?: { maxInlineMediaBytes?: unknown }; debugBundle?: { supported?: unknown } };
141
- }).capabilities;
142
- if (caps?.aiProviders?.maxInlineMediaBytes === undefined || caps.debugBundle?.supported !== true) {
139
+ const caps = discoveryFamilies(disc.json) as { aiProviders?: { maxInlineMediaBytes?: unknown }; debugBundle?: { supported?: unknown } };
140
+ if (caps.aiProviders?.maxInlineMediaBytes === undefined || caps.debugBundle?.supported !== true) {
143
141
  return; // host doesn't serve media + export debug bundles — contract not exercisable
144
142
  }
145
143
  // Find a recent run and inspect its debug bundle for any media.* event.