@openwop/openwop-conformance 1.0.0 → 1.1.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.
- package/CHANGELOG.md +17 -0
- package/README.md +31 -6
- package/api/grpc/openwop.proto +251 -0
- package/api/openapi.yaml +109 -3
- package/coverage.md +48 -9
- package/fixtures/conformance-configurable-schema.json +39 -0
- package/fixtures/conformance-subworkflow-parent.json +1 -1
- package/fixtures/conformance-wasm-pack-memory-cap-breach.json +23 -0
- package/fixtures/openwop-smoke-byok-roundtrip.json +25 -0
- package/fixtures.md +21 -0
- package/package.json +3 -1
- package/schemas/README.md +4 -0
- package/schemas/audit-verify-result.schema.json +90 -0
- package/schemas/capabilities.schema.json +342 -1
- package/schemas/node-pack-manifest.schema.json +4 -4
- package/schemas/pack-lockfile.schema.json +92 -0
- package/schemas/registry-version-manifest.schema.json +145 -0
- package/schemas/run-event-payloads.schema.json +20 -4
- package/schemas/run-event.schema.json +2 -1
- package/schemas/security-advisory.schema.json +109 -0
- package/src/lib/a2a-fake-peer.ts +143 -56
- package/src/lib/behavior-gate.ts +107 -0
- package/src/lib/env.ts +37 -0
- package/src/lib/grpc-framing.test.ts +96 -0
- package/src/lib/grpc-framing.ts +76 -0
- package/src/lib/oidc-issuer.test.ts +328 -0
- package/src/lib/oidc-issuer.ts +241 -0
- package/src/lib/otel-collector-grpc.test.ts +191 -0
- package/src/lib/otel-collector.test.ts +303 -0
- package/src/lib/otel-collector.ts +318 -14
- package/src/lib/otlp-protobuf.test.ts +461 -0
- package/src/lib/otlp-protobuf.ts +529 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +147 -28
- package/src/scenarios/agentConfidenceEscalation.test.ts +1 -0
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +1 -0
- package/src/scenarios/agentMemoryRedactionContract.test.ts +1 -0
- package/src/scenarios/agentMemoryRoundTrip.test.ts +1 -0
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +1 -0
- package/src/scenarios/agentMessageReducer.test.ts +1 -0
- package/src/scenarios/agentMetadata.test.ts +1 -0
- package/src/scenarios/agentPackExport.test.ts +1 -0
- package/src/scenarios/agentPackInstall.test.ts +1 -0
- package/src/scenarios/agentPackProvenance.test.ts +1 -0
- package/src/scenarios/audit-log-integrity.test.ts +3 -6
- package/src/scenarios/auth-api-key-rotation.test.ts +182 -0
- package/src/scenarios/auth-mtls.test.ts +274 -0
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +259 -0
- package/src/scenarios/auth-oidc-user-bearer.test.ts +361 -0
- package/src/scenarios/bulk-cancel.test.ts +111 -0
- package/src/scenarios/configurable-schema.test.ts +48 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +1 -0
- package/src/scenarios/conversationLifecycle.test.ts +1 -0
- package/src/scenarios/conversationReplayDeterminism.test.ts +1 -0
- package/src/scenarios/conversationVsLegacySuspend.test.ts +1 -0
- package/src/scenarios/debug-bundle-truncation.test.ts +95 -0
- package/src/scenarios/discovery.test.ts +183 -0
- package/src/scenarios/http-client-ssrf.test.ts +71 -0
- package/src/scenarios/idempotency.test.ts +6 -0
- package/src/scenarios/idempotencyRetry.test.ts +3 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +205 -34
- package/src/scenarios/mcp-toolcall-redaction.test.ts +66 -0
- package/src/scenarios/memory-compaction-event-emitted.test.ts +121 -0
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +116 -0
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +127 -0
- package/src/scenarios/metric-emission.test.ts +113 -0
- package/src/scenarios/multi-region-idempotency.test.ts +39 -4
- package/src/scenarios/orchestratorConservativePath.test.ts +1 -0
- package/src/scenarios/orchestratorDispatch.test.ts +1 -0
- package/src/scenarios/orchestratorTermination.test.ts +1 -0
- package/src/scenarios/otel-emission-grpc.test.ts +98 -0
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +139 -0
- package/src/scenarios/pause-resume.test.ts +119 -0
- package/src/scenarios/production-backpressure.test.ts +342 -0
- package/src/scenarios/production-retention-expiry.test.ts +164 -0
- package/src/scenarios/registry-public.test.ts +222 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +35 -0
- package/src/scenarios/replay-retention-expiry.test.ts +178 -0
- package/src/scenarios/restart-during-run.test.ts +177 -0
- package/src/scenarios/spec-corpus-validity.test.ts +59 -26
- package/src/scenarios/staleClaim.test.ts +3 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +67 -10
- package/src/scenarios/wasm-pack-memory-cap.test.ts +64 -9
- package/src/scenarios/webhook-negative.test.ts +90 -0
- package/src/scenarios/webhook-signed-delivery.test.ts +178 -0
- package/src/setup.ts +25 -1
- package/vitest.config.ts +5 -1
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0012 §B — `memory.compacted` event emission shape.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that hosts advertising
|
|
5
|
+
* `capabilities.memory.compaction.supported: true` emit a canonical
|
|
6
|
+
* `memory.compacted` event payload per `run-event-payloads.schema.json`
|
|
7
|
+
* §`memoryCompacted` whenever a compaction run completes.
|
|
8
|
+
*
|
|
9
|
+
* Required fields per the schema:
|
|
10
|
+
* - `memoryRef` (string, non-empty)
|
|
11
|
+
* - `outputId` (string, non-empty)
|
|
12
|
+
* - `sourceCount` (integer ≥ 1)
|
|
13
|
+
* - `trigger` (closed enum: `host-managed | client-requested | both`)
|
|
14
|
+
* - `byteSize` (integer ≥ 0)
|
|
15
|
+
*
|
|
16
|
+
* Optional:
|
|
17
|
+
* - `sourceIds` (array of non-empty strings; exhaustive within the
|
|
18
|
+
* array — no "and N more" semantics — when present)
|
|
19
|
+
*
|
|
20
|
+
* Gating identical to `memory-compaction-sr1-carry-forward.test.ts`:
|
|
21
|
+
* capability advertisement + test seam reachable.
|
|
22
|
+
*
|
|
23
|
+
* @see RFCS/0012-memory-compaction-profile.md §B
|
|
24
|
+
* @see schemas/run-event-payloads.schema.json §memoryCompacted
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect } from 'vitest';
|
|
28
|
+
import { driver } from '../lib/driver.js';
|
|
29
|
+
|
|
30
|
+
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-event_longTerm';
|
|
31
|
+
|
|
32
|
+
interface MemoryCaps {
|
|
33
|
+
compaction?: { supported?: boolean };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function isCompactionAdvertised(): Promise<boolean> {
|
|
37
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
38
|
+
const memory = (disco.json as { capabilities?: { memory?: MemoryCaps } }).capabilities?.memory;
|
|
39
|
+
return memory?.compaction?.supported === true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function isTestSeamReachable(): Promise<boolean> {
|
|
43
|
+
const r = await driver.post('/v1/test/memory/compact', {});
|
|
44
|
+
return r.status !== 404;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('memory-compaction-event-emitted: canonical memory.compacted payload shape', () => {
|
|
48
|
+
it('compaction run returns a canonical memoryCompacted payload', async () => {
|
|
49
|
+
if (!(await isCompactionAdvertised())) {
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.warn('[rfc0012-event] capabilities.memory.compaction.supported not advertised; skipping');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!(await isTestSeamReachable())) {
|
|
55
|
+
// eslint-disable-next-line no-console
|
|
56
|
+
console.warn('[rfc0012-event] test seam unreachable; skipping');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const seed = await driver.post('/v1/test/memory/seed', {
|
|
61
|
+
memoryRef: MEMORY_REF,
|
|
62
|
+
entries: [
|
|
63
|
+
{ id: `event-src-${Date.now()}-a`, content: 'First memory entry.' },
|
|
64
|
+
{ id: `event-src-${Date.now()}-b`, content: 'Second memory entry.' },
|
|
65
|
+
{ id: `event-src-${Date.now()}-c`, content: 'Third memory entry.' },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
expect(seed.status).toBe(201);
|
|
69
|
+
|
|
70
|
+
const compactRes = await driver.post('/v1/test/memory/compact', {
|
|
71
|
+
memoryRef: MEMORY_REF,
|
|
72
|
+
});
|
|
73
|
+
expect(compactRes.status).toBe(200);
|
|
74
|
+
|
|
75
|
+
const event = compactRes.json as {
|
|
76
|
+
type?: string;
|
|
77
|
+
payload?: Record<string, unknown>;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
expect(event.type, driver.describe(
|
|
81
|
+
'observability.md §"Canonical run lifecycle event names"',
|
|
82
|
+
'event MUST be type=memory.compacted',
|
|
83
|
+
)).toBe('memory.compacted');
|
|
84
|
+
|
|
85
|
+
const payload = event.payload ?? {};
|
|
86
|
+
|
|
87
|
+
expect(typeof payload.memoryRef, driver.describe(
|
|
88
|
+
'RFC 0012 §B / run-event-payloads.schema.json §memoryCompacted',
|
|
89
|
+
'memoryRef MUST be a non-empty string',
|
|
90
|
+
)).toBe('string');
|
|
91
|
+
expect((payload.memoryRef as string).length).toBeGreaterThan(0);
|
|
92
|
+
|
|
93
|
+
expect(typeof payload.outputId, driver.describe(
|
|
94
|
+
'RFC 0012 §B',
|
|
95
|
+
'outputId MUST be a string identifying the distilled entry',
|
|
96
|
+
)).toBe('string');
|
|
97
|
+
expect((payload.outputId as string).length).toBeGreaterThan(0);
|
|
98
|
+
|
|
99
|
+
expect(Number.isInteger(payload.sourceCount), driver.describe(
|
|
100
|
+
'RFC 0012 §B',
|
|
101
|
+
'sourceCount MUST be an integer',
|
|
102
|
+
)).toBe(true);
|
|
103
|
+
expect(payload.sourceCount as number).toBeGreaterThanOrEqual(1);
|
|
104
|
+
|
|
105
|
+
expect(['host-managed', 'client-requested', 'both']).toContain(payload.trigger);
|
|
106
|
+
|
|
107
|
+
expect(Number.isInteger(payload.byteSize), driver.describe(
|
|
108
|
+
'RFC 0012 §B',
|
|
109
|
+
'byteSize MUST be an integer',
|
|
110
|
+
)).toBe(true);
|
|
111
|
+
expect(payload.byteSize as number).toBeGreaterThanOrEqual(0);
|
|
112
|
+
|
|
113
|
+
if (payload.sourceIds !== undefined) {
|
|
114
|
+
expect(Array.isArray(payload.sourceIds)).toBe(true);
|
|
115
|
+
for (const id of payload.sourceIds as unknown[]) {
|
|
116
|
+
expect(typeof id, 'sourceIds entries MUST be strings').toBe('string');
|
|
117
|
+
expect((id as string).length).toBeGreaterThan(0);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0012 §C — `compacted-from:<id>` provenance tag convention.
|
|
3
|
+
*
|
|
4
|
+
* The distilled entry SHOULD (not MUST) carry a tag of the form
|
|
5
|
+
* `compacted-from:<compactionRunId>` where `<compactionRunId>` is a
|
|
6
|
+
* host-issued opaque identifier. This lets `MemoryAdapter.list`
|
|
7
|
+
* consumers detect compacted entries without needing access to the
|
|
8
|
+
* `memory.compacted` event stream.
|
|
9
|
+
*
|
|
10
|
+
* SOFT ASSERTION: log-and-warn if absent, fail only if a present tag
|
|
11
|
+
* is malformed. Hosts with structurally-constrained tag-spaces (legacy
|
|
12
|
+
* tag-prefix discipline, fixed-vocabulary tagging) MAY omit this —
|
|
13
|
+
* the `memory.compacted` event itself remains the canonical provenance
|
|
14
|
+
* signal.
|
|
15
|
+
*
|
|
16
|
+
* Gating identical to the other RFC 0012 scenarios.
|
|
17
|
+
*
|
|
18
|
+
* @see RFCS/0012-memory-compaction-profile.md §C
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { driver } from '../lib/driver.js';
|
|
23
|
+
|
|
24
|
+
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-tag_longTerm';
|
|
25
|
+
const COMPACTED_FROM_RE = /^compacted-from:[^\s:][^\s]*$/;
|
|
26
|
+
|
|
27
|
+
interface MemoryCaps {
|
|
28
|
+
compaction?: { supported?: boolean };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface MemoryListResponse {
|
|
32
|
+
entries?: Array<{ id?: string; tags?: string[] }>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function isCompactionAdvertised(): Promise<boolean> {
|
|
36
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
37
|
+
const memory = (disco.json as { capabilities?: { memory?: MemoryCaps } }).capabilities?.memory;
|
|
38
|
+
return memory?.compaction?.supported === true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function isTestSeamReachable(): Promise<boolean> {
|
|
42
|
+
const r = await driver.post('/v1/test/memory/compact', {});
|
|
43
|
+
return r.status !== 404;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe('memory-compaction-provenance-tag: compacted-from:<id> tag follows §C convention', () => {
|
|
47
|
+
it('compacted entry carries a well-formed compacted-from tag, OR omits it cleanly (no malformed tags)', async () => {
|
|
48
|
+
if (!(await isCompactionAdvertised())) {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.warn('[rfc0012-tag] capabilities.memory.compaction.supported not advertised; skipping');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (!(await isTestSeamReachable())) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.warn('[rfc0012-tag] test seam unreachable; skipping');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Seed + compact.
|
|
60
|
+
const seedStamp = Date.now();
|
|
61
|
+
const seed = await driver.post('/v1/test/memory/seed', {
|
|
62
|
+
memoryRef: MEMORY_REF,
|
|
63
|
+
entries: [
|
|
64
|
+
{ id: `tag-src-${seedStamp}-1`, content: 'Source content alpha.' },
|
|
65
|
+
{ id: `tag-src-${seedStamp}-2`, content: 'Source content beta.' },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
expect(seed.status).toBe(201);
|
|
69
|
+
|
|
70
|
+
const compactRes = await driver.post('/v1/test/memory/compact', {
|
|
71
|
+
memoryRef: MEMORY_REF,
|
|
72
|
+
});
|
|
73
|
+
expect(compactRes.status).toBe(200);
|
|
74
|
+
|
|
75
|
+
const event = compactRes.json as { payload?: { outputId?: string } };
|
|
76
|
+
const outputId = event.payload?.outputId;
|
|
77
|
+
expect(typeof outputId).toBe('string');
|
|
78
|
+
|
|
79
|
+
// Resolve the entry via the wire MemoryAdapter list surface (no
|
|
80
|
+
// direct get-by-id wire endpoint; we filter list results).
|
|
81
|
+
// Hosts that don't expose memory:list on the wire skip — this is
|
|
82
|
+
// a `MemoryAdapter.list` surface check, which the canonical
|
|
83
|
+
// capabilities.memory.supported claim already covers.
|
|
84
|
+
const listRes = await driver.get(
|
|
85
|
+
`/v1/memory/${encodeURIComponent(MEMORY_REF)}?limit=50`,
|
|
86
|
+
);
|
|
87
|
+
if (listRes.status === 404) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.warn('[rfc0012-tag] host does not expose memory:list at /v1/memory/{ref}; skipping tag inspection (canonical provenance signal remains the memory.compacted event itself)');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
expect(listRes.status, 'memory:list MUST return 200 when reachable').toBe(200);
|
|
93
|
+
|
|
94
|
+
const body = (listRes.json as MemoryListResponse) ?? {};
|
|
95
|
+
const entries = body.entries ?? [];
|
|
96
|
+
const output = entries.find((e) => e.id === outputId);
|
|
97
|
+
if (!output) {
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.warn(`[rfc0012-tag] outputId ${outputId} not visible via memory:list; cannot inspect tags`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const tags = output.tags ?? [];
|
|
103
|
+
|
|
104
|
+
// RFC 0012 §C: SHOULD-tag, soft assertion.
|
|
105
|
+
const provenance = tags.find((t) => t.startsWith('compacted-from:'));
|
|
106
|
+
if (provenance === undefined) {
|
|
107
|
+
// eslint-disable-next-line no-console
|
|
108
|
+
console.warn('[rfc0012-tag] output entry has no compacted-from:<id> tag — RFC 0012 §C is SHOULD, not MUST; pass with warning');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
expect(provenance, driver.describe(
|
|
112
|
+
'RFC 0012 §C',
|
|
113
|
+
'compacted-from tag MUST match `compacted-from:<id>` shape (non-empty id, no whitespace) when present',
|
|
114
|
+
)).toMatch(COMPACTED_FROM_RE);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0012 §D — SR-1 carry-forward through memory compaction.
|
|
3
|
+
*
|
|
4
|
+
* Verifies the load-bearing security claim of RFC 0012 (Memory
|
|
5
|
+
* Compaction Profile, `Active` 2026-05-13 — comment window closes
|
|
6
|
+
* 2026-05-20): when a host advertising
|
|
7
|
+
* `capabilities.memory.compaction.supported: true` produces a
|
|
8
|
+
* compacted `MemoryEntry`, the derived content MUST pass the
|
|
9
|
+
* same BYOK redaction harness as a fresh `put`. The fact that
|
|
10
|
+
* source entries were SR-1-compliant at original `put` time is
|
|
11
|
+
* NOT evidence to skip redaction on derived content — summarization
|
|
12
|
+
* models can introduce secret-shaped substrings (hallucinated
|
|
13
|
+
* tokens, format-leaks from in-context examples) not present in
|
|
14
|
+
* any source.
|
|
15
|
+
*
|
|
16
|
+
* Gating:
|
|
17
|
+
* - `capabilities.memory.compaction.supported` MUST be `true`.
|
|
18
|
+
* - Host MUST expose the test seam at `POST /v1/test/memory/{seed,
|
|
19
|
+
* compact}` — gated on the host's `OPENWOP_TEST_TRIGGER_COMPACTION`
|
|
20
|
+
* env var. Without it the scenario can't synchronously drive
|
|
21
|
+
* compaction (RFC 0012 normates only `trigger: 'host-managed'`).
|
|
22
|
+
* The seam itself is host-implementation-specific; the conformance
|
|
23
|
+
* suite skips when the seam isn't reachable.
|
|
24
|
+
*
|
|
25
|
+
* @see RFCS/0012-memory-compaction-profile.md §D
|
|
26
|
+
* @see SECURITY/invariants.yaml `memory-compaction-sr-1-carry-forward`
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { driver } from '../lib/driver.js';
|
|
31
|
+
|
|
32
|
+
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-sr1_longTerm';
|
|
33
|
+
|
|
34
|
+
interface MemoryCaps {
|
|
35
|
+
compaction?: { supported?: boolean };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function isCompactionAdvertised(): Promise<boolean> {
|
|
39
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
40
|
+
const memory = (disco.json as { capabilities?: { memory?: MemoryCaps } }).capabilities?.memory;
|
|
41
|
+
return memory?.compaction?.supported === true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function isTestSeamReachable(): Promise<boolean> {
|
|
45
|
+
// Probe the seam with an empty body — expects 400 if reachable
|
|
46
|
+
// (validation_error on missing memoryRef), 404 when disabled.
|
|
47
|
+
const r = await driver.post('/v1/test/memory/compact', {});
|
|
48
|
+
return r.status !== 404;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('memory-compaction-sr1-carry-forward: derived content passes the BYOK redaction harness', () => {
|
|
52
|
+
it('compacted MemoryEntry content MUST NOT carry source-side form-leak signatures', async () => {
|
|
53
|
+
if (!(await isCompactionAdvertised())) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.warn('[rfc0012-sr1] capabilities.memory.compaction.supported not advertised; skipping');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!(await isTestSeamReachable())) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.warn('[rfc0012-sr1] test seam /v1/test/memory/compact unreachable; skipping (set host\'s OPENWOP_TEST_TRIGGER_COMPACTION=true)');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 1. Seed source entries containing:
|
|
65
|
+
// - The canonical `[BYOK:...]` form-leak signature (placeholder
|
|
66
|
+
// surfaces verbatim — should be caught by SR-1 carry-forward).
|
|
67
|
+
// - A non-canonical `<REDACTED:...>` marker that the host's
|
|
68
|
+
// redaction harness should re-canonicalize.
|
|
69
|
+
// - Plain, non-sensitive prose.
|
|
70
|
+
const seed = await driver.post('/v1/test/memory/seed', {
|
|
71
|
+
memoryRef: MEMORY_REF,
|
|
72
|
+
entries: [
|
|
73
|
+
{ id: `sr1-src-${Date.now()}-1`, content: 'User confirmed: [BYOK:hk_live_canary_42]' },
|
|
74
|
+
{ id: `sr1-src-${Date.now()}-2`, content: 'Resolved <REDACTED:db-prod-creds> outage.' },
|
|
75
|
+
{ id: `sr1-src-${Date.now()}-3`, content: 'Customer asked about pricing tiers.' },
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
expect(seed.status, 'seed endpoint MUST return 201 when reachable').toBe(201);
|
|
79
|
+
|
|
80
|
+
// 2. Drive compaction synchronously.
|
|
81
|
+
const compactRes = await driver.post('/v1/test/memory/compact', {
|
|
82
|
+
memoryRef: MEMORY_REF,
|
|
83
|
+
});
|
|
84
|
+
expect(compactRes.status, 'compact MUST return 200 with ≥2 source entries').toBe(200);
|
|
85
|
+
|
|
86
|
+
const event = compactRes.json as {
|
|
87
|
+
type?: string;
|
|
88
|
+
payload?: { outputId?: string; memoryRef?: string };
|
|
89
|
+
// Out-of-band field from the test seam carrying the persisted
|
|
90
|
+
// entry bytes; the wire-level `memory.compacted` event does NOT
|
|
91
|
+
// carry content. Required for SR-1 verification — the canonical
|
|
92
|
+
// event payload is shape-only and would pass this scenario
|
|
93
|
+
// trivially without it.
|
|
94
|
+
outputContent?: string;
|
|
95
|
+
};
|
|
96
|
+
expect(event.type, 'event payload MUST be type=memory.compacted').toBe('memory.compacted');
|
|
97
|
+
|
|
98
|
+
if (typeof event.outputContent !== 'string') {
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.warn('[rfc0012-sr1] test seam did not return outputContent; the wire-level memory.compacted shape does not surface content so without a host-side seam we cannot verify §D end-to-end. Skipping.');
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The load-bearing assertion: the PERSISTED entry content (what
|
|
105
|
+
// future MemoryAdapter.get / list consumers would see) MUST NOT
|
|
106
|
+
// carry source-side form-leak signatures. A host that skips its
|
|
107
|
+
// BYOK redaction pass on derived content fails here.
|
|
108
|
+
expect(event.outputContent.includes('[BYOK:hk_live_canary_42]'), driver.describe(
|
|
109
|
+
'RFC 0012 §D',
|
|
110
|
+
'derived MemoryEntry.content MUST NOT carry source-side [BYOK:...] form-leak signatures (SR-1 carry-forward)',
|
|
111
|
+
)).toBe(false);
|
|
112
|
+
expect(event.outputContent.includes('<REDACTED:db-prod-creds>'), driver.describe(
|
|
113
|
+
'RFC 0012 §D',
|
|
114
|
+
'derived MemoryEntry.content MUST NOT echo non-canonical <REDACTED:...> markers from sources',
|
|
115
|
+
)).toBe(false);
|
|
116
|
+
|
|
117
|
+
// Positive: the canonical `[REDACTED:...]` placeholder MUST be
|
|
118
|
+
// present where SR-1 carry-forward re-substituted a source-side
|
|
119
|
+
// leak. Pinning this prevents a host from "passing" by simply
|
|
120
|
+
// stripping source content rather than redacting it (which would
|
|
121
|
+
// also lose audit signal).
|
|
122
|
+
expect(event.outputContent, driver.describe(
|
|
123
|
+
'RFC 0012 §D',
|
|
124
|
+
'derived MemoryEntry.content MUST carry canonical [REDACTED:...] placeholders where source-side leaks were re-substituted',
|
|
125
|
+
)).toMatch(/\[REDACTED:[^\]]+\]/);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track 11: metric-emission verification.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that hosts claiming `capabilities.observability.metrics`
|
|
5
|
+
* emit the canonical `openwop.run.backlog`, `openwop.queue.depth`, and
|
|
6
|
+
* (after at least one completed run) `openwop.run.duration` metrics
|
|
7
|
+
* documented in `spec/v1/observability.md`.
|
|
8
|
+
*
|
|
9
|
+
* Operator contract (same as `otel-emission.test.ts`):
|
|
10
|
+
* 1. Start the conformance suite with `OPENWOP_OTEL_COLLECTOR=true`
|
|
11
|
+
* and `OPENWOP_OTEL_COLLECTOR_PORT=<port>`.
|
|
12
|
+
* 2. Boot the host with `OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:<port>`.
|
|
13
|
+
*
|
|
14
|
+
* Skip conditions:
|
|
15
|
+
* - Collector disabled (`OPENWOP_OTEL_COLLECTOR` unset / false).
|
|
16
|
+
* - Host doesn't advertise `capabilities.observability.metrics.supported`.
|
|
17
|
+
*
|
|
18
|
+
* @see spec/v1/observability.md §"Metrics"
|
|
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
|
+
import { getCollector } from '../lib/otel-collector.js';
|
|
26
|
+
|
|
27
|
+
const FIXTURE = 'conformance-noop';
|
|
28
|
+
|
|
29
|
+
interface MetricsCaps {
|
|
30
|
+
supported?: boolean;
|
|
31
|
+
names?: ReadonlyArray<string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function metricsAdvertised(): Promise<MetricsCaps | null> {
|
|
35
|
+
try {
|
|
36
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
37
|
+
const caps = (disco.json as {
|
|
38
|
+
capabilities?: { observability?: { metrics?: MetricsCaps } };
|
|
39
|
+
}).capabilities;
|
|
40
|
+
return caps?.observability?.metrics ?? null;
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function waitForMetric(name: string, timeoutMs = 5_000): Promise<boolean> {
|
|
47
|
+
const collector = getCollector();
|
|
48
|
+
if (!collector) return false;
|
|
49
|
+
const deadline = Date.now() + timeoutMs;
|
|
50
|
+
while (Date.now() < deadline) {
|
|
51
|
+
if (collector.metricByName(name)) return true;
|
|
52
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
53
|
+
}
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
describe('metric-emission: canonical openwop.* metrics arrive at the collector', () => {
|
|
58
|
+
it('host emits openwop.run.backlog, openwop.queue.depth, and openwop.run.duration', async () => {
|
|
59
|
+
if (!getCollector()) {
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.warn(
|
|
62
|
+
'[metric-emission] collector not started; set OPENWOP_OTEL_COLLECTOR=true to run',
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const metricsCaps = await metricsAdvertised();
|
|
67
|
+
if (!metricsCaps?.supported) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.warn(
|
|
70
|
+
'[metric-emission] host does not advertise observability.metrics.supported; skipping',
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!isFixtureAdvertised(FIXTURE)) {
|
|
75
|
+
// eslint-disable-next-line no-console
|
|
76
|
+
console.warn(`[metric-emission] ${FIXTURE} not advertised; skipping`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const collector = getCollector()!;
|
|
81
|
+
collector.reset();
|
|
82
|
+
|
|
83
|
+
// Drive at least one completed run so openwop.run.duration has a sample.
|
|
84
|
+
const create = await driver.post('/v1/runs', { workflowId: FIXTURE });
|
|
85
|
+
expect(create.status).toBe(201);
|
|
86
|
+
const runId = (create.json as { runId: string }).runId;
|
|
87
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
88
|
+
|
|
89
|
+
// Wait for the host's metric-emit tick to land at the collector.
|
|
90
|
+
const sawBacklog = await waitForMetric('openwop.run.backlog', 5_000);
|
|
91
|
+
expect(sawBacklog, driver.describe(
|
|
92
|
+
'observability.md §"Metrics"',
|
|
93
|
+
'host claiming metrics MUST emit openwop.run.backlog',
|
|
94
|
+
)).toBe(true);
|
|
95
|
+
|
|
96
|
+
const sawQueueDepth = await waitForMetric('openwop.queue.depth', 5_000);
|
|
97
|
+
expect(sawQueueDepth, driver.describe(
|
|
98
|
+
'observability.md §"Metrics"',
|
|
99
|
+
'host claiming metrics MUST emit openwop.queue.depth',
|
|
100
|
+
)).toBe(true);
|
|
101
|
+
|
|
102
|
+
const sawDuration = await waitForMetric('openwop.run.duration', 5_000);
|
|
103
|
+
expect(sawDuration, driver.describe(
|
|
104
|
+
'observability.md §"Metrics"',
|
|
105
|
+
'host claiming metrics MUST emit openwop.run.duration after a completed run',
|
|
106
|
+
)).toBe(true);
|
|
107
|
+
|
|
108
|
+
// Shape spot-check: backlog gauge data point has a numeric value.
|
|
109
|
+
const backlog = collector.metricByName('openwop.run.backlog')!;
|
|
110
|
+
expect(backlog.kind).toBe('gauge');
|
|
111
|
+
expect(typeof backlog.dataPoint.value).toBe('number');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -2,18 +2,29 @@
|
|
|
2
2
|
* Track 13: multi-region idempotency capability shape (idempotency.md v1.1).
|
|
3
3
|
*
|
|
4
4
|
* Verifies that hosts advertising the multi-region idempotency annex
|
|
5
|
-
* surface a valid `capabilities.idempotency.crossRegion` value
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* surface a valid `capabilities.idempotency.crossRegion` value AND, when
|
|
6
|
+
* claiming `'best-effort'` or `'strict'`, expose the operator-tier
|
|
7
|
+
* metric names per `idempotency.md` §"Operator surface".
|
|
8
|
+
*
|
|
9
|
+
* The annex's partition-replay convergence rule cannot be exercised
|
|
10
|
+
* black-box (it requires multi-region host deployment under a real
|
|
11
|
+
* partition); the algorithm itself is verified in-process via the
|
|
12
|
+
* Postgres host's `multi-region-idempotency.test.ts` smoke against
|
|
13
|
+
* the canonical resolver. This scenario validates the discovery-
|
|
14
|
+
* document shape so clients can rely on the capability for routing
|
|
15
|
+
* decisions.
|
|
9
16
|
*
|
|
10
17
|
* @see spec/v1/idempotency.md §"Multi-region idempotency"
|
|
18
|
+
* @see examples/hosts/postgres/src/multi-region.ts (canonical resolver)
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
import { describe, it, expect } from 'vitest';
|
|
14
22
|
import { driver } from '../lib/driver.js';
|
|
15
23
|
|
|
16
24
|
const ALLOWED = new Set(['single-region', 'best-effort', 'strict']);
|
|
25
|
+
const REQUIRED_METRICS_WHEN_MULTI_REGION = [
|
|
26
|
+
'openwop.idempotency.cross_region_conflicts_total',
|
|
27
|
+
];
|
|
17
28
|
|
|
18
29
|
interface IdempotencyCaps {
|
|
19
30
|
supported?: boolean;
|
|
@@ -22,6 +33,10 @@ interface IdempotencyCaps {
|
|
|
22
33
|
crossRegion?: string;
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
interface ObservabilityCaps {
|
|
37
|
+
metrics?: { names?: string[] };
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
describe('multi-region-idempotency: capability shape', () => {
|
|
26
41
|
it('idempotency.crossRegion (when advertised) MUST be one of the closed enum', async () => {
|
|
27
42
|
const disco = await driver.get('/.well-known/openwop');
|
|
@@ -49,4 +64,24 @@ describe('multi-region-idempotency: capability shape', () => {
|
|
|
49
64
|
expect(idem.layer2RetentionSeconds).toBeGreaterThan(0);
|
|
50
65
|
}
|
|
51
66
|
});
|
|
67
|
+
|
|
68
|
+
it('multi-region hosts SHOULD expose the cross-region conflict counter per §"Operator surface"', async () => {
|
|
69
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
70
|
+
const caps = (disco.json as { capabilities?: { idempotency?: IdempotencyCaps; observability?: ObservabilityCaps } })
|
|
71
|
+
.capabilities;
|
|
72
|
+
const crossRegion = caps?.idempotency?.crossRegion;
|
|
73
|
+
|
|
74
|
+
if (crossRegion !== 'best-effort' && crossRegion !== 'strict') {
|
|
75
|
+
// Single-region hosts have no conflicts to count — skip.
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const advertised = new Set(caps?.observability?.metrics?.names ?? []);
|
|
80
|
+
for (const name of REQUIRED_METRICS_WHEN_MULTI_REGION) {
|
|
81
|
+
expect(advertised.has(name), driver.describe(
|
|
82
|
+
'idempotency.md §"Operator surface"',
|
|
83
|
+
`multi-region hosts SHOULD advertise metric "${name}" so operators can monitor conflict frequency`,
|
|
84
|
+
)).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
52
87
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Multi-Agent Shift Phase 5 — CP-1 conservative-path orchestrator suspend.
|
|
3
|
+
* Normative reference: RFCS/0006-orchestrator.md
|
|
3
4
|
*
|
|
4
5
|
* Verifies the CP-1 invariant: when a `core.orchestrator.supervisor`
|
|
5
6
|
* would emit a decision with `confidence < escalationThreshold`, the
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Multi-Agent Shift Phase 5 — orchestrator → dispatch → next-worker round-trip.
|
|
3
|
+
* Normative reference: RFCS/0006-orchestrator.md
|
|
3
4
|
*
|
|
4
5
|
* Verifies that a workflow with `core.orchestrator.supervisor` →
|
|
5
6
|
* `core.dispatch` topology emits the canonical event sequence:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track 11: OTel span emission over OTLP/gRPC.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that hosts advertising `capabilities.observability.otel.exportProtocols`
|
|
5
|
+
* including `"grpc"` can emit `openwop.*` spans over OTLP/gRPC to the
|
|
6
|
+
* in-suite collector and that the gRPC framing path captures the same
|
|
7
|
+
* `openwop.run` + `openwop.node.*` shape as the HTTP-JSON path.
|
|
8
|
+
*
|
|
9
|
+
* The gRPC collector is the parallel HTTP/2 server inside
|
|
10
|
+
* `OtelCollector` (started via `startGrpc()` in setup.ts when the
|
|
11
|
+
* `OPENWOP_OTEL_COLLECTOR=true` flag is set). The host points its
|
|
12
|
+
* exporter at the printed `:<grpcPort>` (h2c) with
|
|
13
|
+
* `OTEL_EXPORTER_OTLP_PROTOCOL=grpc`. Spans captured over gRPC land
|
|
14
|
+
* in the same store as HTTP — `getCollector().spans()` returns the
|
|
15
|
+
* union.
|
|
16
|
+
*
|
|
17
|
+
* Skip conditions:
|
|
18
|
+
* - Collector disabled (`OPENWOP_OTEL_COLLECTOR` unset / false).
|
|
19
|
+
* - Host does not advertise `capabilities.observability.otel.exportProtocols`
|
|
20
|
+
* including `"grpc"` (presumed not configured for gRPC emission).
|
|
21
|
+
* - Required fixture (`conformance-noop`) not advertised.
|
|
22
|
+
*
|
|
23
|
+
* @see spec/v1/observability.md §"Export protocols"
|
|
24
|
+
* @see conformance/src/lib/otel-collector.ts §_handleGrpcStream
|
|
25
|
+
* @see conformance/src/lib/grpc-framing.ts
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { describe, it, expect } from 'vitest';
|
|
29
|
+
import { driver } from '../lib/driver.js';
|
|
30
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
31
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
32
|
+
import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
|
|
33
|
+
|
|
34
|
+
const FIXTURE = 'conformance-noop';
|
|
35
|
+
|
|
36
|
+
async function advertisesGrpcExport(): Promise<boolean> {
|
|
37
|
+
try {
|
|
38
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
39
|
+
const caps = (disco.json as {
|
|
40
|
+
capabilities?: {
|
|
41
|
+
observability?: { otel?: { exportProtocols?: unknown } };
|
|
42
|
+
};
|
|
43
|
+
}).capabilities;
|
|
44
|
+
const protocols = caps?.observability?.otel?.exportProtocols;
|
|
45
|
+
return Array.isArray(protocols) && protocols.includes('grpc');
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('otel-emission-grpc: OTLP/gRPC export path', () => {
|
|
52
|
+
it('host emits openwop.run spans over OTLP/gRPC; collector captures them via the shared store', async () => {
|
|
53
|
+
if (!getCollector()) {
|
|
54
|
+
// eslint-disable-next-line no-console
|
|
55
|
+
console.warn('[otel-emission-grpc] collector not started; set OPENWOP_OTEL_COLLECTOR=true to run');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!isFixtureAdvertised(FIXTURE)) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.warn(`[otel-emission-grpc] fixture ${FIXTURE} not advertised; skipping`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!(await advertisesGrpcExport())) {
|
|
64
|
+
// eslint-disable-next-line no-console
|
|
65
|
+
console.warn(
|
|
66
|
+
'[otel-emission-grpc] host does not advertise capabilities.observability.otel.exportProtocols including "grpc"; skipping. ' +
|
|
67
|
+
'Hosts MAY opt into gRPC export by emitting OTLP via the OTLP/gRPC transport and adding `"grpc"` to the array.',
|
|
68
|
+
);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const collector = getCollector()!;
|
|
73
|
+
collector.reset();
|
|
74
|
+
|
|
75
|
+
const create = await driver.post('/v1/runs', { workflowId: FIXTURE });
|
|
76
|
+
expect(create.status).toBe(201);
|
|
77
|
+
const runId = (create.json as { runId: string }).runId;
|
|
78
|
+
|
|
79
|
+
await pollUntilTerminal(runId, { timeoutMs: 15_000 });
|
|
80
|
+
|
|
81
|
+
// gRPC and HTTP-JSON spans both land in `_spans`, so the existing
|
|
82
|
+
// span-query helpers work transparently. If the host emits over
|
|
83
|
+
// both transports, we capture both; the assertion only requires
|
|
84
|
+
// at least one openwop.run span correlated by runId.
|
|
85
|
+
const runSpans = await waitForRunSpans(runId, { timeoutMs: 5_000, minCount: 1 });
|
|
86
|
+
|
|
87
|
+
expect(runSpans.length, driver.describe(
|
|
88
|
+
'observability.md §"Export protocols" + RFC 0008/0009 Track 11',
|
|
89
|
+
'host advertising exportProtocols ∋ "grpc" MUST emit openwop.* spans over OTLP/gRPC',
|
|
90
|
+
)).toBeGreaterThan(0);
|
|
91
|
+
|
|
92
|
+
const runSpan = runSpans.find((s) => s.name === 'openwop.run');
|
|
93
|
+
expect(runSpan?.attributes.get('openwop.run_id'), driver.describe(
|
|
94
|
+
'observability.md §"Run-level attributes"',
|
|
95
|
+
'openwop.run span MUST carry openwop.run_id attribute',
|
|
96
|
+
)).toBe(runId);
|
|
97
|
+
});
|
|
98
|
+
});
|