@openwop/openwop-conformance 1.14.0 → 1.16.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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Budget enforcement — the §C lifecycle + §D hard-stop (RFC 0084) — behavioral.
3
+ *
4
+ * Gated on `capabilities.budget.supported` (root-first per RFC 0073). Soft-skips
5
+ * when unadvertised (default) / hard-fails under `OPENWOP_REQUIRE_BEHAVIOR=true`.
6
+ * The always-on wire-shape coverage lives in `budget-policy-shape.test.ts`; this
7
+ * asserts host BEHAVIOR via the `POST /v1/host/sample/budget/run` seam + the test
8
+ * event-log seam:
9
+ *
10
+ * 1. HARD COST EXHAUST (§C/§D, requires `enforce:"hard"`) — a hard-cost run
11
+ * accrues to exhaustion, emitting in strict sequence:
12
+ * `budget.reserved` → `budget.consumed` → `budget.threshold.crossed{percent}`
13
+ * → `budget.exhausted` → `cap.breached{kind:"budget-cost"}` →
14
+ * `run.failed{error:"budget_exhausted"}`.
15
+ * 2. MODEL DENIED (§D model policy) — a run whose model violates the budget
16
+ * allow/deny list is refused with `budget_model_denied` BEFORE the provider
17
+ * call (no model call, fail-closed).
18
+ * 3. ADVISORY (§D, `enforce:"advisory"`) — the same accrual emits the
19
+ * `budget.*` events but does NOT stop the run (no `cap.breached`, no
20
+ * `run.failed{budget_exhausted}`).
21
+ * 4. CONTENT-FREE (SR-1 / `budget-no-pricing-leak`) — every `budget.*` payload
22
+ * carries only dimension/limit/consumed/remaining/percent scalars, never a
23
+ * provider pricing table or per-token rate.
24
+ *
25
+ * Spec references:
26
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/budget-policy.md (§C/§D)
27
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0084-budget-quota-and-cost-policy.md
28
+ * - https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml (budget-no-pricing-leak)
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { driver } from '../lib/driver.js';
33
+ import { behaviorGate } from '../lib/behavior-gate.js';
34
+ import { readBudgetCap, driveBudgetRun, BUDGET_CAP_KINDS, BUDGET_CONTENT_FORBIDDEN } from '../lib/budgetPolicy.js';
35
+ import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
36
+ import type { TestEvent } from '../lib/event-log-query.js';
37
+
38
+ function seq(events: TestEvent[], type: string): number {
39
+ const e = events.find((x) => x.type === type);
40
+ return e ? e.sequence : -1;
41
+ }
42
+
43
+ function expectContentFree(events: TestEvent[]): void {
44
+ for (const e of events.filter((x) => x.type.startsWith('budget.'))) {
45
+ for (const f of BUDGET_CONTENT_FORBIDDEN) {
46
+ expect(
47
+ !(f in e.payload),
48
+ driver.describe('RFC 0084 §F (SR-1) / budget-no-pricing-leak', `budget.* MUST be content-free (no ${f})`),
49
+ ).toBe(true);
50
+ }
51
+ }
52
+ }
53
+
54
+ describe('budget-enforcement (RFC 0084 §C/§D)', () => {
55
+ it('runs the reserved→consumed→threshold→exhausted→cap.breached→run.failed chain, refuses denied models, and honors advisory mode', async () => {
56
+ const cap = await readBudgetCap();
57
+ if (!behaviorGate('openwop-budget-enforcement', cap?.supported === true)) return;
58
+ if (!(await isEventLogSeamAvailable())) return; // event-log seam absent — soft-skip
59
+
60
+ // ---- Leg 1: hard cost exhaust (§C/§D) -------------------------------
61
+ const hard = await driveBudgetRun({ scenario: 'hard-cost-exhaust' });
62
+ if (hard === null) return; // budget seam absent — soft-skip the whole behavior
63
+ if (hard.runId) {
64
+ const q = await queryTestEvents(hard.runId);
65
+ if (q.ok) {
66
+ const ev = q.events.slice().sort((a, b) => a.sequence - b.sequence);
67
+ const reserved = seq(ev, 'budget.reserved');
68
+ const threshold = seq(ev, 'budget.threshold.crossed');
69
+ const exhausted = seq(ev, 'budget.exhausted');
70
+ const failed = seq(ev, 'run.failed');
71
+ const capBreached = ev.find((e) => e.type === 'cap.breached' && typeof e.payload.kind === 'string' && (e.payload.kind as string).startsWith('budget-'));
72
+
73
+ expect(
74
+ reserved >= 0 && exhausted >= 0,
75
+ driver.describe('budget-policy.md §C', 'a hard budget run MUST emit budget.reserved + budget.exhausted'),
76
+ ).toBe(true);
77
+ // §C ordering: reserved < threshold.crossed < exhausted < run.failed.
78
+ if (threshold >= 0) {
79
+ expect(
80
+ reserved < threshold && threshold < exhausted,
81
+ driver.describe('RFC 0084 §C', 'ordering MUST be reserved < threshold.crossed < exhausted'),
82
+ ).toBe(true);
83
+ const tc = ev.find((e) => e.type === 'budget.threshold.crossed');
84
+ expect(
85
+ typeof tc?.payload.percent === 'number',
86
+ driver.describe('run-event-payloads.schema.json#budgetThresholdCrossed', 'threshold.crossed MUST carry a numeric percent'),
87
+ ).toBe(true);
88
+ }
89
+ // §D hard-stop: exhausted → cap.breached{budget-*} → run.failed{budget_exhausted}.
90
+ expect(
91
+ capBreached !== undefined,
92
+ driver.describe('RFC 0084 §D', 'exhaustion MUST emit cap.breached with a budget-* kind'),
93
+ ).toBe(true);
94
+ if (capBreached) {
95
+ expect(
96
+ BUDGET_CAP_KINDS.includes(capBreached.payload.kind as string),
97
+ driver.describe('RFC 0084 §D', 'cap.breached.kind MUST be in the closed budget vocabulary'),
98
+ ).toBe(true);
99
+ expect(
100
+ exhausted <= capBreached.sequence && capBreached.sequence <= failed,
101
+ driver.describe('RFC 0084 §D', 'ordering MUST be exhausted ≤ cap.breached ≤ run.failed'),
102
+ ).toBe(true);
103
+ }
104
+ const failedEvt = ev.find((e) => e.type === 'run.failed');
105
+ expect(
106
+ failedEvt?.payload.error === 'budget_exhausted',
107
+ driver.describe('RFC 0084 §D', 'a hard-budget overrun MUST fail the run with error budget_exhausted'),
108
+ ).toBe(true);
109
+ expectContentFree(ev);
110
+ }
111
+ }
112
+
113
+ // ---- Leg 2: model denied (§D model policy, fail-closed) -------------
114
+ const denied = await driveBudgetRun({ scenario: 'model-denied' });
115
+ if (denied !== null) {
116
+ expect(
117
+ denied.error === 'budget_model_denied',
118
+ driver.describe('RFC 0084 §D', 'a model violating the budget allow/deny list MUST be refused with budget_model_denied'),
119
+ ).toBe(true);
120
+ expect(
121
+ denied.modelCalled !== true,
122
+ driver.describe('RFC 0084 §D', 'a denied model MUST be refused BEFORE the provider call (fail-closed)'),
123
+ ).toBe(true);
124
+ }
125
+
126
+ // ---- Leg 3: advisory mode emits events but never stops --------------
127
+ if (cap?.enforce === 'advisory' || cap?.enforce === undefined) {
128
+ const adv = await driveBudgetRun({ scenario: 'advisory' });
129
+ if (adv !== null && adv.runId) {
130
+ const q = await queryTestEvents(adv.runId);
131
+ if (q.ok) {
132
+ const ev = q.events;
133
+ const hasBudgetEvents = ev.some((e) => e.type.startsWith('budget.'));
134
+ const stopped = ev.some(
135
+ (e) =>
136
+ (e.type === 'cap.breached' && typeof e.payload.kind === 'string' && (e.payload.kind as string).startsWith('budget-')) ||
137
+ (e.type === 'run.failed' && e.payload.error === 'budget_exhausted'),
138
+ );
139
+ if (hasBudgetEvents) {
140
+ expect(
141
+ !stopped,
142
+ driver.describe('RFC 0084 §D', 'advisory enforcement MUST emit budget.* events without stopping the run'),
143
+ ).toBe(true);
144
+ }
145
+ expectContentFree(ev);
146
+ }
147
+ }
148
+ }
149
+
150
+ await resetTestSeam();
151
+ });
152
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Memory-capability degraded projection (RFC 0080 §C) — behavioral.
3
+ *
4
+ * Gated on `capabilities.agents.manifestRuntime` + `capabilities.memory`
5
+ * (root-first per RFC 0073). Soft-skips when either is unadvertised (default) /
6
+ * hard-fails under `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape
7
+ * coverage lives in `memory-capability-model-shape.test.ts` (the schema fields +
8
+ * the closed dimension enum); this asserts host BEHAVIOR on the NORMATIVE
9
+ * `GET /v1/agents` inventory:
10
+ *
11
+ * §C iff-contract — for EVERY inventory entry, when the host cannot satisfy an
12
+ * agent's requested `memoryShape` it MUST stamp `memoryDegraded: true` together
13
+ * with a NON-EMPTY `degradedMemoryDimensions[]` whose members are the RFC 0080
14
+ * §A dimension names (the CLOSED enum, NOT the `memoryShape` keys) and are
15
+ * unique; a non-degraded entry MUST carry `memoryDegraded` absent or `false`
16
+ * and MUST NOT carry a non-empty `degradedMemoryDimensions`.
17
+ *
18
+ * Non-vacuity — the inventory MUST be non-empty (the cap is advertised + the
19
+ * endpoint serves). When `OPENWOP_DEGRADED_AGENT_ID` names an agent the host
20
+ * knows is degraded (an agent whose `memoryShape` exceeds host capability —
21
+ * e.g. one requesting `longTerm` on a host without long-term durability), the
22
+ * degraded branch is asserted NON-VACUOUSLY against that agent.
23
+ *
24
+ * Black-box on the normative path — no POST seam.
25
+ *
26
+ * Spec references:
27
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md (§"Memory capability model")
28
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0080-agent-memory-capability-reconciliation.md
29
+ */
30
+
31
+ import { describe, it, expect } from 'vitest';
32
+ import { driver } from '../lib/driver.js';
33
+ import { behaviorGate } from '../lib/behavior-gate.js';
34
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
35
+ import { readManifestRuntimeCap, listManifestAgents } from '../lib/agentRuntime.js';
36
+
37
+ /** The CLOSED RFC 0080 §A dimension vocabulary (agent-inventory-response.schema.json
38
+ * `degradedMemoryDimensions` enum). NOT the `memoryShape` keys. */
39
+ const DIMENSIONS = [
40
+ 'read',
41
+ 'write',
42
+ 'search',
43
+ 'long-term-durability',
44
+ 'compaction',
45
+ 'attribution',
46
+ 'replay-snapshot',
47
+ 'retention',
48
+ ];
49
+
50
+ interface InventoryEntry {
51
+ agentId?: string;
52
+ memoryDegraded?: unknown;
53
+ degradedMemoryDimensions?: unknown;
54
+ [k: string]: unknown;
55
+ }
56
+
57
+ describe('memory-degraded-projection (RFC 0080 §C)', () => {
58
+ it('stamps memoryDegraded + a closed-enum degradedMemoryDimensions on degraded agents and nothing on the rest', async () => {
59
+ const mr = await readManifestRuntimeCap();
60
+ const memory = await readCapabilityFamily<Record<string, unknown>>('memory');
61
+ const advertised = mr?.supported === true && !!memory && memory.supported === true;
62
+ if (!behaviorGate('openwop-memory-degraded', advertised)) return;
63
+
64
+ const inv = await listManifestAgents();
65
+ if (inv === null) return; // host advertises the cap but doesn't serve /v1/agents — soft-skip
66
+ const agents = (inv.agents ?? []) as InventoryEntry[];
67
+
68
+ // Non-vacuity: an advertising + serving host MUST expose its inventory.
69
+ expect(
70
+ agents.length >= 1,
71
+ driver.describe('agent-memory.md §"Memory capability model"', 'GET /v1/agents MUST return the installed manifest agents'),
72
+ ).toBe(true);
73
+
74
+ // §C iff-contract on EVERY entry.
75
+ for (const a of agents) {
76
+ const degraded = a.memoryDegraded === true;
77
+ const dims = a.degradedMemoryDimensions;
78
+
79
+ if (degraded) {
80
+ expect(
81
+ Array.isArray(dims) && dims.length >= 1,
82
+ driver.describe('RFC 0080 §C', `memoryDegraded:true MUST carry a non-empty degradedMemoryDimensions (agent ${a.agentId})`),
83
+ ).toBe(true);
84
+ if (Array.isArray(dims)) {
85
+ for (const d of dims) {
86
+ expect(
87
+ typeof d === 'string' && DIMENSIONS.includes(d),
88
+ driver.describe('agent-inventory-response.schema.json', `degradedMemoryDimensions members MUST be RFC 0080 §A dimension names (got ${String(d)})`),
89
+ ).toBe(true);
90
+ }
91
+ expect(
92
+ new Set(dims as string[]).size === dims.length,
93
+ driver.describe('RFC 0080 §C', 'degradedMemoryDimensions MUST be unique'),
94
+ ).toBe(true);
95
+ }
96
+ } else {
97
+ // Not degraded ⇒ no non-empty dimension list (absent or empty both pass).
98
+ expect(
99
+ dims === undefined || (Array.isArray(dims) && dims.length === 0),
100
+ driver.describe('RFC 0080 §C', `a non-degraded entry MUST NOT carry a non-empty degradedMemoryDimensions (agent ${a.agentId})`),
101
+ ).toBe(true);
102
+ }
103
+ }
104
+
105
+ // Non-vacuous degraded branch when the host names a known-degraded agent.
106
+ const degradedId = process.env.OPENWOP_DEGRADED_AGENT_ID;
107
+ if (degradedId) {
108
+ const target = agents.find((a) => a.agentId === degradedId);
109
+ expect(
110
+ target !== undefined,
111
+ driver.describe('RFC 0080 §C', `OPENWOP_DEGRADED_AGENT_ID=${degradedId} MUST appear in the inventory`),
112
+ ).toBe(true);
113
+ if (target) {
114
+ expect(
115
+ target.memoryDegraded === true && Array.isArray(target.degradedMemoryDimensions) && target.degradedMemoryDimensions.length >= 1,
116
+ driver.describe('RFC 0080 §C', 'the named degraded agent MUST project memoryDegraded:true + a non-empty degradedMemoryDimensions'),
117
+ ).toBe(true);
118
+ }
119
+ }
120
+ });
121
+ });
@@ -0,0 +1,261 @@
1
+ /**
2
+ * otel-collector-canary-inspection — always-on proof that the conformance
3
+ * OTel collector inspects real OTLP span attributes for secret leakage.
4
+ *
5
+ * Context: `secret-leakage-otel-attribute.test.ts` proves a host doesn't
6
+ * leak a BYOK canary on its `GET /v1/host/sample/test/otel/spans` scrape
7
+ * seam. But the scrape seam reports what the host *says* it emitted; a
8
+ * host could redact there yet still ship the plaintext over the wire via
9
+ * its real OTLP exporter. `docs/KNOWN-LIMITS.md` tracked exactly this gap:
10
+ *
11
+ * "The conformance OTel collector seam doesn't yet inspect span
12
+ * attributes; a host could pass conformance while leaking BYOK
13
+ * material on telemetry exports."
14
+ *
15
+ * `OtelCollector.findCanaryLeakage()` (added with this scenario) closes
16
+ * the harness half: it scans every captured span's name, attributes, and
17
+ * resource attributes — plus metric data-point attributes — for the
18
+ * canary, returning the offending surface. This file is the always-on,
19
+ * server-free proof that the inspector is NON-VACUOUS: it stands up a
20
+ * real collector, POSTs synthetic OTLP/HTTP-JSON payloads through the
21
+ * collector's actual ingest path, and asserts the inspector (a) catches a
22
+ * canary embedded in a span attribute / resource attribute / span name /
23
+ * metric attribute, and (b) reports zero hits on a redacted payload.
24
+ *
25
+ * Pairing this with the live, capability-gated collector scrape in
26
+ * `secret-leakage-otel-attribute.test.ts` means the invariant is proven
27
+ * end-to-end the moment a host exports OTLP to the collector — the
28
+ * inspector itself is no longer the missing piece.
29
+ *
30
+ * Runs unconditionally (no host, no network): it exercises the collector
31
+ * library directly, so it executes in CI on every run.
32
+ *
33
+ * @see conformance/src/lib/otel-collector.ts findCanaryLeakage
34
+ * @see SECURITY/invariants.yaml secret-leakage-otel-attribute
35
+ * @see docs/KNOWN-LIMITS.md "Behavior tests too coarse to fully prove an invariant"
36
+ */
37
+
38
+ import { describe, it, expect, afterEach } from 'vitest';
39
+ import { OtelCollector } from '../lib/otel-collector.js';
40
+
41
+ const CANARY = 'sk-canary-DO-NOT-LEAK-0f3a9c';
42
+ const REDACTED = '[REDACTED:openwop-conformance-canary-secret]';
43
+
44
+ /** Build an OTLP/HTTP-JSON traces export carrying the given span+resource attrs. */
45
+ function tracesPayload(opts: {
46
+ spanName: string;
47
+ spanAttrs: Record<string, string>;
48
+ resourceAttrs: Record<string, string>;
49
+ }): unknown {
50
+ const toAttrs = (m: Record<string, string>) =>
51
+ Object.entries(m).map(([key, value]) => ({ key, value: { stringValue: value } }));
52
+ return {
53
+ resourceSpans: [
54
+ {
55
+ resource: { attributes: toAttrs(opts.resourceAttrs) },
56
+ scopeSpans: [
57
+ {
58
+ scope: { name: 'openwop' },
59
+ spans: [
60
+ {
61
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
62
+ spanId: 'bbbbbbbbbbbbbbbb',
63
+ name: opts.spanName,
64
+ startTimeUnixNano: '1',
65
+ endTimeUnixNano: '2',
66
+ attributes: toAttrs(opts.spanAttrs),
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ },
72
+ ],
73
+ };
74
+ }
75
+
76
+ /** Build an OTLP/HTTP-JSON metrics export with one sum data point carrying attrs. */
77
+ function metricsPayload(metricName: string, attrs: Record<string, string>): unknown {
78
+ return {
79
+ resourceMetrics: [
80
+ {
81
+ scopeMetrics: [
82
+ {
83
+ scope: { name: 'openwop' },
84
+ metrics: [
85
+ {
86
+ name: metricName,
87
+ sum: {
88
+ dataPoints: [
89
+ {
90
+ asInt: '1',
91
+ attributes: Object.entries(attrs).map(([key, value]) => ({
92
+ key,
93
+ value: { stringValue: value },
94
+ })),
95
+ },
96
+ ],
97
+ },
98
+ },
99
+ ],
100
+ },
101
+ ],
102
+ },
103
+ ],
104
+ };
105
+ }
106
+
107
+ // NOTE: assertions here intentionally use bare `expect(...)` rather than
108
+ // `expect(..., driver.describe('spec.md §section', 'requirement'))`. This is a
109
+ // HARNESS self-test — it verifies the conformance collector's own
110
+ // `findCanaryLeakage()` inspector, not a host's compliance with a spec
111
+ // requirement, so there is no spec section to cite (consistent with other
112
+ // library-level tests, e.g. `sandbox-wasm-isolation.test.ts`). The
113
+ // host-facing, spec-citing assertion lives in the collector-export block of
114
+ // `secret-leakage-otel-attribute.test.ts`.
115
+ /**
116
+ * Build a traces export with `spanCount` spans that all share ONE resource
117
+ * (hence one set of resource attributes). Used to prove resource-attribute
118
+ * leaks are deduped to a single hit rather than reported once per span.
119
+ */
120
+ function multiSpanSharedResourcePayload(spanCount: number, resourceAttrs: Record<string, string>): unknown {
121
+ const toAttrs = (m: Record<string, string>) =>
122
+ Object.entries(m).map(([key, value]) => ({ key, value: { stringValue: value } }));
123
+ return {
124
+ resourceSpans: [
125
+ {
126
+ resource: { attributes: toAttrs(resourceAttrs) },
127
+ scopeSpans: [
128
+ {
129
+ scope: { name: 'openwop' },
130
+ spans: Array.from({ length: spanCount }, (_unused, i) => ({
131
+ traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
132
+ spanId: `span${i}`.padEnd(16, '0'),
133
+ name: `openwop.node.execute.${i}`,
134
+ startTimeUnixNano: '1',
135
+ endTimeUnixNano: '2',
136
+ attributes: toAttrs({ 'openwop.node.id': `n${i}` }),
137
+ })),
138
+ },
139
+ ],
140
+ },
141
+ ],
142
+ };
143
+ }
144
+
145
+ describe('otel-collector-canary-inspection: collector inspects real OTLP exports', () => {
146
+ let collector: OtelCollector | null = null;
147
+
148
+ afterEach(async () => {
149
+ if (collector) {
150
+ await collector.stop();
151
+ collector = null;
152
+ }
153
+ });
154
+
155
+ async function postTraces(payload: unknown): Promise<void> {
156
+ const res = await fetch(`${collector!.endpoint()}/v1/traces`, {
157
+ method: 'POST',
158
+ headers: { 'content-type': 'application/json' },
159
+ body: JSON.stringify(payload),
160
+ });
161
+ expect(res.status).toBeLessThan(300);
162
+ }
163
+
164
+ async function postMetrics(payload: unknown): Promise<void> {
165
+ const res = await fetch(`${collector!.endpoint()}/v1/metrics`, {
166
+ method: 'POST',
167
+ headers: { 'content-type': 'application/json' },
168
+ body: JSON.stringify(payload),
169
+ });
170
+ expect(res.status).toBeLessThan(300);
171
+ }
172
+
173
+ it('catches a canary embedded in a span attribute value', async () => {
174
+ collector = new OtelCollector();
175
+ await collector.start();
176
+ await postTraces(
177
+ tracesPayload({
178
+ spanName: 'openwop.node.execute',
179
+ spanAttrs: { 'openwop.node.id': 'n1', 'http.request.header.authorization': `Bearer ${CANARY}` },
180
+ resourceAttrs: { 'service.name': 'host' },
181
+ }),
182
+ );
183
+
184
+ const leaks = collector.findCanaryLeakage(CANARY);
185
+ expect(leaks.length).toBeGreaterThan(0);
186
+ const attrLeak = leaks.find((l) => l.surface === 'span.attribute');
187
+ expect(attrLeak).toBeDefined();
188
+ expect(attrLeak!.key).toBe('http.request.header.authorization');
189
+ expect(attrLeak!.value).toContain(CANARY);
190
+ });
191
+
192
+ it('catches a canary in a resource attribute and in a span name', async () => {
193
+ collector = new OtelCollector();
194
+ await collector.start();
195
+ await postTraces(
196
+ tracesPayload({
197
+ spanName: `openwop.run ${CANARY}`,
198
+ spanAttrs: { 'openwop.run.id': 'r1' },
199
+ resourceAttrs: { 'service.name': 'host', 'deployment.token': CANARY },
200
+ }),
201
+ );
202
+
203
+ const leaks = collector.findCanaryLeakage(CANARY);
204
+ const surfaces = new Set(leaks.map((l) => l.surface));
205
+ expect(surfaces.has('span.name')).toBe(true);
206
+ expect(surfaces.has('span.resourceAttribute')).toBe(true);
207
+ });
208
+
209
+ it('catches a canary in a metric data-point attribute', async () => {
210
+ collector = new OtelCollector();
211
+ await collector.start();
212
+ await postMetrics(metricsPayload('openwop.node.duration', { 'secret.echo': CANARY }));
213
+
214
+ const leaks = collector.findCanaryLeakage(CANARY);
215
+ const metricLeak = leaks.find((l) => l.surface === 'metric.attribute');
216
+ expect(metricLeak).toBeDefined();
217
+ expect(metricLeak!.emitterName).toBe('openwop.node.duration');
218
+ });
219
+
220
+ it('dedups a resource-attribute leak to ONE hit even when shared across many spans', async () => {
221
+ collector = new OtelCollector();
222
+ await collector.start();
223
+ // 5 spans sharing one resource whose attribute leaks the canary. Without
224
+ // dedup this would report 5 identical resource-attribute hits.
225
+ await postTraces(multiSpanSharedResourcePayload(5, { 'service.name': 'host', 'deployment.token': CANARY }));
226
+
227
+ const leaks = collector.findCanaryLeakage(CANARY);
228
+ const resourceLeaks = leaks.filter((l) => l.surface === 'span.resourceAttribute' && l.key === 'deployment.token');
229
+ expect(resourceLeaks.length).toBe(1);
230
+ });
231
+
232
+ it('reports ZERO hits when the host redacts the canary before export (positive control)', async () => {
233
+ collector = new OtelCollector();
234
+ await collector.start();
235
+ await postTraces(
236
+ tracesPayload({
237
+ spanName: 'openwop.node.execute',
238
+ spanAttrs: { 'openwop.node.id': 'n1', 'http.request.header.authorization': `Bearer ${REDACTED}` },
239
+ resourceAttrs: { 'service.name': 'host', 'deployment.token': REDACTED },
240
+ }),
241
+ );
242
+ await postMetrics(metricsPayload('openwop.node.duration', { 'secret.echo': REDACTED }));
243
+
244
+ expect(collector.findCanaryLeakage(CANARY)).toEqual([]);
245
+ });
246
+
247
+ it('an empty or whitespace canary never produces a (vacuous) hit', async () => {
248
+ collector = new OtelCollector();
249
+ await collector.start();
250
+ await postTraces(
251
+ tracesPayload({
252
+ spanName: 'openwop.node.execute',
253
+ spanAttrs: { 'a': 'b' },
254
+ resourceAttrs: { 'service.name': 'host' },
255
+ }),
256
+ );
257
+
258
+ expect(collector.findCanaryLeakage('')).toEqual([]);
259
+ expect(collector.findCanaryLeakage(' ')).toEqual([]);
260
+ });
261
+ });