@openwop/openwop-conformance 1.135.0 → 1.135.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.135.0",
3
+ "version": "1.135.2",
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.135.0",
4
- "corpusCommit": "1b8327c673b8a11c004c17b848ba0a8d2128c2b8"
3
+ "suiteVersion": "1.135.2",
4
+ "corpusCommit": "101bf981f0431710e81a3e7ac108c5b6b49cddd9"
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'`.",
@@ -28,6 +28,7 @@ import { driver } from '../lib/driver.js';
28
28
  import { behaviorGate } from '../lib/behavior-gate.js';
29
29
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
30
30
  import { getA2AFakePeer } from '../lib/a2a-fake-peer.js';
31
+ import { readErrorCode } from '../lib/error-envelope.js';
31
32
 
32
33
  /**
33
34
  * Callback-shaped: the host issues A2A calls to the suite's fake peer, which records the negotiated version header.
@@ -153,9 +154,10 @@ describe('RFC 0152 §B — A2A version negotiation', () => {
153
154
  });
154
155
  if (drive.status === 404 || drive.status === 403) return;
155
156
  expect(drive.status >= 400, driver.describe('RFCS/0152 §B', 'an unsupported version MUST fail')).toBe(true);
156
- const err = (drive.json as { error?: { code?: string; retriable?: boolean } }).error;
157
+ // S28: canonical envelope is FLAT (`error: "<code>"`, error-envelope.schema.json);
158
+ // the nested `error.code` is tolerated by the lib only through the deprecation window.
157
159
  expect(
158
- err?.code,
160
+ readErrorCode(drive.json),
159
161
  driver.describe(
160
162
  'RFCS/0152 §B',
161
163
  'the upstream version error MUST be projected through the canonical OpenWOP interop error ' +
@@ -183,8 +183,7 @@ const AUTH_SCOPED_PROFILE = 'openwop-discovery-auth-scoped';
183
183
 
184
184
  async function readDiscoveryCaps(): Promise<DiscoveryCaps | undefined> {
185
185
  const disco = await driver.get('/.well-known/openwop', { authenticated: false });
186
- return (disco.json as { capabilities?: { discovery?: DiscoveryCaps } }).capabilities
187
- ?.discovery;
186
+ return (discoveryFamilies(disco.json) as { discovery?: DiscoveryCaps }).discovery;
188
187
  }
189
188
 
190
189
  function isAuthScopedAdvertised(disc: DiscoveryCaps | undefined): boolean {
@@ -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', () => {
@@ -26,6 +26,7 @@
26
26
 
27
27
  import { describe, it, expect } from 'vitest';
28
28
  import { driver } from '../lib/driver.js';
29
+ import { readErrorCode } from '../lib/error-envelope.js';
29
30
  import { seamAbsent } from '../lib/soft-skip.js';
30
31
  import { behaviorGate } from '../lib/behavior-gate.js';
31
32
  import { capabilityFamily } from '../lib/discovery-capabilities.js';
@@ -154,8 +155,10 @@ describe('RFC 0153 §A/§B — MCP revision negotiation', () => {
154
155
  drive.status >= 400,
155
156
  driver.describe('RFCS/0153 §B', 'an unsupported revision MUST fail rather than silently proceed'),
156
157
  ).toBe(true);
158
+ // S28: canonical envelope is FLAT (`error: "<code>"`, error-envelope.schema.json);
159
+ // the nested `error.code` is tolerated by the lib only through the deprecation window.
157
160
  expect(
158
- (drive.json as { error?: { code?: string } }).error?.code,
161
+ readErrorCode(drive.json),
159
162
  driver.describe(
160
163
  'RFCS/0153 §B',
161
164
  'the upstream error MUST be projected through the canonical OpenWOP interop envelope — a ' +
@@ -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.
@@ -21,6 +21,7 @@
21
21
  import { describe, it, expect } from 'vitest';
22
22
  import { softSkip } from '../lib/soft-skip.js';
23
23
  import { driver } from '../lib/driver.js';
24
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
24
25
  import { pollUntilTerminal } from '../lib/polling.js';
25
26
  import { isFixtureAdvertised } from '../lib/fixtures.js';
26
27
  import { getCollector } from '../lib/otel-collector.js';
@@ -43,10 +44,8 @@ interface MetricsCaps {
43
44
  async function metricsAdvertised(): Promise<MetricsCaps | null> {
44
45
  try {
45
46
  const disco = await driver.get('/.well-known/openwop');
46
- const caps = (disco.json as {
47
- capabilities?: { observability?: { metrics?: MetricsCaps } };
48
- }).capabilities;
49
- return caps?.observability?.metrics ?? null;
47
+ const caps = discoveryFamilies(disco.json) as { observability?: { metrics?: MetricsCaps } };
48
+ return caps.observability?.metrics ?? null;
50
49
  } catch {
51
50
  return null;
52
51
  }
@@ -20,6 +20,7 @@
20
20
 
21
21
  import { describe, it, expect, beforeAll } from 'vitest';
22
22
  import { driver } from '../lib/driver.js';
23
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
23
24
 
24
25
  /** RFC 0031 §C — spec-reserved capability identifiers. */
