@xaccefy/pi-casefile 0.9.0 → 0.9.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.
@@ -0,0 +1,693 @@
1
+ /**
2
+ * Harness-side target/control replay — Tier 2 of docs/poc-trust-model.md.
3
+ *
4
+ * The machine floor cannot trust a worker's self-reported `re_executed`
5
+ * boolean. This module makes the HARNESS re-send the evidence's `verify`
6
+ * request with its own HTTP client and apply the same `expect` predicates
7
+ * (status / body_contains / body_regex) to target and control responses. The
8
+ * worker supplies the predicate; the harness owns both evidence acquisition
9
+ * and predicate execution. The main agent performs the later semantic review.
10
+ *
11
+ * Policy:
12
+ * - Private/internal hosts require explicit operator authorization; otherwise
13
+ * replay fails closed.
14
+ * - Redirects are manual and every hop is checked before it is fetched.
15
+ * - Target must match while the identical control request must not.
16
+ *
17
+ * Undici's custom dispatcher pins the approved DNS result through connect;
18
+ * node:dns and node:net provide resolution and address classification.
19
+ */
20
+
21
+ import { createHash, randomBytes } from "node:crypto";
22
+ import { lookup as dnsLookup } from "node:dns/promises";
23
+ import { isIP } from "node:net";
24
+ import { Worker } from "node:worker_threads";
25
+ import { isPublicIpAddress } from "@xaccefy/pi-shared";
26
+ import { Agent, fetch as undiciFetch } from "undici";
27
+ import { POC_CANARY_PLACEHOLDER, type PoCEvidence, type VerifyExpect } from "./evidence.ts";
28
+
29
+ // Public re-export makes the single-source classifier identity testable across
30
+ // the web tool and confirmation replay paths.
31
+ export { isPublicIpAddress } from "@xaccefy/pi-shared";
32
+
33
+ export type HarnessVerifyResult = {
34
+ /** true = the harness sent both target and control requests and judged them. */
35
+ attempted: boolean;
36
+ /** Present when attempted: target matched and control did not. */
37
+ pass?: boolean;
38
+ /** Backward-compatible target status summary. */
39
+ status?: number;
40
+ /** Machine-observed target/control response summaries. */
41
+ target?: HarnessResponseObservation;
42
+ control?: HarnessResponseObservation;
43
+ differential?: "target_only" | "both" | "control_only" | "neither";
44
+ /** Independent harness-generated reflection signal, when the template supports one. */
45
+ canary?: HarnessCanaryResult;
46
+ /** Honest machine claim: predicates alone, or predicates plus a causal canary. */
47
+ proofStrength?: "predicate_differential" | "canary_differential";
48
+ note: string;
49
+ };
50
+
51
+ export type HarnessCanaryResult = {
52
+ mode: "reflection";
53
+ attempted: boolean;
54
+ pass?: boolean;
55
+ tokenSha256: string;
56
+ targetObserved?: boolean;
57
+ controlObserved?: boolean;
58
+ note: string;
59
+ };
60
+
61
+ export type HarnessResponseObservation = {
62
+ attempted: boolean;
63
+ matched?: boolean;
64
+ status?: number;
65
+ url: string;
66
+ bodySha256?: string;
67
+ bodyBytes?: number;
68
+ canaryObserved?: boolean;
69
+ note: string;
70
+ };
71
+
72
+ const MAX_BODY_BYTES = 2 * 1024 * 1024;
73
+ const TIMEOUT_MS = 30_000;
74
+ const MAX_REDIRECTS = 5;
75
+ const REGEX_TIMEOUT_MS = 250;
76
+
77
+ type ResolvedAddress = { address: string; family: 4 | 6 };
78
+
79
+ async function resolveHost(hostname: string): Promise<ResolvedAddress[]> {
80
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
81
+ const literalFamily = isIP(host);
82
+ if (literalFamily) return [{ address: host, family: literalFamily as 4 | 6 }];
83
+ const lookup = dnsLookup(host, { all: true, verbatim: true }) as Promise<ResolvedAddress[]>;
84
+ let timer: ReturnType<typeof setTimeout> | undefined;
85
+ try {
86
+ return await Promise.race([
87
+ lookup,
88
+ new Promise<never>((_, reject) => {
89
+ timer = setTimeout(() => reject(new Error("DNS lookup timed out")), 5_000);
90
+ }),
91
+ ]);
92
+ } finally {
93
+ if (timer) clearTimeout(timer);
94
+ }
95
+ }
96
+
97
+ function parseNetworkTarget(target: string): { url: URL; explicitProtocol: boolean } | undefined {
98
+ const value = target.trim();
99
+ if (!value || /\s/.test(value)) return;
100
+ const explicitProtocol = /^https?:\/\//i.test(value);
101
+ try {
102
+ const url = new URL(explicitProtocol ? value : `http://${value}`);
103
+ if (!(url.protocol === "http:" || url.protocol === "https:") || !url.hostname) return;
104
+ if (url.username || url.password) return;
105
+ return { url, explicitProtocol };
106
+ } catch {
107
+ return;
108
+ }
109
+ }
110
+
111
+ function effectivePort(url: URL): string {
112
+ return url.port || (url.protocol === "https:" ? "443" : "80");
113
+ }
114
+
115
+ function sameTargetIdentity(left: string, right: string): boolean {
116
+ const a = parseNetworkTarget(left);
117
+ const b = parseNetworkTarget(right);
118
+ if (!a || !b) return false;
119
+ if (
120
+ a.url.hostname.toLowerCase().replace(/\.$/, "") !==
121
+ b.url.hostname.toLowerCase().replace(/\.$/, "")
122
+ ) {
123
+ return false;
124
+ }
125
+ if ((a.url.port || b.url.port) && effectivePort(a.url) !== effectivePort(b.url)) return false;
126
+ return !(a.explicitProtocol && b.explicitProtocol && a.url.protocol !== b.url.protocol);
127
+ }
128
+
129
+ /**
130
+ * A control is a trust anchor, not an agent invention. The operator supplies
131
+ * an allowlist of approved control origins/hosts through the process env.
132
+ */
133
+ export function controlTargetAuthorizationError(
134
+ controlTarget: string,
135
+ allowedRaw: string | undefined = process.env.PI_POC_CONTROL_TARGETS,
136
+ ): string | undefined {
137
+ const allowed = (allowedRaw ?? "")
138
+ .split(/[,\n]/)
139
+ .map((value) => value.trim())
140
+ .filter(Boolean);
141
+ if (allowed.length === 0) {
142
+ return "no operator-approved controls are configured in PI_POC_CONTROL_TARGETS";
143
+ }
144
+ if (!allowed.some((candidate) => sameTargetIdentity(candidate, controlTarget))) {
145
+ return `control target ${controlTarget} is not present in the operator-approved PI_POC_CONTROL_TARGETS allowlist`;
146
+ }
147
+ return;
148
+ }
149
+
150
+ /**
151
+ * Bind a model-authored verify URL to the target the harness actually ran.
152
+ * A bare target permits either HTTP scheme; an explicit target URL binds the
153
+ * scheme as well as hostname and effective port.
154
+ */
155
+ export function verifyUrlBindingError(verifyUrl: string, target: string): string | undefined {
156
+ const declared = parseNetworkTarget(target);
157
+ let observed: URL;
158
+ try {
159
+ observed = new URL(verifyUrl);
160
+ } catch {
161
+ return `verify.url is not parseable: ${verifyUrl}`;
162
+ }
163
+ if (!declared) return `target is not an HTTP network target: ${target}`;
164
+ const declaredHost = declared.url.hostname.toLowerCase().replace(/\.$/, "");
165
+ const observedHost = observed.hostname.toLowerCase().replace(/\.$/, "");
166
+ if (declaredHost !== observedHost) {
167
+ return `verify.url host ${observedHost} does not match run target ${declaredHost}`;
168
+ }
169
+ if (
170
+ (declared.url.port || observed.port) &&
171
+ effectivePort(declared.url) !== effectivePort(observed)
172
+ ) {
173
+ return `verify.url port ${effectivePort(observed)} does not match run target port ${effectivePort(declared.url)}`;
174
+ }
175
+ if (declared.explicitProtocol && declared.url.protocol !== observed.protocol) {
176
+ return `verify.url protocol ${observed.protocol} does not match run target protocol ${declared.url.protocol}`;
177
+ }
178
+ return;
179
+ }
180
+
181
+ function controlUrlFor(targetVerifyUrl: string, controlTarget: string): URL | undefined {
182
+ const control = parseNetworkTarget(controlTarget);
183
+ if (!control) return;
184
+ const target = new URL(targetVerifyUrl);
185
+ const url = new URL(control.url.origin);
186
+ url.pathname = target.pathname;
187
+ url.search = target.search;
188
+ return url;
189
+ }
190
+
191
+ // ── Predicate evaluation ──────────────────────────────────────────────
192
+
193
+ const REGEX_WORKER_SOURCE = `
194
+ const { parentPort, workerData } = require("node:worker_threads");
195
+ try {
196
+ const matches = workerData.patterns.map((pattern) => new RegExp(pattern).test(workerData.body));
197
+ parentPort.postMessage({ matches });
198
+ } catch (error) {
199
+ parentPort.postMessage({ error: error instanceof Error ? error.message : String(error) });
200
+ }
201
+ `;
202
+
203
+ async function evaluateRegexes(
204
+ patterns: string[],
205
+ body: string,
206
+ ): Promise<{ matches?: boolean[]; error?: string }> {
207
+ if (patterns.length === 0) return { matches: [] };
208
+ return new Promise((resolve) => {
209
+ const worker = new Worker(REGEX_WORKER_SOURCE, {
210
+ eval: true,
211
+ workerData: { patterns, body },
212
+ });
213
+ let settled = false;
214
+ const finish = (result: { matches?: boolean[]; error?: string }) => {
215
+ if (settled) return;
216
+ settled = true;
217
+ clearTimeout(timer);
218
+ void worker.terminate();
219
+ resolve(result);
220
+ };
221
+ const timer = setTimeout(
222
+ () => finish({ error: `evaluation exceeded ${REGEX_TIMEOUT_MS}ms` }),
223
+ REGEX_TIMEOUT_MS,
224
+ );
225
+ worker.once("message", (message) => finish(message));
226
+ worker.once("error", (error) =>
227
+ finish({ error: error instanceof Error ? error.message : String(error) }),
228
+ );
229
+ worker.once("exit", (code) => {
230
+ if (code !== 0) finish({ error: `worker exited with code ${code}` });
231
+ });
232
+ });
233
+ }
234
+
235
+ /** Apply an evidence expect spec to a harness-observed response. */
236
+ export async function evaluateExpect(
237
+ expect: VerifyExpect,
238
+ status: number,
239
+ body: string,
240
+ ): Promise<string[]> {
241
+ const failures: string[] = [];
242
+ if (expect.status && !expect.status.includes(status)) {
243
+ failures.push(`status ${status} not in [${expect.status.join(", ")}]`);
244
+ }
245
+ for (const needle of expect.body_contains ?? []) {
246
+ if (!body.includes(needle)) failures.push(`body_contains missing: ${needle}`);
247
+ }
248
+ const patterns = expect.body_regex ?? [];
249
+ const regex = await evaluateRegexes(patterns, body);
250
+ if (regex.error) {
251
+ failures.push(`body_regex evaluation failed: ${regex.error}`);
252
+ } else {
253
+ for (const [index, re] of patterns.entries()) {
254
+ if (regex.matches?.[index] !== true) failures.push(`body_regex failed: ${re}`);
255
+ }
256
+ }
257
+ return failures;
258
+ }
259
+
260
+ /** Read a response body with a hard byte cap (bounded memory, honest note). */
261
+ async function readBodyCapped(
262
+ res: Response,
263
+ ): Promise<{ text: string; truncated: boolean; bytes: number; sha256: string }> {
264
+ const reader = res.body?.getReader();
265
+ if (!reader) {
266
+ return {
267
+ text: "",
268
+ truncated: false,
269
+ bytes: 0,
270
+ sha256: createHash("sha256").update("").digest("hex"),
271
+ };
272
+ }
273
+ const chunks: Uint8Array[] = [];
274
+ let bytes = 0;
275
+ let truncated = false;
276
+ for (;;) {
277
+ const { done, value } = await reader.read();
278
+ if (done) break;
279
+ if (!value) continue;
280
+ const remaining = MAX_BODY_BYTES - bytes;
281
+ if (remaining > 0) {
282
+ const kept = value.byteLength > remaining ? value.subarray(0, remaining) : value;
283
+ chunks.push(kept);
284
+ bytes += kept.byteLength;
285
+ }
286
+ if (value.byteLength > remaining || bytes >= MAX_BODY_BYTES) {
287
+ truncated = true;
288
+ await reader.cancel().catch(() => undefined);
289
+ break;
290
+ }
291
+ }
292
+ const body = Buffer.concat(chunks).subarray(0, MAX_BODY_BYTES);
293
+ return {
294
+ text: new TextDecoder().decode(body),
295
+ truncated,
296
+ bytes: body.byteLength,
297
+ sha256: createHash("sha256").update(body).digest("hex"),
298
+ };
299
+ }
300
+
301
+ // ── Replay ────────────────────────────────────────────────────────────
302
+
303
+ type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
304
+ let harnessFetchForTest: FetchLike | undefined;
305
+
306
+ /** Test seam; production callers leave this undefined and use DNS-pinned undici. */
307
+ export function setHarnessFetchForTest(fetchImpl: FetchLike | undefined): void {
308
+ harnessFetchForTest = fetchImpl;
309
+ }
310
+
311
+ type ReplayOptions = {
312
+ timeoutMs?: number;
313
+ allowPrivate?: boolean;
314
+ /** Test-only injection; production always uses the DNS-pinned undici path. */
315
+ fetchImpl?: FetchLike;
316
+ };
317
+
318
+ async function fetchPinned(
319
+ url: URL,
320
+ init: RequestInit,
321
+ addresses: ResolvedAddress[],
322
+ ): Promise<{ response: Response; close: () => Promise<void> }> {
323
+ const first = addresses[0];
324
+ const pinnedLookup = (
325
+ _hostname: string,
326
+ options: { all?: boolean },
327
+ callback: (
328
+ error: NodeJS.ErrnoException | null,
329
+ address: string | ResolvedAddress[],
330
+ family?: 4 | 6,
331
+ ) => void,
332
+ ) => {
333
+ if (options?.all) callback(null, addresses);
334
+ else callback(null, first.address, first.family);
335
+ };
336
+ const agent = new Agent({
337
+ connect: { lookup: pinnedLookup as never },
338
+ });
339
+ try {
340
+ const response = (await undiciFetch(url, {
341
+ ...(init as object),
342
+ dispatcher: agent,
343
+ } as never)) as unknown as Response;
344
+ return { response, close: () => agent.close() };
345
+ } catch (error) {
346
+ await agent.close().catch(() => undefined);
347
+ throw error;
348
+ }
349
+ }
350
+
351
+ async function replayRequest(
352
+ verify: PoCEvidence["verify"],
353
+ expect: VerifyExpect,
354
+ canaryToken?: string,
355
+ opts?: ReplayOptions,
356
+ ): Promise<HarnessResponseObservation> {
357
+ let url: URL;
358
+ try {
359
+ url = new URL(verify.url);
360
+ } catch {
361
+ return {
362
+ attempted: false,
363
+ url: verify.url,
364
+ note: `verify.url unparseable (${verify.url})`,
365
+ };
366
+ }
367
+ const observedUrl = () =>
368
+ canaryToken ? url.toString().replaceAll(canaryToken, POC_CANARY_PLACEHOLDER) : url.toString();
369
+ if (!(url.protocol === "http:" || url.protocol === "https:")) {
370
+ return { attempted: false, url: verify.url, note: `verify.url protocol ${url.protocol}` };
371
+ }
372
+
373
+ let method = verify.method.toUpperCase();
374
+ let body = method === "GET" || method === "HEAD" ? undefined : verify.body;
375
+ let headers: Headers;
376
+ try {
377
+ headers = new Headers(verify.headers);
378
+ } catch (error) {
379
+ return {
380
+ attempted: false,
381
+ url: observedUrl(),
382
+ note: `verify.headers invalid: ${(error as Error).message}`,
383
+ };
384
+ }
385
+ const signal = AbortSignal.timeout(opts?.timeoutMs ?? TIMEOUT_MS);
386
+ const lockedHostname = url.hostname.toLowerCase().replace(/\.$/, "");
387
+ const fetchImpl = opts?.fetchImpl ?? harnessFetchForTest;
388
+
389
+ for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
390
+ const localName =
391
+ url.hostname.toLowerCase() === "localhost" ||
392
+ url.hostname.toLowerCase().endsWith(".localhost");
393
+ if (!opts?.allowPrivate && localName) {
394
+ return {
395
+ attempted: false,
396
+ url: observedUrl(),
397
+ note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
398
+ };
399
+ }
400
+ let addresses: ResolvedAddress[] = [];
401
+ if (!fetchImpl) {
402
+ try {
403
+ addresses = await resolveHost(url.hostname);
404
+ } catch (error) {
405
+ return {
406
+ attempted: true,
407
+ url: observedUrl(),
408
+ note: `request errored (DNS): ${(error as Error).message}`,
409
+ };
410
+ }
411
+ if (addresses.length === 0) {
412
+ return {
413
+ attempted: true,
414
+ url: observedUrl(),
415
+ note: `request errored (DNS): ${url.hostname} resolved to no addresses`,
416
+ };
417
+ }
418
+ } else if (isIP(url.hostname.replace(/^\[|\]$/g, ""))) {
419
+ addresses = [
420
+ {
421
+ address: url.hostname.replace(/^\[|\]$/g, ""),
422
+ family: isIP(url.hostname.replace(/^\[|\]$/g, "")) as 4 | 6,
423
+ },
424
+ ];
425
+ }
426
+ if (!opts?.allowPrivate && addresses.some((address) => !isPublicIpAddress(address.address))) {
427
+ return {
428
+ attempted: false,
429
+ url: observedUrl(),
430
+ note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
431
+ };
432
+ }
433
+
434
+ let closeFetched: (() => Promise<void>) | undefined;
435
+ try {
436
+ const requestInit: RequestInit = {
437
+ method,
438
+ headers,
439
+ body,
440
+ redirect: "manual",
441
+ signal,
442
+ };
443
+ const fetched = fetchImpl
444
+ ? { response: await fetchImpl(url, requestInit), close: async () => undefined }
445
+ : await fetchPinned(url, requestInit, addresses);
446
+ closeFetched = fetched.close;
447
+ const res = fetched.response;
448
+ const location = res.headers.get("location");
449
+ if (location && [301, 302, 303, 307, 308].includes(res.status)) {
450
+ if (redirects === MAX_REDIRECTS) {
451
+ await res.body?.cancel().catch(() => undefined);
452
+ await fetched.close().catch(() => undefined);
453
+ closeFetched = undefined;
454
+ return {
455
+ attempted: true,
456
+ status: res.status,
457
+ url: observedUrl(),
458
+ note: `redirect limit exceeded (${MAX_REDIRECTS})`,
459
+ };
460
+ }
461
+ const next = new URL(location, url);
462
+ if (!(next.protocol === "http:" || next.protocol === "https:")) {
463
+ await res.body?.cancel().catch(() => undefined);
464
+ await fetched.close().catch(() => undefined);
465
+ closeFetched = undefined;
466
+ return {
467
+ attempted: true,
468
+ status: res.status,
469
+ url: observedUrl(),
470
+ note: `redirected to disallowed protocol ${next.protocol}`,
471
+ };
472
+ }
473
+ if (next.hostname.toLowerCase().replace(/\.$/, "") !== lockedHostname) {
474
+ await res.body?.cancel().catch(() => undefined);
475
+ await fetched.close().catch(() => undefined);
476
+ closeFetched = undefined;
477
+ return {
478
+ attempted: true,
479
+ status: res.status,
480
+ url: observedUrl(),
481
+ note: `redirect left the bound host (${url.hostname} -> ${next.hostname})`,
482
+ };
483
+ }
484
+ if (next.origin !== url.origin) {
485
+ for (const name of ["authorization", "cookie", "proxy-authorization"]) {
486
+ headers.delete(name);
487
+ }
488
+ }
489
+ if (
490
+ res.status === 303 ||
491
+ ((res.status === 301 || res.status === 302) && method === "POST")
492
+ ) {
493
+ method = "GET";
494
+ body = undefined;
495
+ headers.delete("content-length");
496
+ headers.delete("content-type");
497
+ }
498
+ await res.body?.cancel().catch(() => undefined);
499
+ await fetched.close().catch(() => undefined);
500
+ closeFetched = undefined;
501
+ url = next;
502
+ continue;
503
+ }
504
+
505
+ const observed = await readBodyCapped(res);
506
+ await fetched.close().catch(() => undefined);
507
+ closeFetched = undefined;
508
+ if (observed.truncated) {
509
+ return {
510
+ attempted: true,
511
+ status: res.status,
512
+ url: observedUrl(),
513
+ bodySha256: observed.sha256,
514
+ bodyBytes: observed.bytes,
515
+ note: "response body exceeded the 2 MiB capture limit; matcher result is inconclusive",
516
+ };
517
+ }
518
+ const failures = await evaluateExpect(expect, res.status, observed.text);
519
+ const canaryObserved = canaryToken ? observed.text.includes(canaryToken) : undefined;
520
+ return {
521
+ attempted: true,
522
+ matched: failures.length === 0,
523
+ status: res.status,
524
+ url: observedUrl(),
525
+ bodySha256: observed.sha256,
526
+ bodyBytes: observed.bytes,
527
+ canaryObserved,
528
+ note:
529
+ failures.length === 0
530
+ ? `status ${res.status}, all predicates matched`
531
+ : failures.join("; "),
532
+ };
533
+ } catch (e) {
534
+ await closeFetched?.().catch(() => undefined);
535
+ return {
536
+ attempted: true,
537
+ url: observedUrl(),
538
+ note: `request errored (DNS/TLS/timeout): ${(e as Error).message}`,
539
+ };
540
+ }
541
+ }
542
+
543
+ return { attempted: true, url: observedUrl(), note: "unreachable redirect state" };
544
+ }
545
+
546
+ function injectCanary(
547
+ verify: PoCEvidence["verify"],
548
+ token: string | undefined,
549
+ ): PoCEvidence["verify"] {
550
+ if (!token) return verify;
551
+ const replace = (value: string) => value.replace(POC_CANARY_PLACEHOLDER, token);
552
+ return {
553
+ ...verify,
554
+ url: replace(verify.url),
555
+ body: verify.body === undefined ? undefined : replace(verify.body),
556
+ headers:
557
+ verify.headers === undefined
558
+ ? undefined
559
+ : Object.fromEntries(
560
+ Object.entries(verify.headers).map(([key, value]) => [key, replace(value)]),
561
+ ),
562
+ };
563
+ }
564
+
565
+ function canaryResult(
566
+ token: string | undefined,
567
+ target: HarnessResponseObservation,
568
+ control?: HarnessResponseObservation,
569
+ ): HarnessCanaryResult | undefined {
570
+ if (!token) return;
571
+ const attempted =
572
+ target.canaryObserved !== undefined && (control ? control.canaryObserved !== undefined : true);
573
+ const pass = control
574
+ ? attempted && target.canaryObserved === true && control.canaryObserved === false
575
+ : attempted && target.canaryObserved === true;
576
+ return {
577
+ mode: "reflection",
578
+ attempted,
579
+ pass,
580
+ tokenSha256: createHash("sha256").update(token).digest("hex"),
581
+ targetObserved: target.canaryObserved,
582
+ controlObserved: control?.canaryObserved,
583
+ note: control
584
+ ? `canary ${pass ? "target-only" : "failed"}: target=${String(target.canaryObserved)}, control=${String(control.canaryObserved)}`
585
+ : `canary ${pass ? "observed" : "not observed"} on target`,
586
+ };
587
+ }
588
+
589
+ /**
590
+ * Re-send the evidence's verify request with the harness's own client and
591
+ * judge the response against verify.expect. Never throws — the outcome is a
592
+ * structured result the ledger gate interprets.
593
+ */
594
+ export async function replayVerify(
595
+ evidence: PoCEvidence,
596
+ opts?: ReplayOptions,
597
+ ): Promise<HarnessVerifyResult> {
598
+ const token = evidence.verify.canary
599
+ ? `poc_canary_${randomBytes(24).toString("hex")}`
600
+ : undefined;
601
+ const verify = injectCanary(evidence.verify, token);
602
+ const target = await replayRequest(verify, verify.expect, token, opts);
603
+ const canary = canaryResult(token, target);
604
+ return {
605
+ attempted: target.attempted,
606
+ pass: target.matched === true,
607
+ status: target.status,
608
+ target,
609
+ canary,
610
+ proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
611
+ note: `harness replay: ${target.note}${canary ? `; ${canary.note}` : ""}`,
612
+ };
613
+ }
614
+
615
+ /**
616
+ * Execute one harness-owned request template against both the case target and
617
+ * a distinct control origin. The PoC cannot weaken the control request: the
618
+ * harness preserves method, path, query, headers, body, and target predicates,
619
+ * changing only the origin to the declared control target.
620
+ */
621
+ export async function replayDifferential(
622
+ evidence: PoCEvidence,
623
+ caseTarget: string,
624
+ controlTarget: string,
625
+ opts?: ReplayOptions,
626
+ ): Promise<HarnessVerifyResult> {
627
+ const bindingError = verifyUrlBindingError(evidence.verify.url, caseTarget);
628
+ if (bindingError) {
629
+ return { attempted: false, pass: false, note: `target binding failed: ${bindingError}` };
630
+ }
631
+ if (sameTargetIdentity(caseTarget, controlTarget)) {
632
+ return {
633
+ attempted: false,
634
+ pass: false,
635
+ note: "control target resolves to the same network identity as the case target",
636
+ };
637
+ }
638
+ const controlUrl = controlUrlFor(evidence.verify.url, controlTarget);
639
+ if (!controlUrl) {
640
+ return {
641
+ attempted: false,
642
+ pass: false,
643
+ note: `control target is not an HTTP network target: ${controlTarget}`,
644
+ };
645
+ }
646
+
647
+ const token = evidence.verify.canary
648
+ ? `poc_canary_${randomBytes(24).toString("hex")}`
649
+ : undefined;
650
+ const targetVerify = injectCanary(evidence.verify, token);
651
+ const target = await replayRequest(targetVerify, targetVerify.expect, token, opts);
652
+ const control = await replayRequest(
653
+ {
654
+ ...targetVerify,
655
+ url: injectCanary({ ...evidence.verify, url: controlUrl.toString() }, token).url,
656
+ },
657
+ targetVerify.expect,
658
+ token,
659
+ opts,
660
+ );
661
+ const attempted = target.attempted && control.attempted;
662
+ const conclusive = target.matched !== undefined && control.matched !== undefined;
663
+ const targetMatched = target.matched === true;
664
+ const controlMatched = control.matched === true;
665
+ const differential = conclusive
666
+ ? targetMatched
667
+ ? controlMatched
668
+ ? "both"
669
+ : "target_only"
670
+ : controlMatched
671
+ ? "control_only"
672
+ : "neither"
673
+ : undefined;
674
+ const canary = canaryResult(token, target, control);
675
+ const pass =
676
+ attempted &&
677
+ conclusive &&
678
+ differential === "target_only" &&
679
+ (canary === undefined || canary.pass === true);
680
+ return {
681
+ attempted,
682
+ pass,
683
+ status: target.status,
684
+ target,
685
+ control,
686
+ differential,
687
+ canary,
688
+ proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
689
+ note:
690
+ `harness differential ${differential ?? "inconclusive"}: target (${target.note}); ` +
691
+ `control (${control.note})${canary ? `; ${canary.note}` : ""}`,
692
+ };
693
+ }