@openwop/openwop-conformance 1.1.1 → 1.2.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 (86) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +2 -2
  3. package/coverage.md +26 -14
  4. package/fixtures/conformance-agent-low-confidence.json +7 -4
  5. package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
  6. package/fixtures/conformance-agent-reasoning.json +23 -4
  7. package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
  8. package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
  9. package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
  10. package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
  11. package/fixtures/conformance-dispatch-input-mapping.json +49 -0
  12. package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
  13. package/fixtures/conformance-dispatch-output-mapping.json +49 -0
  14. package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
  15. package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
  16. package/fixtures.md +12 -2
  17. package/package.json +1 -1
  18. package/schemas/README.md +7 -0
  19. package/schemas/agent-ref.schema.json +1 -1
  20. package/schemas/ai-envelope.schema.json +106 -0
  21. package/schemas/capabilities.schema.json +248 -0
  22. package/schemas/core-conformance-mock-agent-config.schema.json +147 -0
  23. package/schemas/dispatch-config.schema.json +26 -0
  24. package/schemas/envelopes/clarification.request.schema.json +43 -0
  25. package/schemas/envelopes/error.schema.json +26 -0
  26. package/schemas/envelopes/schema.request.schema.json +22 -0
  27. package/schemas/envelopes/schema.response.schema.json +22 -0
  28. package/schemas/node-pack-manifest.schema.json +5 -0
  29. package/schemas/pack-lockfile.schema.json +16 -0
  30. package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
  31. package/src/lib/webhook-receiver.ts +137 -0
  32. package/src/lib/workflow-chain-expansion.ts +213 -0
  33. package/src/scenarios/agentPackCatalog.test.ts +216 -0
  34. package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
  35. package/src/scenarios/agentReasoningEvents.test.ts +58 -7
  36. package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
  37. package/src/scenarios/ai-envelope-shape.test.ts +362 -0
  38. package/src/scenarios/aiEnvelope.capBreached.test.ts +173 -0
  39. package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
  40. package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
  41. package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
  42. package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
  43. package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
  44. package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -0
  45. package/src/scenarios/append-ordering.test.ts +44 -0
  46. package/src/scenarios/artifact-auth.test.ts +58 -0
  47. package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
  48. package/src/scenarios/blob-presign-expiry.test.ts +66 -0
  49. package/src/scenarios/blob-roundtrip.test.ts +48 -0
  50. package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
  51. package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
  52. package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
  53. package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
  54. package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
  55. package/src/scenarios/fs-path-traversal.test.ts +124 -0
  56. package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
  57. package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
  58. package/src/scenarios/kv-atomic-increment.test.ts +74 -0
  59. package/src/scenarios/kv-cas.test.ts +75 -0
  60. package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
  61. package/src/scenarios/kv-ttl-expiry.test.ts +47 -0
  62. package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
  63. package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
  64. package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
  65. package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
  66. package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
  67. package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
  68. package/src/scenarios/pause-resume.test.ts +43 -0
  69. package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
  70. package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
  71. package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
  72. package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
  73. package/src/scenarios/spec-corpus-validity.test.ts +17 -1
  74. package/src/scenarios/sql-injection-rejection.test.ts +84 -0
  75. package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
  76. package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
  77. package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
  78. package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
  79. package/src/scenarios/table-cursor-pagination.test.ts +47 -0
  80. package/src/scenarios/table-schema-enforcement.test.ts +47 -0
  81. package/src/scenarios/vector-knn-roundtrip.test.ts +48 -0
  82. package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
  83. package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
  84. package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
  85. package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
  86. package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