25
26
  const RESERVED_IDENTIFIERS: ReadonlySet<string> = new Set([
@@ -58,7 +59,7 @@ beforeAll(async () => {
58
59
  SKIP_REASON = 'discovery doc unreachable';
59
60
  return;
60
61
  }
61
- const caps = (disco.json as DiscoveryDoc).capabilities ?? {};
62
+ const caps = discoveryFamilies(disco.json) as NonNullable<DiscoveryDoc['capabilities']>;
62
63
  if (caps.modelCapabilities?.supported !== true) {
63
64
  SKIP_REASON = 'host does not advertise capabilities.modelCapabilities.supported: true';
64
65
  return;
@@ -28,6 +28,7 @@
28
28
  import { describe, it, expect } from 'vitest';
29
29
  import { softSkip } from '../lib/soft-skip.js';
30
30
  import { driver } from '../lib/driver.js';
31
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
31
32
  import { pollUntilTerminal } from '../lib/polling.js';
32
33
  import { isFixtureAdvertised } from '../lib/fixtures.js';
33
34
  import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
@@ -45,12 +46,8 @@ const FIXTURE = 'conformance-noop';
45
46
  async function advertisesGrpcExport(): Promise<boolean> {
46
47
  try {
47
48
  const disco = await driver.get('/.well-known/openwop');
48
- const caps = (disco.json as {
49
- capabilities?: {
50
- observability?: { otel?: { exportProtocols?: unknown } };
51
- };
52
- }).capabilities;
53
- const protocols = caps?.observability?.otel?.exportProtocols;
49
+ const caps = discoveryFamilies(disco.json) as { observability?: { otel?: { exportProtocols?: unknown } } };
50
+ const protocols = caps.observability?.otel?.exportProtocols;
54
51
  return Array.isArray(protocols) && protocols.includes('grpc');
55
52
  } catch {
56
53
  return false;
@@ -26,6 +26,7 @@
26
26
  import { describe, it, expect } from 'vitest';
27
27
  import { softSkip } from '../lib/soft-skip.js';
28
28
  import { driver } from '../lib/driver.js';
29
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
29
30
  import { pollUntilTerminal } from '../lib/polling.js';
30
31
  import { isFixtureAdvertised } from '../lib/fixtures.js';
31
32
  import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
@@ -43,7 +44,7 @@ const FIXTURE = 'conformance-noop';
43
44
  async function isObservabilityAdvertised(): Promise<boolean> {
44
45
  try {
45
46
  const disco = await driver.get('/.well-known/openwop');
46
- const caps = (disco.json as { capabilities?: { observability?: unknown } }).capabilities ?? {};
47
+ const caps = discoveryFamilies(disco.json) as { observability?: unknown };
47
48
  return caps.observability !== undefined;
48
49
  } catch {
49
50
  return false;
@@ -36,6 +36,7 @@
36
36
  import { describe, it, expect } from 'vitest';
37
37
  import { softSkip } from '../lib/soft-skip.js';
38
38
  import { driver } from '../lib/driver.js';
39
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
39
40
  import { pollUntilTerminal } from '../lib/polling.js';
40
41
  import { isFixtureAdvertised } from '../lib/fixtures.js';
41
42
  import { isScenarioOptedOut } from '../lib/env.js';
@@ -70,7 +71,7 @@ function makeTraceparent(): { header: string; traceId: string } {
70
71
  async function isObservabilityAdvertised(): Promise<boolean> {
71
72
  try {
72
73
  const disco = await driver.get('/.well-known/openwop');
73
- const caps = (disco.json as { capabilities?: { observability?: unknown } }).capabilities ?? {};
74
+ const caps = discoveryFamilies(disco.json) as { observability?: unknown };
74
75
  return caps.observability !== undefined;
75
76
  } catch {
76
77
  return false;
@@ -20,6 +20,7 @@
20
20
  import { describe, it, expect } from 'vitest';
21
21
  import { softSkip } from '../lib/soft-skip.js';
22
22
  import { driver } from '../lib/driver.js';
23
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
23
24
  import { pollUntilTerminal } from '../lib/polling.js';
24
25
  import { isFixtureAdvertised } from '../lib/fixtures.js';
25
26
  import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
@@ -45,7 +46,7 @@ function makeTraceparent(): { header: string; traceId: string } {
45
46
  async function isObservabilityAdvertised(): Promise<boolean> {
46
47
  try {
47
48
  const disco = await driver.get('/.well-known/openwop');
48
- const caps = (disco.json as { capabilities?: { observability?: unknown } }).capabilities ?? {};
49
+ const caps = discoveryFamilies(disco.json) as { observability?: unknown };
49
50
  return caps.observability !== undefined;
50
51
  } catch {
51
52
  return false;
@@ -35,6 +35,7 @@ import { driver } from '../lib/driver.js';
35
35
  import { behaviorGate } from '../lib/behavior-gate.js';
36
36
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
37
37
  import { SCHEMAS_DIR } from '../lib/paths.js';
38
+ import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
38
39
 
39
40
  const GATE = 'openwop-self-hosted-runner';
40
41
 
@@ -51,23 +52,14 @@ function readSchema(name: string): JsonSchema {
51
52
  return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as JsonSchema;
52
53
  }
53
54
 
54
- /** Read the canonical error code from a response body (tolerant of shapes). */
55
- function errCode(json: unknown): string | undefined {
56
- const j = json as { error?: unknown; code?: unknown };
57
- if (typeof j?.code === 'string') return j.code;
58
- if (typeof j?.error === 'string') return j.error;
59
- const e = j?.error as { code?: unknown } | undefined;
60
- if (e && typeof e.code === 'string') return e.code;
61
- return undefined;
62
- }
63
-
64
- /** Read a boolean `retriable` flag from either `{retriable}` or `{error:{retriable}}`. */
65
- function retriable(json: unknown): boolean | undefined {
66
- const j = json as { retriable?: unknown; error?: { retriable?: unknown } };
67
- if (typeof j?.retriable === 'boolean') return j.retriable;
68
- if (j?.error && typeof j.error.retriable === 'boolean') return j.error.retriable;
69
- return undefined;
70
- }
55
+ // S28 (2026-08-17): the HTTP error envelope is READ through lib/error-envelope.ts
56
+ // flat `{ error: "<code>", message, details.retriable }` per error-envelope.schema.json
57
+ // (S22), with the pre-S22 nested `{ error: { code, retriable } }` tolerated only through
58
+ // the deprecation window. The local readers this replaced accepted top-level `retriable`
59
+ // or nested `error.retriable` and never `details.retriable`, so a schema-valid host could
60
+ // not pass the retriable-flag leg at all (openwop-app H27 → S28).
61
+ const errCode = readErrorCode;
62
+ const retriable = readRetriable;
71
63
 
72
64
  interface SelfHostedRunner {
73
65
  supported?: boolean;
@@ -137,6 +137,24 @@ describe('RFC 0153 §A — MCP versioned discovery', () => {
137
137
  ).toBe(true);
138
138
  });
139
139
 
140
+ it('the v1.0 baseline `serverUrls` (required by mcp-discoverability when supported:true) validates — path or absolute URL, non-empty, no duplicates', () => {
141
+ // S27 (2026-08-17): the schema was `additionalProperties: false` and did not
142
+ // list `serverUrls`, while mcp-discoverability.test.ts REQUIRED it — a host
143
+ // could not satisfy the scenario and the schema at once. The second sibling
144
+ // host measured the contradiction on its own advert.
145
+ expect(
146
+ validate({ supported: true, serverUrls: ['/v1/host/openwop-app/mcp'], protocolVersions: ['2026-07-28'] }),
147
+ JSON.stringify(validate.errors),
148
+ ).toBe(true);
149
+ expect(
150
+ validate({ supported: true, serverUrls: ['https://example.test/canvasApi/v1/mcp'], protocolVersions: ['2024-11-05'], preferredVersion: '2024-11-05' }),
151
+ JSON.stringify(validate.errors),
152
+ ).toBe(true);
153
+ expect(validate({ supported: true, serverUrls: [] }), 'empty serverUrls is the bare `supported: true` problem').toBe(false);
154
+ expect(validate({ supported: true, serverUrls: ['/a', '/a'] }), 'duplicates').toBe(false);
155
+ expect(validate({ supported: true, serverUrls: [''] }), 'an empty string is not a mount').toBe(false);
156
+ });
157
+
140
158
  it('versions use MCP date form exactly', () => {
141
159
  // MCP revisions ARE dates. `latest` or a non-padded month would let two
142
160
  // hosts disagree about which revision they share while both validate.
@@ -22,13 +22,13 @@
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
  import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
26
27
 
27
28
  async function isWebhookSupported(): Promise<boolean> {
28
29
  const disco = await driver.get('/.well-known/openwop');
29
- const caps = (disco.json as { capabilities?: { webhooks?: { supported?: boolean } } })
30
- .capabilities;
31
- return caps?.webhooks?.supported === true;
30
+ const caps = discoveryFamilies(disco.json) as { webhooks?: { supported?: boolean } };
31
+ return caps.webhooks?.supported === true;
32
32
  }
33
33
 
34
34
  describe('webhook-negative: SSRF guard rejects private destinations', () => {
@@ -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 DiscoveryWebhooks {
20
21
  webhooks?: {
@@ -26,7 +27,7 @@ interface DiscoveryWebhooks {
26
27
  describe('webhook-sig-algorithm: host advertises supported algorithm set', () => {
27
28
  it('discovery surfaces a webhooks.signatureAlgorithms array including "v1"', async () => {
28
29
  const disco = await driver.get('/.well-known/openwop');
29
- const caps = (disco.json as { capabilities?: DiscoveryWebhooks }).capabilities ?? {};
30
+ const caps = discoveryFamilies(disco.json) as DiscoveryWebhooks;
30
31
  const webhooks = caps.webhooks;
31
32
 
32
33
  if (!webhooks?.supported) {
@@ -28,6 +28,7 @@ import { softSkip } from '../lib/soft-skip.js';
28
28
  import { createHmac } from 'node:crypto';
29
29
  import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http';
30
30
  import { driver } from '../lib/driver.js';
31
+ import { discoveryFamilies } from '../lib/discovery-capabilities.js';
31
32
  import { pollUntilTerminal } from '../lib/polling.js';
32
33
  import { isFixtureAdvertised } from '../lib/fixtures.js';
33
34
  import { discoverOwnedTenant } from '../lib/webhook-receiver.js';
@@ -70,8 +71,8 @@ afterEach(async () => {
70
71
 
71
72
  async function isWebhookSupported(): Promise<boolean> {
72
73
  const disco = await driver.get('/.well-known/openwop');
73
- const caps = (disco.json as { capabilities?: { webhooks?: { supported?: boolean } } }).capabilities;
74
- return caps?.webhooks?.supported === true;
74
+ const caps = discoveryFamilies(disco.json) as { webhooks?: { supported?: boolean } };
75
+ return caps.webhooks?.supported === true;
75
76
  }
76
77
 
77
78
  describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
@@ -46,7 +46,7 @@ import { loadEnv } from '../lib/env.js';
46
46
  import { behaviorGate } from '../lib/behavior-gate.js';
47
47
  import { FIXTURES_DIR } from '../lib/paths.js';
48
48
  import { expandChain, expandChainWithCompensation, type WorkflowChain, type WorkflowChainWithCompensation } from '../lib/workflow-chain-expansion.js';
49
- import { capabilityFamily } from '../lib/discovery-capabilities.js';
49
+ import { capabilityFamily, discoveryFamilies } from '../lib/discovery-capabilities.js';
50
50
  import { softSkip } from '../lib/soft-skip.js';
51
51
  import { readErrorCode } from '../lib/error-envelope.js';
52
52
 
@@ -84,9 +84,7 @@ interface ChainCaps {
84
84
 
85
85
  async function isExpansionAdvertised(): Promise<boolean> {
86
86
  const disco = await driver.get('/.well-known/openwop');
87
- const caps =
88
- (disco.json as { capabilities?: { workflowChainPacks?: ChainCaps } }).capabilities
89
- ?.workflowChainPacks ?? {};
87
+ const caps = (discoveryFamilies(disco.json) as { workflowChainPacks?: ChainCaps }).workflowChainPacks ?? {};
90
88
  return caps.hostExpansionSeam === true;
91
89
  }
92
90
 
@@ -105,8 +103,7 @@ describe('workflow-chain-host-expansion: live host wraps expansion algorithm cor
105
103
  if (!behaviorGate(PROFILE, await isExpansionAdvertised())) return;
106
104
 
107
105
  const disco = await driver.get('/.well-known/openwop');
108
- const caps = (disco.json as { capabilities?: { workflowChainPacks?: ChainCaps } }).capabilities
109
- ?.workflowChainPacks;
106
+ const caps = (discoveryFamilies(disco.json) as { workflowChainPacks?: ChainCaps }).workflowChainPacks;
110
107
  expect(
111
108
  caps,
112
109
  driver.describe(