@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.
- package/CHANGELOG.md +25 -0
- package/README.md +2 -2
- package/coverage.md +26 -14
- package/fixtures/conformance-agent-low-confidence.json +7 -4
- package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
- package/fixtures/conformance-agent-reasoning.json +23 -4
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
- package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
- package/fixtures/conformance-dispatch-input-mapping.json +49 -0
- package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
- package/fixtures/conformance-dispatch-output-mapping.json +49 -0
- package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
- package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
- package/fixtures.md +12 -2
- package/package.json +1 -1
- package/schemas/README.md +7 -0
- package/schemas/agent-ref.schema.json +1 -1
- package/schemas/ai-envelope.schema.json +106 -0
- package/schemas/capabilities.schema.json +248 -0
- package/schemas/core-conformance-mock-agent-config.schema.json +147 -0
- package/schemas/dispatch-config.schema.json +26 -0
- package/schemas/envelopes/clarification.request.schema.json +43 -0
- package/schemas/envelopes/error.schema.json +26 -0
- package/schemas/envelopes/schema.request.schema.json +22 -0
- package/schemas/envelopes/schema.response.schema.json +22 -0
- package/schemas/node-pack-manifest.schema.json +5 -0
- package/schemas/pack-lockfile.schema.json +16 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
- package/src/lib/webhook-receiver.ts +137 -0
- package/src/lib/workflow-chain-expansion.ts +213 -0
- package/src/scenarios/agentPackCatalog.test.ts +216 -0
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
- package/src/scenarios/agentReasoningEvents.test.ts +58 -7
- package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
- package/src/scenarios/ai-envelope-shape.test.ts +362 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +173 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
- package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -0
- package/src/scenarios/append-ordering.test.ts +44 -0
- package/src/scenarios/artifact-auth.test.ts +58 -0
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/blob-presign-expiry.test.ts +66 -0
- package/src/scenarios/blob-roundtrip.test.ts +48 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
- package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
- package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
- package/src/scenarios/fs-path-traversal.test.ts +124 -0
- package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
- package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
- package/src/scenarios/kv-atomic-increment.test.ts +74 -0
- package/src/scenarios/kv-cas.test.ts +75 -0
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
- package/src/scenarios/kv-ttl-expiry.test.ts +47 -0
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
- package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
- package/src/scenarios/pause-resume.test.ts +43 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
- package/src/scenarios/spec-corpus-validity.test.ts +17 -1
- package/src/scenarios/sql-injection-rejection.test.ts +84 -0
- package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
- package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
- package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
- package/src/scenarios/table-cursor-pagination.test.ts +47 -0
- package/src/scenarios/table-schema-enforcement.test.ts +47 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +48 -0
- package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
- package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0022 §B — `core.subWorkflow` `inputMapping` seeding (HVMAP-2).
|
|
3
|
+
* Normative reference: RFCS/0022-dispatch-input-output-mapping.md §B
|
|
4
|
+
* + spec/v1/node-packs.md §"`core.subWorkflow` contract" (post-RFC-0022).
|
|
5
|
+
*
|
|
6
|
+
* Verifies that when a `core.subWorkflow` config carries `inputMapping`,
|
|
7
|
+
* the host seeds the child run's initial variable bag with
|
|
8
|
+
* `parentVariables[parentKey]` projections AFTER the child's
|
|
9
|
+
* `variables[].defaultValue` fold (so `inputMapping` overrides matching
|
|
10
|
+
* defaults). Verified end-to-end against the Postgres reference host on
|
|
11
|
+
* 2026-05-18 alongside the RFC's `Active` promotion.
|
|
12
|
+
*
|
|
13
|
+
* Capability-gated: skips when host doesn't advertise
|
|
14
|
+
* `capabilities.subWorkflow.inputMapping: true`. Fixture-gated: requires
|
|
15
|
+
* `conformance-subworkflow-input-mapping` + the matching child fixture.
|
|
16
|
+
*
|
|
17
|
+
* @see RFCS/0022-dispatch-input-output-mapping.md §B
|
|
18
|
+
* @see spec/v1/node-packs.md §"`core.subWorkflow` contract"
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
24
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
25
|
+
|
|
26
|
+
const PARENT = 'conformance-subworkflow-input-mapping';
|
|
27
|
+
const CHILD = 'conformance-subworkflow-input-mapping-child';
|
|
28
|
+
const SKIP = !isFixtureAdvertised(PARENT) || !isFixtureAdvertised(CHILD);
|
|
29
|
+
|
|
30
|
+
interface RunEvent {
|
|
31
|
+
readonly type: string;
|
|
32
|
+
readonly nodeId?: string;
|
|
33
|
+
readonly payload?: { outputs?: { childRunId?: string } } & Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RunSnapshot {
|
|
37
|
+
readonly status: string;
|
|
38
|
+
readonly variables?: Record<string, unknown>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe.skipIf(SKIP)('subworkflow-input-mapping: parent → child variable seeding (RFC 0022 §B)', () => {
|
|
42
|
+
it('HVMAP-2: inputMapping seeds child variables, overrides defaultValue, child reads parent projection', async () => {
|
|
43
|
+
// Parent fixture declares currentPrdId='prd-1' as its defaultValue.
|
|
44
|
+
// The parent's `core.subWorkflow` node carries
|
|
45
|
+
// `inputMapping: { receivedPrdId: 'currentPrdId' }`. Child fixture
|
|
46
|
+
// declares `receivedPrdId.defaultValue='baked-in'` — the mapping
|
|
47
|
+
// MUST override that default per RFC 0022 §B.
|
|
48
|
+
const create = await driver.post('/v1/runs', { workflowId: PARENT });
|
|
49
|
+
expect(create.status).toBe(201);
|
|
50
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
51
|
+
|
|
52
|
+
const parentTerminal = (await pollUntilTerminal(parentRunId)) as RunSnapshot;
|
|
53
|
+
expect(parentTerminal.status, driver.describe(
|
|
54
|
+
'RFCS/0022-dispatch-input-output-mapping.md §B',
|
|
55
|
+
'parent run MUST reach terminal `completed` once the child finishes',
|
|
56
|
+
)).toBe('completed');
|
|
57
|
+
|
|
58
|
+
// Locate the child run via the parent's event log.
|
|
59
|
+
const eventsRes = await driver.get(`/v1/runs/${encodeURIComponent(parentRunId)}/events`);
|
|
60
|
+
expect(eventsRes.status).toBe(200);
|
|
61
|
+
const events = ((eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? []);
|
|
62
|
+
const subwfCompleted = events.find(
|
|
63
|
+
(e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
|
|
64
|
+
);
|
|
65
|
+
expect(subwfCompleted, driver.describe(
|
|
66
|
+
'spec/v1/node-packs.md §"`core.subWorkflow` contract"',
|
|
67
|
+
'parent event log MUST contain `node.completed` for the subwf-call node',
|
|
68
|
+
)).toBeDefined();
|
|
69
|
+
const childRunId = subwfCompleted?.payload?.outputs?.childRunId;
|
|
70
|
+
expect(typeof childRunId).toBe('string');
|
|
71
|
+
|
|
72
|
+
// Inspect the child run's final variables — receivedPrdId MUST be
|
|
73
|
+
// the parent's projection ("prd-1"), NOT the child's defaultValue
|
|
74
|
+
// ("baked-in").
|
|
75
|
+
const childSnapshotRes = await driver.get(`/v1/runs/${encodeURIComponent(childRunId!)}`);
|
|
76
|
+
expect(childSnapshotRes.status).toBe(200);
|
|
77
|
+
const childSnapshot = childSnapshotRes.json as RunSnapshot;
|
|
78
|
+
expect(childSnapshot.status, driver.describe(
|
|
79
|
+
'spec/v1/node-packs.md §"`core.subWorkflow` contract"',
|
|
80
|
+
'child run MUST reach terminal `completed`',
|
|
81
|
+
)).toBe('completed');
|
|
82
|
+
const childVars = childSnapshot.variables ?? {};
|
|
83
|
+
expect(childVars.receivedPrdId, driver.describe(
|
|
84
|
+
'RFCS/0022-dispatch-input-output-mapping.md §B',
|
|
85
|
+
'child `receivedPrdId` MUST be parent\'s `currentPrdId` projection ("prd-1"), overriding the child\'s defaultValue "baked-in"',
|
|
86
|
+
)).toBe('prd-1');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it.todo(
|
|
90
|
+
'HVMAP-2-unset: parent.currentPrdId unset; child receivedPrdId MUST surface as `undefined` (NOT omitted, NOT `null`). Requires a second parent fixture variant that omits currentPrdId\'s defaultValue.',
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
it.todo(
|
|
94
|
+
'HVMAP-2-no-midrun-propagation: child mid-run; parent updates currentPrdId; child receivedPrdId MUST remain at seeded value (one-shot fold per §B normative bullet). Requires a multi-step child that suspends + a parent path that mutates.',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
it.todo(
|
|
98
|
+
'HVMAP-2-refusal: host advertises core.subWorkflow surface but NOT capabilities.subWorkflow.inputMapping: true; workflow with non-empty inputMapping MUST fail registration with validation_error + details.requiredCapability === "subWorkflow.inputMapping". Requires a host-capability-toggle hook in the conformance harness.',
|
|
99
|
+
);
|
|
100
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table-cross-tenant-isolation — RFC 0016 §B point 1.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that rows inserted
|
|
5
|
+
* under tenant A MUST NOT appear in queries under tenant B against the
|
|
6
|
+
* same table+filter.
|
|
7
|
+
*
|
|
8
|
+
* @see RFCS/0016-host-table-storage-capability.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 final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["tableStorage"] : undefined;
|
|
23
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function call(tenantId: string, op: string, args: Record<string, unknown>) {
|
|
27
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'table', op, args });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('table-cross-tenant-isolation: advertisement shape (RFC 0016)', () => {
|
|
31
|
+
it('capabilities.tableStorage is either absent or a well-formed object', async () => {
|
|
32
|
+
const cap = await readCap();
|
|
33
|
+
if (cap === null) return;
|
|
34
|
+
expect(
|
|
35
|
+
typeof cap.supported,
|
|
36
|
+
driver.describe(
|
|
37
|
+
'capabilities.schema.json §tableStorage',
|
|
38
|
+
'capabilities.tableStorage.supported MUST be a boolean when present',
|
|
39
|
+
),
|
|
40
|
+
).toBe('boolean');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('table-cross-tenant-isolation: behavioral (RFC 0016 §B point 1)', () => {
|
|
45
|
+
it('insert under tenant A → query under tenant B returns 0 rows', async () => {
|
|
46
|
+
const cap = await readCap();
|
|
47
|
+
if (!cap || cap.supported !== true) return;
|
|
48
|
+
const table = `xtenant_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
49
|
+
|
|
50
|
+
const insertRes = await call('tenant-a', 'insert', {
|
|
51
|
+
table,
|
|
52
|
+
row: { id: 'r1', body: 'from-A' },
|
|
53
|
+
});
|
|
54
|
+
if (insertRes.status === 404) return; // seam not exposed
|
|
55
|
+
expect(insertRes.status, 'insert MUST succeed').toBe(200);
|
|
56
|
+
|
|
57
|
+
const queryRes = await call('tenant-b', 'query', { table, filter: {} });
|
|
58
|
+
expect(queryRes.status).toBe(200);
|
|
59
|
+
const body = queryRes.json as { rows?: unknown[] };
|
|
60
|
+
expect(
|
|
61
|
+
Array.isArray(body.rows) ? body.rows.length : -1,
|
|
62
|
+
driver.describe('RFC 0016 §B point 1', 'tenant B MUST see 0 rows in tenant A table'),
|
|
63
|
+
).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table-cursor-pagination — RFC 0016 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0016 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.tableStorage` 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: query MUST support filter + cursor pagination.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0016-*.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>)["tableStorage"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('table-cursor-pagination: advertisement shape (RFC 0016)', () => {
|
|
32
|
+
it('capabilities.tableStorage 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 §tableStorage',
|
|
39
|
+
'capabilities.tableStorage.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('table-cursor-pagination: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("first page returns N rows + nextCursor; second page resumes; final page returns nextCursor=null");
|
|
47
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* table-schema-enforcement — RFC 0016 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0016 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.tableStorage` 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: Subsequent rows MUST conform to the schema established on first insert.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0016-*.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>)["tableStorage"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('table-schema-enforcement: advertisement shape (RFC 0016)', () => {
|
|
32
|
+
it('capabilities.tableStorage 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 §tableStorage',
|
|
39
|
+
'capabilities.tableStorage.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('table-schema-enforcement: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("first insert declares schema; subsequent insert with wrong column type is rejected");
|
|
47
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vector-knn-roundtrip — RFC 0018 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.vectorStore` 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: upsert then query returns the same vectors in top-k order.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0018-*.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>)["vectorStore"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('vector-knn-roundtrip: advertisement shape (RFC 0018)', () => {
|
|
32
|
+
it('capabilities.vectorStore 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 §vectorStore',
|
|
39
|
+
'capabilities.vectorStore.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('vector-knn-roundtrip: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("upsert 10 vectors → query with one of them returns it as top-1");
|
|
47
|
+
it.todo("topK respects the configured limit");
|
|
48
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CF-5 close-out — receiver-side rejection contract per
|
|
3
|
+
* `spec/v1/webhooks.md` §"Signature recipe" + §"Replay-attack
|
|
4
|
+
* resistance". The companion to `webhook-signed-delivery.test.ts`
|
|
5
|
+
* (which verifies the FORWARD direction: host signs correctly) —
|
|
6
|
+
* this scenario verifies the REVERSE direction: a properly-
|
|
7
|
+
* implemented receiver MUST reject five named adversarial inputs.
|
|
8
|
+
*
|
|
9
|
+
* The reference receiver implementation lives at
|
|
10
|
+
* `conformance/src/lib/webhook-receiver.ts`. The SDK ships an
|
|
11
|
+
* identical-behavior `verifyWebhookSignature` helper across all
|
|
12
|
+
* three reference SDKs (SDK-3 close-out).
|
|
13
|
+
*
|
|
14
|
+
* Five adversarial cases:
|
|
15
|
+
*
|
|
16
|
+
* 1. Tampered body — host's HMAC is valid for body B; adversary
|
|
17
|
+
* delivers body B'. Receiver MUST reject with
|
|
18
|
+
* `signature_mismatch`.
|
|
19
|
+
* 2. Tampered HMAC — body is valid; adversary flips a byte of the
|
|
20
|
+
* v1=<hex> signature. Receiver MUST reject with
|
|
21
|
+
* `signature_mismatch`.
|
|
22
|
+
* 3. Stale timestamp — body + HMAC are valid but timestamp is
|
|
23
|
+
* older than the default 5-minute window. Receiver MUST reject
|
|
24
|
+
* with `timestamp_expired`.
|
|
25
|
+
* 4. Replayed signature — adversary resends a previously-accepted
|
|
26
|
+
* delivery within the window. Receiver MUST reject with
|
|
27
|
+
* `duplicate_signature`.
|
|
28
|
+
* 5. Wrong algorithm — host sends `algorithm: v2` (a future
|
|
29
|
+
* version a v1-only receiver doesn't recognize). Receiver MUST
|
|
30
|
+
* reject with `wrong_algorithm`.
|
|
31
|
+
*
|
|
32
|
+
* This scenario is purely receiver-side; it does NOT touch the host
|
|
33
|
+
* under test. It runs unconditionally (no capability gating).
|
|
34
|
+
*
|
|
35
|
+
* @see spec/v1/webhooks.md
|
|
36
|
+
* @see conformance/src/lib/webhook-receiver.ts
|
|
37
|
+
* @see sdk/typescript/src/webhook-helpers.ts
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import { describe, it, expect } from 'vitest';
|
|
41
|
+
import {
|
|
42
|
+
createReceiverState,
|
|
43
|
+
signPayload,
|
|
44
|
+
verifyWebhookDelivery,
|
|
45
|
+
} from '../lib/webhook-receiver.js';
|
|
46
|
+
|
|
47
|
+
describe('webhook-receiver-adversarial: receiver rejects five canonical attacks', () => {
|
|
48
|
+
const secret = 'test-secret-do-not-use-in-prod';
|
|
49
|
+
const body = JSON.stringify({ runId: 'r-conformance', type: 'run.completed' });
|
|
50
|
+
const nowSec = 1_715_775_600;
|
|
51
|
+
const ts = nowSec;
|
|
52
|
+
|
|
53
|
+
it('positive control: receiver accepts a freshly-signed valid delivery', () => {
|
|
54
|
+
const state = createReceiverState();
|
|
55
|
+
const { signatureHeader, timestampHeader, algorithmHeader } = signPayload(secret, ts, body);
|
|
56
|
+
const result = verifyWebhookDelivery(
|
|
57
|
+
secret,
|
|
58
|
+
signatureHeader,
|
|
59
|
+
algorithmHeader,
|
|
60
|
+
timestampHeader,
|
|
61
|
+
body,
|
|
62
|
+
state,
|
|
63
|
+
{ nowSeconds: nowSec },
|
|
64
|
+
);
|
|
65
|
+
expect(
|
|
66
|
+
result.accepted,
|
|
67
|
+
'webhooks.md §"Signature recipe": valid signature + fresh timestamp + correct algorithm MUST be accepted',
|
|
68
|
+
).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('case 1: tampered body → signature_mismatch', () => {
|
|
72
|
+
const state = createReceiverState();
|
|
73
|
+
const { signatureHeader, timestampHeader, algorithmHeader } = signPayload(secret, ts, body);
|
|
74
|
+
const tamperedBody = JSON.stringify({ runId: 'r-conformance', type: 'run.failed' });
|
|
75
|
+
const result = verifyWebhookDelivery(
|
|
76
|
+
secret,
|
|
77
|
+
signatureHeader,
|
|
78
|
+
algorithmHeader,
|
|
79
|
+
timestampHeader,
|
|
80
|
+
tamperedBody,
|
|
81
|
+
state,
|
|
82
|
+
{ nowSeconds: nowSec },
|
|
83
|
+
);
|
|
84
|
+
expect(result.accepted).toBe(false);
|
|
85
|
+
if (!result.accepted) {
|
|
86
|
+
expect(
|
|
87
|
+
result.reason,
|
|
88
|
+
'webhooks.md §"Signature recipe": tampered body MUST be rejected with signature_mismatch',
|
|
89
|
+
).toBe('signature_mismatch');
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('case 2: tampered HMAC → signature_mismatch', () => {
|
|
94
|
+
const state = createReceiverState();
|
|
95
|
+
const { signatureHeader, timestampHeader, algorithmHeader } = signPayload(secret, ts, body);
|
|
96
|
+
// Flip one hex character of the v1=<hex> signature.
|
|
97
|
+
const flipIndex = 5;
|
|
98
|
+
const orig = signatureHeader[flipIndex]!;
|
|
99
|
+
const replacement = orig === '0' ? '1' : '0';
|
|
100
|
+
const tampered = signatureHeader.slice(0, flipIndex) + replacement + signatureHeader.slice(flipIndex + 1);
|
|
101
|
+
const result = verifyWebhookDelivery(
|
|
102
|
+
secret,
|
|
103
|
+
tampered,
|
|
104
|
+
algorithmHeader,
|
|
105
|
+
timestampHeader,
|
|
106
|
+
body,
|
|
107
|
+
state,
|
|
108
|
+
{ nowSeconds: nowSec },
|
|
109
|
+
);
|
|
110
|
+
expect(result.accepted).toBe(false);
|
|
111
|
+
if (!result.accepted) {
|
|
112
|
+
expect(result.reason).toBe('signature_mismatch');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('case 3: stale timestamp → timestamp_expired', () => {
|
|
117
|
+
const state = createReceiverState();
|
|
118
|
+
const staleTs = nowSec - 10_000; // way past the 5-minute window
|
|
119
|
+
const { signatureHeader, timestampHeader, algorithmHeader } = signPayload(secret, staleTs, body);
|
|
120
|
+
const result = verifyWebhookDelivery(
|
|
121
|
+
secret,
|
|
122
|
+
signatureHeader,
|
|
123
|
+
algorithmHeader,
|
|
124
|
+
timestampHeader,
|
|
125
|
+
body,
|
|
126
|
+
state,
|
|
127
|
+
{ nowSeconds: nowSec },
|
|
128
|
+
);
|
|
129
|
+
expect(result.accepted).toBe(false);
|
|
130
|
+
if (!result.accepted) {
|
|
131
|
+
expect(
|
|
132
|
+
result.reason,
|
|
133
|
+
'webhooks.md §"Replay-attack resistance": timestamp older than freshness window MUST be rejected with timestamp_expired',
|
|
134
|
+
).toBe('timestamp_expired');
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('case 4: replayed signature → duplicate_signature', () => {
|
|
139
|
+
const state = createReceiverState();
|
|
140
|
+
const { signatureHeader, timestampHeader, algorithmHeader } = signPayload(secret, ts, body);
|
|
141
|
+
// First delivery accepted.
|
|
142
|
+
const first = verifyWebhookDelivery(
|
|
143
|
+
secret,
|
|
144
|
+
signatureHeader,
|
|
145
|
+
algorithmHeader,
|
|
146
|
+
timestampHeader,
|
|
147
|
+
body,
|
|
148
|
+
state,
|
|
149
|
+
{ nowSeconds: nowSec },
|
|
150
|
+
);
|
|
151
|
+
expect(first.accepted).toBe(true);
|
|
152
|
+
// Replay — same signature, same body, same timestamp, still
|
|
153
|
+
// within the window.
|
|
154
|
+
const replay = verifyWebhookDelivery(
|
|
155
|
+
secret,
|
|
156
|
+
signatureHeader,
|
|
157
|
+
algorithmHeader,
|
|
158
|
+
timestampHeader,
|
|
159
|
+
body,
|
|
160
|
+
state,
|
|
161
|
+
{ nowSeconds: nowSec },
|
|
162
|
+
);
|
|
163
|
+
expect(replay.accepted).toBe(false);
|
|
164
|
+
if (!replay.accepted) {
|
|
165
|
+
expect(
|
|
166
|
+
replay.reason,
|
|
167
|
+
'webhooks.md §"Replay-attack resistance": replay of an already-accepted signature MUST be rejected with duplicate_signature',
|
|
168
|
+
).toBe('duplicate_signature');
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('case 5: wrong algorithm → wrong_algorithm', () => {
|
|
173
|
+
const state = createReceiverState();
|
|
174
|
+
const { signatureHeader, timestampHeader } = signPayload(secret, ts, body);
|
|
175
|
+
const result = verifyWebhookDelivery(
|
|
176
|
+
secret,
|
|
177
|
+
signatureHeader,
|
|
178
|
+
'v2',
|
|
179
|
+
timestampHeader,
|
|
180
|
+
body,
|
|
181
|
+
state,
|
|
182
|
+
{ nowSeconds: nowSec },
|
|
183
|
+
);
|
|
184
|
+
expect(result.accepted).toBe(false);
|
|
185
|
+
if (!result.accepted) {
|
|
186
|
+
expect(
|
|
187
|
+
result.reason,
|
|
188
|
+
'webhooks.md §"Signature algorithm versioning": algorithm other than v1 MUST be rejected with wrong_algorithm by a v1-only receiver',
|
|
189
|
+
).toBe('wrong_algorithm');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('case 6: malformed signature header → malformed_signature_header', () => {
|
|
194
|
+
const state = createReceiverState();
|
|
195
|
+
const { timestampHeader, algorithmHeader } = signPayload(secret, ts, body);
|
|
196
|
+
const result = verifyWebhookDelivery(
|
|
197
|
+
secret,
|
|
198
|
+
'not-a-canonical-header',
|
|
199
|
+
algorithmHeader,
|
|
200
|
+
timestampHeader,
|
|
201
|
+
body,
|
|
202
|
+
state,
|
|
203
|
+
{ nowSeconds: nowSec },
|
|
204
|
+
);
|
|
205
|
+
expect(result.accepted).toBe(false);
|
|
206
|
+
if (!result.accepted) {
|
|
207
|
+
expect(result.reason).toBe('malformed_signature_header');
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
});
|