@@ -0,0 +1,85 @@
1
+ /**
2
+ * kv-cross-tenant-isolation — RFC 0015 §B + SECURITY/invariants.yaml
3
+ * `kv-cross-tenant-isolation`.
4
+ *
5
+ * Status: ACTIVE (advertisement + behavioral). The behavioral half drives
6
+ * cross-tenant proof through the optional reference-host test seam at
7
+ * `POST /v1/host/sample/test/surface` (env-gated on `OPENWOP_TEST_SEAM_ENABLED=true`).
8
+ * Hosts that don't expose the seam (HTTP 404) soft-skip the behavioral
9
+ * assertions and verify advertisement shape only.
10
+ *
11
+ * Summary: host.kvStorage MUST partition values by tenant. A `set` issued
12
+ * under tenant A MUST NOT be visible to a `get` under tenant B at the
13
+ * same key.
14
+ *
15
+ * @see RFCS/0015-host-kv-storage-capability.md
16
+ * @see SECURITY/invariants.yaml — kv-cross-tenant-isolation
17
+ */
18
+
19
+ import { describe, it, expect } from 'vitest';
20
+ import { driver } from '../lib/driver.js';
21
+
22
+ interface DiscoveryDoc {
23
+ capabilities?: Record<string, unknown>;
24
+ }
25
+
26
+ async function readCap(): Promise<Record<string, unknown> | null> {
27
+ const res = await driver.get('/.well-known/openwop');
28
+ const body = res.json as DiscoveryDoc | undefined;
29
+ const top = body?.capabilities as Record<string, unknown> | undefined;
30
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
31
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
32
+ }
33
+
34
+ async function call(tenantId: string, op: string, args: Record<string, unknown>) {
35
+ return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'kv', op, args });
36
+ }
37
+
38
+ describe('kv-cross-tenant-isolation: advertisement shape (RFC 0015)', () => {
39
+ it('capabilities.kvStorage is either absent or a well-formed object', async () => {
40
+ const cap = await readCap();
41
+ if (cap === null) return;
42
+ expect(
43
+ typeof cap.supported,
44
+ driver.describe(
45
+ 'capabilities.schema.json §kvStorage',
46
+ 'capabilities.kvStorage.supported MUST be a boolean when present',
47
+ ),
48
+ ).toBe('boolean');
49
+ });
50
+ });
51
+
52
+ describe('kv-cross-tenant-isolation: behavioral (RFC 0015 §B)', () => {
53
+ it('set under tenant A → get under tenant B with same key returns found:false', async () => {
54
+ const cap = await readCap();
55
+ if (!cap || cap.supported !== true) return;
56
+ const key = `xtenant-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
57
+ const setRes = await call('tenant-a', 'set', { key, value: 'from-A' });
58
+ if (setRes.status === 404) return; // host doesn't expose test seam
59
+ expect(setRes.status, driver.describe('RFC 0015 §B', 'set MUST succeed')).toBe(200);
60
+
61
+ const getRes = await call('tenant-b', 'get', { key });
62
+ expect(getRes.status).toBe(200);
63
+ const body = getRes.json as { value?: unknown; found?: boolean };
64
+ expect(
65
+ body.found,
66
+ driver.describe(
67
+ 'SECURITY/invariants.yaml kv-cross-tenant-isolation',
68
+ 'tenant B MUST NOT see tenant A value at the same key',
69
+ ),
70
+ ).toBe(false);
71
+ });
72
+
73
+ it('same-tenant set→get round-trips the value', async () => {
74
+ const cap = await readCap();
75
+ if (!cap || cap.supported !== true) return;
76
+ const key = `roundtrip-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
77
+ const setRes = await call('tenant-a', 'set', { key, value: 'rt-A' });
78
+ if (setRes.status === 404) return;
79
+ expect(setRes.status).toBe(200);
80
+ const getRes = await call('tenant-a', 'get', { key });
81
+ const body = getRes.json as { value?: unknown; found?: boolean };
82
+ expect(body.found, 'same-tenant get MUST succeed').toBe(true);
83
+ expect(body.value, 'same-tenant get MUST return the stored value').toBe('rt-A');
84
+ });
85
+ });
@@ -0,0 +1,47 @@
1
+ /**
2
+ * kv-ttl-expiry — RFC 0015 advertisement-shape verification + behavioral placeholders.
3
+ *
4
+ * Status: ACTIVE (advertisement-shape). RFC 0015 promoted to `Active`
5
+ * 2026-05-17. The matching `capabilities.kvStorage` block has landed in
6
+ * `schemas/capabilities.schema.json`. This scenario asserts the advertisement
7
+ * shape against any host that boots the conformance suite, and keeps the
8
+ * deeper behavioral assertions as `it.todo()` until a reference host wires
9
+ * a test seam.
10
+ *
11
+ * Summary: TTL honored with at most a 1-second drift on expiry visibility.
12
+ *
13
+ * @see RFCS/0015-*.md
14
+ */
15
+
16
+ import { describe, it, expect } from 'vitest';
17
+ import { driver } from '../lib/driver.js';
18
+
19
+ interface DiscoveryDoc {
20
+ capabilities?: Record<string, unknown>;
21
+ }
22
+
23
+ async function readCap(): Promise<Record<string, unknown> | null> {
24
+ const res = await driver.get('/.well-known/openwop');
25
+ const body = res.json as DiscoveryDoc | undefined;
26
+ const top = body?.capabilities as Record<string, unknown> | undefined;
27
+ const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["kvStorage"] : undefined;
28
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
+ }
30
+
31
+ describe('kv-ttl-expiry: advertisement shape (RFC 0015)', () => {
32
+ it('capabilities.kvStorage is either absent or a well-formed object', async () => {
33
+ const cap = await readCap();
34
+ if (cap === null) return; // host doesn't advertise — skip
35
+ expect(
36
+ typeof cap.supported,
37
+ driver.describe(
38
+ 'capabilities.schema.json §kvStorage',
39
+ 'capabilities.kvStorage.supported MUST be a boolean when present',
40
+ ),
41
+ ).toBe('boolean');
42
+ });
43
+ });
44
+
45
+ describe('kv-ttl-expiry: behavioral assertions (placeholders — need host test seam)', () => {
46
+ it.todo("set with ttl=2 → get at t+1 returns the value; get at t+3 returns not-found");
47
+ });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * mcp-server-elicitation-bridge — RFC 0020 §A point 3 (bidirectional elicitation).
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). Asserts that when a workflow
5
+ * has a `core.openwop.mcp.handle-elicitation` node, inbound `elicitation/create`
6
+ * is bridged into the workflow's `ctx.suspend({kind: 'clarification', profile:
7
+ * 'openwop-mcp-elicitation'})`. The host returns either a `pending` action
8
+ * (run suspended awaiting input) OR an accept/decline/cancel response (run
9
+ * completed without suspending).
10
+ *
11
+ * @see RFCS/0020-host-mcp-server-composition.md
12
+ */
13
+
14
+ import { describe, it, expect } from 'vitest';
15
+ import { driver } from '../lib/driver.js';
16
+
17
+ interface DiscoveryDoc {
18
+ capabilities?: Record<string, unknown>;
19
+ }
20
+
21
+ async function readCap(): Promise<Record<string, unknown> | null> {
22
+ const res = await driver.get('/.well-known/openwop');
23
+ const body = res.json as DiscoveryDoc | undefined;
24
+ const top = body?.capabilities as Record<string, unknown> | undefined;
25
+ const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
26
+ const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
27
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
28
+ }
29
+
30
+ async function rpc(method: string, params?: Record<string, unknown>) {
31
+ const id = Math.floor(Math.random() * 1e6);
32
+ const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
33
+ if (params !== undefined) req.params = params;
34
+ const res = await driver.post('/v1/host/sample/mcp', req);
35
+ return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
36
+ }
37
+
38
+ async function registerElicitationHandlerWorkflow(): Promise<boolean> {
39
+ const res = await driver.post('/v1/host/sample/workflows', {
40
+ workflowId: `mcp.elicit.${Date.now()}`,
41
+ nodes: [
42
+ { nodeId: 'elicit', typeId: 'core.openwop.mcp.handle-elicitation' },
43
+ ],
44
+ });
45
+ return res.status === 200 || res.status === 201;
46
+ }
47
+
48
+ describe('mcp-server-elicitation-bridge: advertisement shape (RFC 0020)', () => {
49
+ it('elicitationBridge is a boolean when serverMount.supported', async () => {
50
+ const cap = await readCap();
51
+ if (!cap || cap.supported !== true) return;
52
+ if (cap.elicitationBridge === undefined) return;
53
+ expect(
54
+ typeof cap.elicitationBridge,
55
+ driver.describe('RFC 0020 §B', 'mcp.serverMount.elicitationBridge MUST be boolean when present'),
56
+ ).toBe('boolean');
57
+ });
58
+ });
59
+
60
+ describe('mcp-server-elicitation-bridge: behavioral (RFC 0020 §A point 3)', () => {
61
+ it('elicitation/create bridges into a handle-elicitation workflow', async () => {
62
+ const cap = await readCap();
63
+ if (!cap || cap.supported !== true || cap.elicitationBridge !== true) return;
64
+ if (!(await registerElicitationHandlerWorkflow())) return;
65
+
66
+ const r = await rpc('elicitation/create', {
67
+ message: 'What is your name?',
68
+ requestedSchema: {
69
+ type: 'object',
70
+ properties: { name: { type: 'string' } },
71
+ required: ['name'],
72
+ },
73
+ });
74
+ if (r.status === 404) return;
75
+ expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
76
+ const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
77
+ expect(
78
+ dispatched,
79
+ driver.describe(
80
+ 'RFC 0020 §A point 3',
81
+ 'elicitation/create MUST dispatch to handle-elicitation workflow (not return method_not_found)',
82
+ ),
83
+ ).toBe(true);
84
+ if (r.body.result) {
85
+ const result = r.body.result as { action?: string };
86
+ expect(
87
+ ['pending', 'accept', 'decline', 'cancel'].includes(result.action ?? ''),
88
+ 'elicitation response action MUST be one of {pending,accept,decline,cancel}',
89
+ ).toBe(true);
90
+ }
91
+ });
92
+ });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * mcp-server-prompt-roundtrip — RFC 0020 §A (prompts/list + prompts/get).
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral).
5
+ *
6
+ * @see RFCS/0020-host-mcp-server-composition.md
7
+ */
8
+
9
+ import { describe, it, expect } from 'vitest';
10
+ import { driver } from '../lib/driver.js';
11
+
12
+ interface DiscoveryDoc {
13
+ capabilities?: Record<string, unknown>;
14
+ }
15
+
16
+ async function readCap(): Promise<Record<string, unknown> | null> {
17
+ const res = await driver.get('/.well-known/openwop');
18
+ const body = res.json as DiscoveryDoc | undefined;
19
+ const top = body?.capabilities as Record<string, unknown> | undefined;
20
+ const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
21
+ const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
22
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
23
+ }
24
+
25
+ async function rpc(method: string, params?: Record<string, unknown>) {
26
+ const id = Math.floor(Math.random() * 1e6);
27
+ const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
28
+ if (params !== undefined) req.params = params;
29
+ const res = await driver.post('/v1/host/sample/mcp', req);
30
+ return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
31
+ }
32
+
33
+ const PROMPT_NAME = `prompt_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
34
+
35
+ async function registerPromptWorkflow(): Promise<boolean> {
36
+ const res = await driver.post('/v1/host/sample/workflows', {
37
+ workflowId: `mcp.prompt.${Date.now()}`,
38
+ nodes: [
39
+ {
40
+ nodeId: 'expose',
41
+ typeId: 'core.openwop.mcp.expose-prompt',
42
+ config: {
43
+ name: PROMPT_NAME,
44
+ description: 'Conformance prompt',
45
+ arguments: [{ name: 'topic', description: 'subject of the prompt', required: false }],
46
+ },
47
+ },
48
+ ],
49
+ });
50
+ return res.status === 200 || res.status === 201;
51
+ }
52
+
53
+ describe('mcp-server-prompt-roundtrip: advertisement shape (RFC 0020)', () => {
54
+ it('capabilities.mcp.serverMount is either absent or a well-formed object', async () => {
55
+ const cap = await readCap();
56
+ if (cap === null) return;
57
+ expect(typeof cap.supported).toBe('boolean');
58
+ });
59
+ });
60
+
61
+ describe('mcp-server-prompt-roundtrip: behavioral (RFC 0020)', () => {
62
+ it('prompts/list returns the exposed prompt and prompts/get returns messages', async () => {
63
+ const cap = await readCap();
64
+ if (!cap || cap.supported !== true) return;
65
+ if (!(await registerPromptWorkflow())) return;
66
+
67
+ const list = await rpc('prompts/list');
68
+ if (list.status === 404) return;
69
+ const prompts = (list.body.result as { prompts?: Array<{ name: string }> } | undefined)?.prompts ?? [];
70
+ expect(
71
+ prompts.find((p) => p.name === PROMPT_NAME),
72
+ driver.describe('RFC 0020 §A', 'prompts/list MUST include exposed prompts'),
73
+ ).toBeDefined();
74
+
75
+ const get = await rpc('prompts/get', { name: PROMPT_NAME, arguments: { topic: 'openwop' } });
76
+ expect(get.status).toBe(200);
77
+ const messages = (get.body.result as { messages?: Array<{ role: string }> } | undefined)?.messages;
78
+ expect(Array.isArray(messages), 'prompts/get MUST return messages[]').toBe(true);
79
+ });
80
+ });
@@ -0,0 +1,82 @@
1
+ /**
2
+ * mcp-server-resource-roundtrip — RFC 0020 §A (resources/list + resources/read).
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). Registers a workflow with
5
+ * `core.openwop.mcp.expose-resource`, then asserts the resource appears
6
+ * in `resources/list` and yields bound content from `resources/read`.
7
+ *
8
+ * @see RFCS/0020-host-mcp-server-composition.md
9
+ */
10
+
11
+ import { describe, it, expect } from 'vitest';
12
+ import { driver } from '../lib/driver.js';
13
+
14
+ interface DiscoveryDoc {
15
+ capabilities?: Record<string, unknown>;
16
+ }
17
+
18
+ async function readCap(): Promise<Record<string, unknown> | null> {
19
+ const res = await driver.get('/.well-known/openwop');
20
+ const body = res.json as DiscoveryDoc | undefined;
21
+ const top = body?.capabilities as Record<string, unknown> | undefined;
22
+ const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
23
+ const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
24
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
25
+ }
26
+
27
+ async function rpc(method: string, params?: Record<string, unknown>) {
28
+ const id = Math.floor(Math.random() * 1e6);
29
+ const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
30
+ if (params !== undefined) req.params = params;
31
+ const res = await driver.post('/v1/host/sample/mcp', req);
32
+ return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
33
+ }
34
+
35
+ const RESOURCE_URI = `mcp://test/${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
36
+
37
+ async function registerResourceWorkflow(): Promise<boolean> {
38
+ const res = await driver.post('/v1/host/sample/workflows', {
39
+ workflowId: `mcp.resource.${Date.now()}`,
40
+ nodes: [
41
+ {
42
+ nodeId: 'expose',
43
+ typeId: 'core.openwop.mcp.expose-resource',
44
+ config: {
45
+ uri: RESOURCE_URI,
46
+ name: 'conformance-test-resource',
47
+ mimeType: 'text/plain',
48
+ },
49
+ },
50
+ ],
51
+ });
52
+ return res.status === 200 || res.status === 201;
53
+ }
54
+
55
+ describe('mcp-server-resource-roundtrip: advertisement shape (RFC 0020)', () => {
56
+ it('capabilities.mcp.serverMount is either absent or a well-formed object', async () => {
57
+ const cap = await readCap();
58
+ if (cap === null) return;
59
+ expect(typeof cap.supported, 'mcp.serverMount.supported MUST be boolean').toBe('boolean');
60
+ });
61
+ });
62
+
63
+ describe('mcp-server-resource-roundtrip: behavioral (RFC 0020)', () => {
64
+ it('resources/list returns the exposed resource and resources/read returns content', async () => {
65
+ const cap = await readCap();
66
+ if (!cap || cap.supported !== true) return;
67
+ if (!(await registerResourceWorkflow())) return;
68
+
69
+ const list = await rpc('resources/list');
70
+ if (list.status === 404) return;
71
+ const resources = (list.body.result as { resources?: Array<{ uri: string }> } | undefined)?.resources ?? [];
72
+ expect(
73
+ resources.find((r) => r.uri === RESOURCE_URI),
74
+ driver.describe('RFC 0020 §A', 'resources/list MUST include exposed resources'),
75
+ ).toBeDefined();
76
+
77
+ const read = await rpc('resources/read', { uri: RESOURCE_URI });
78
+ expect(read.status).toBe(200);
79
+ const contents = (read.body.result as { contents?: Array<{ uri: string }> } | undefined)?.contents;
80
+ expect(Array.isArray(contents), 'resources/read MUST return contents[]').toBe(true);
81
+ });
82
+ });
@@ -0,0 +1,84 @@
1
+ /**
2
+ * mcp-server-sampling-bridge — RFC 0020 §A point 3 (bidirectional sampling).
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). Asserts that when a workflow
5
+ * has a `core.openwop.mcp.handle-sampling` node, inbound `sampling/createMessage`
6
+ * is bridged into the workflow's `ctx.callAI` and returns a sampling result.
7
+ * Gated on `capabilities.mcp.serverMount.samplingBridge: true`.
8
+ *
9
+ * Acceptance test: dispatch produces either a sampling response (when AI
10
+ * keys are provisioned) OR a clean error envelope (proves bridge dispatched
11
+ * but BYOK is absent). Either outcome proves the bridge wired up correctly;
12
+ * a method_not_found (-32601) means the bridge did NOT dispatch.
13
+ *
14
+ * @see RFCS/0020-host-mcp-server-composition.md
15
+ */
16
+
17
+ import { describe, it, expect } from 'vitest';
18
+ import { driver } from '../lib/driver.js';
19
+
20
+ interface DiscoveryDoc {
21
+ capabilities?: Record<string, unknown>;
22
+ }
23
+
24
+ async function readCap(): Promise<Record<string, unknown> | null> {
25
+ const res = await driver.get('/.well-known/openwop');
26
+ const body = res.json as DiscoveryDoc | undefined;
27
+ const top = body?.capabilities as Record<string, unknown> | undefined;
28
+ const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
29
+ const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
30
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
31
+ }
32
+
33
+ async function rpc(method: string, params?: Record<string, unknown>) {
34
+ const id = Math.floor(Math.random() * 1e6);
35
+ const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
36
+ if (params !== undefined) req.params = params;
37
+ const res = await driver.post('/v1/host/sample/mcp', req);
38
+ return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
39
+ }
40
+
41
+ async function registerSamplingHandlerWorkflow(): Promise<boolean> {
42
+ const res = await driver.post('/v1/host/sample/workflows', {
43
+ workflowId: `mcp.sampling.${Date.now()}`,
44
+ nodes: [
45
+ { nodeId: 'sample', typeId: 'core.openwop.mcp.handle-sampling' },
46
+ ],
47
+ });
48
+ return res.status === 200 || res.status === 201;
49
+ }
50
+
51
+ describe('mcp-server-sampling-bridge: advertisement shape (RFC 0020)', () => {
52
+ it('samplingBridge is a boolean when serverMount.supported', async () => {
53
+ const cap = await readCap();
54
+ if (!cap || cap.supported !== true) return;
55
+ if (cap.samplingBridge === undefined) return;
56
+ expect(
57
+ typeof cap.samplingBridge,
58
+ driver.describe('RFC 0020 §B', 'mcp.serverMount.samplingBridge MUST be boolean when present'),
59
+ ).toBe('boolean');
60
+ });
61
+ });
62
+
63
+ describe('mcp-server-sampling-bridge: behavioral (RFC 0020 §A point 3)', () => {
64
+ it('sampling/createMessage bridges into a handle-sampling workflow', async () => {
65
+ const cap = await readCap();
66
+ if (!cap || cap.supported !== true || cap.samplingBridge !== true) return;
67
+ if (!(await registerSamplingHandlerWorkflow())) return;
68
+
69
+ const r = await rpc('sampling/createMessage', {
70
+ messages: [{ role: 'user', content: { type: 'text', text: 'ping' } }],
71
+ maxTokens: 16,
72
+ });
73
+ if (r.status === 404) return;
74
+ expect(r.status, 'JSON-RPC envelope MUST 200').toBe(200);
75
+ const dispatched = !!r.body.result || (!!r.body.error && r.body.error.code !== -32601);
76
+ expect(
77
+ dispatched,
78
+ driver.describe(
79
+ 'RFC 0020 §A point 3',
80
+ 'sampling/createMessage MUST dispatch to handle-sampling workflow (not return method_not_found)',
81
+ ),
82
+ ).toBe(true);
83
+ });
84
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * mcp-server-tool-roundtrip — RFC 0020 §A points 1-2 (workflow → MCP tool).
3
+ *
4
+ * Status: ACTIVE (advertisement + behavioral). The behavioral half registers
5
+ * a workflow with `core.openwop.mcp.expose-tool` via the host's workflow
6
+ * registration endpoint, then issues JSON-RPC `tools/list` + `tools/call`
7
+ * against the reference-host MCP server mount at `/v1/host/sample/mcp`
8
+ * (env-gated on `OPENWOP_MCP_SERVER_ENABLED=true`). Hosts that don't expose
9
+ * the seam (HTTP 404) soft-skip the behavioral assertions and verify
10
+ * advertisement shape only.
11
+ *
12
+ * @see RFCS/0020-host-mcp-server-composition.md
13
+ */
14
+
15
+ import { describe, it, expect } from 'vitest';
16
+ import { driver } from '../lib/driver.js';
17
+
18
+ interface DiscoveryDoc {
19
+ capabilities?: Record<string, unknown>;
20
+ }
21
+
22
+ async function readCap(): Promise<Record<string, unknown> | null> {
23
+ const res = await driver.get('/.well-known/openwop');
24
+ const body = res.json as DiscoveryDoc | undefined;
25
+ const top = body?.capabilities as Record<string, unknown> | undefined;
26
+ const cur = (top && typeof top === 'object') ? (top as Record<string, unknown>)["mcp"] : undefined;
27
+ const final = (cur && typeof cur === 'object') ? (cur as Record<string, unknown>)["serverMount"] : undefined;
28
+ return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
29
+ }
30
+
31
+ async function rpc(method: string, params?: Record<string, unknown>): Promise<{ status: number; body: { result?: unknown; error?: { code: number; message: string } } }> {
32
+ const id = Math.floor(Math.random() * 1e6);
33
+ const req: Record<string, unknown> = { jsonrpc: '2.0', id, method };
34
+ if (params !== undefined) req.params = params;
35
+ const res = await driver.post('/v1/host/sample/mcp', req);
36
+ return { status: res.status, body: res.json as { result?: unknown; error?: { code: number; message: string } } };
37
+ }
38
+
39
+ const TEST_TOOL_NAME = `tool_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
40
+
41
+ async function registerToolWorkflow(): Promise<boolean> {
42
+ const res = await driver.post('/v1/host/sample/workflows', {
43
+ workflowId: `mcp.scenario.${TEST_TOOL_NAME}`,
44
+ nodes: [
45
+ {
46
+ nodeId: 'expose',
47
+ typeId: 'core.openwop.mcp.expose-tool',
48
+ config: {
49
+ name: TEST_TOOL_NAME,
50
+ description: 'Conformance-test tool',
51
+ inputSchema: {
52
+ type: 'object',
53
+ properties: { text: { type: 'string' } },
54
+ required: ['text'],
55
+ additionalProperties: false,
56
+ },
57
+ },
58
+ },
59
+ ],
60
+ });
61
+ return res.status === 200 || res.status === 201;
62
+ }
63
+
64
+ describe('mcp-server-tool-roundtrip: advertisement shape (RFC 0020)', () => {
65
+ it('capabilities.mcp.serverMount is either absent or a well-formed object', async () => {
66
+ const cap = await readCap();
67
+ if (cap === null) return;
68
+ expect(
69
+ typeof cap.supported,
70
+ driver.describe(
71
+ 'capabilities.schema.json §mcp.serverMount',
72
+ 'capabilities.mcp.serverMount.supported MUST be a boolean when present',
73
+ ),
74
+ ).toBe('boolean');
75
+ });
76
+ });
77
+
78
+ describe('mcp-server-tool-roundtrip: behavioral (RFC 0020 §A points 1-2)', () => {
79
+ it('tools/list returns the exposed workflow + tools/call returns a CallToolResult', async () => {
80
+ const cap = await readCap();
81
+ if (!cap || cap.supported !== true) return;
82
+ const registered = await registerToolWorkflow();
83
+ if (!registered) return; // host doesn't expose workflow registration
84
+
85
+ const list = await rpc('tools/list');
86
+ if (list.status === 404) return; // host doesn't expose the seam
87
+ expect(list.status, 'tools/list MUST 200').toBe(200);
88
+ const tools = (list.body.result as { tools?: Array<{ name: string }> } | undefined)?.tools ?? [];
89
+ const found = tools.find((t) => t.name === TEST_TOOL_NAME);
90
+ expect(
91
+ found,
92
+ driver.describe(
93
+ 'RFC 0020 §A point 2',
94
+ 'tools/list MUST include workflows exposed via core.openwop.mcp.expose-tool',
95
+ ),
96
+ ).toBeDefined();
97
+
98
+ const call = await rpc('tools/call', { name: TEST_TOOL_NAME, arguments: { text: 'hello' } });
99
+ expect(call.status, 'tools/call MUST 200').toBe(200);
100
+ const result = call.body.result as { content?: Array<{ type: string }>; isError?: boolean } | undefined;
101
+ expect(
102
+ Array.isArray(result?.content),
103
+ driver.describe('RFC 0020 §C', 'CallToolResult MUST contain content[]'),
104
+ ).toBe(true);
105
+ expect(typeof result?.isError, 'CallToolResult.isError MUST be boolean').toBe('boolean');
106
+ });
107
+ });