@xaccefy/pi-casefile 0.10.0 → 0.11.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.
@@ -1,60 +1,47 @@
1
1
  /**
2
- * Harness-side target/control replay — Tier 2 of docs/poc-trust-model.md.
2
+ * Harness-side attack/baseline replay.
3
3
  *
4
4
  * The machine floor cannot trust a caller's self-reported `re_executed`
5
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
- * main agent supplies the predicate; the harness owns both evidence acquisition
9
- * and predicate execution before the later semantic review.
6
+ * (attack) request and the evidence's `baseline` (legitimate) request with
7
+ * its own HTTP client, applying the attack's `expect` predicates to BOTH
8
+ * responses. The main agent supplies the predicate and the baseline; the
9
+ * harness owns evidence acquisition and predicate execution before the later
10
+ * semantic review.
10
11
  *
11
12
  * Policy:
12
13
  * - Private/internal hosts require explicit operator authorization; otherwise
13
14
  * replay fails closed.
14
15
  * - Redirects are manual and every hop is checked before it is fetched.
15
- * - Target must match while the identical control request must not.
16
+ * - The attack request must match while the baseline request must not.
16
17
  *
17
18
  * Undici's custom dispatcher pins the approved DNS result through connect;
18
19
  * node:dns and node:net provide resolution and address classification.
19
20
  */
20
21
 
21
- import { createHash, randomBytes } from "node:crypto";
22
+ import { createHash } from "node:crypto";
22
23
  import { lookup as dnsLookup } from "node:dns/promises";
23
24
  import { isIP } from "node:net";
24
25
  import { Worker } from "node:worker_threads";
25
26
  import { isPublicIpAddress } from "@xaccefy/pi-shared";
26
27
  import { Agent, fetch as undiciFetch } from "undici";
27
- import { POC_CANARY_PLACEHOLDER, type PoCEvidence, type VerifyExpect } from "./evidence.ts";
28
+ import type { PoCEvidence, VerifyExpect } from "./evidence.ts";
28
29
 
29
30
  // Public re-export makes the single-source classifier identity testable across
30
31
  // the web tool and confirmation replay paths.
31
32
  export { isPublicIpAddress } from "@xaccefy/pi-shared";
32
33
 
33
34
  export type HarnessVerifyResult = {
34
- /** true = the harness sent both target and control requests and judged them. */
35
+ /** true = the harness sent both attack and baseline requests and judged them. */
35
36
  attempted: boolean;
36
- /** Present when attempted: target matched and control did not. */
37
+ /** Present when attempted: attack matched and baseline did not. */
37
38
  pass?: boolean;
38
39
  /** Backward-compatible target status summary. */
39
40
  status?: number;
40
- /** Machine-observed target/control response summaries. */
41
+ /** Machine-observed attack/baseline response summaries. */
41
42
  target?: HarnessResponseObservation;
42
43
  control?: HarnessResponseObservation;
43
44
  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
45
  note: string;
59
46
  };
60
47
 
@@ -65,7 +52,6 @@ export type HarnessResponseObservation = {
65
52
  url: string;
66
53
  bodySha256?: string;
67
54
  bodyBytes?: number;
68
- canaryObserved?: boolean;
69
55
  note: string;
70
56
  };
71
57
 
@@ -76,8 +62,16 @@ const REGEX_TIMEOUT_MS = 250;
76
62
 
77
63
  type ResolvedAddress = { address: string; family: 4 | 6 };
78
64
 
65
+ /** Comparison-normalize a hostname: lowercase, strip IPv6 brackets and any trailing root dot. */
66
+ function normHost(hostname: string): string {
67
+ return hostname
68
+ .toLowerCase()
69
+ .replace(/^\[|\]$/g, "")
70
+ .replace(/\.$/, "");
71
+ }
72
+
79
73
  async function resolveHost(hostname: string): Promise<ResolvedAddress[]> {
80
- const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
74
+ const host = normHost(hostname);
81
75
  const literalFamily = isIP(host);
82
76
  if (literalFamily) return [{ address: host, family: literalFamily as 4 | 6 }];
83
77
  const lookup = dnsLookup(host, { all: true, verbatim: true }) as Promise<ResolvedAddress[]>;
@@ -112,41 +106,6 @@ function effectivePort(url: URL): string {
112
106
  return url.port || (url.protocol === "https:" ? "443" : "80");
113
107
  }
114
108
 
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
109
  /**
151
110
  * Bind a model-authored verify URL to the target the harness actually ran.
152
111
  * A bare target permits either HTTP scheme; an explicit target URL binds the
@@ -161,8 +120,8 @@ export function verifyUrlBindingError(verifyUrl: string, target: string): string
161
120
  return `verify.url is not parseable: ${verifyUrl}`;
162
121
  }
163
122
  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(/\.$/, "");
123
+ const declaredHost = normHost(declared.url.hostname);
124
+ const observedHost = normHost(observed.hostname);
166
125
  if (declaredHost !== observedHost) {
167
126
  return `verify.url host ${observedHost} does not match run target ${declaredHost}`;
168
127
  }
@@ -178,16 +137,6 @@ export function verifyUrlBindingError(verifyUrl: string, target: string): string
178
137
  return;
179
138
  }
180
139
 
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
140
  // ── Predicate evaluation ──────────────────────────────────────────────
192
141
 
193
142
  const REGEX_WORKER_SOURCE = `
@@ -351,7 +300,6 @@ async function fetchPinned(
351
300
  async function replayRequest(
352
301
  verify: PoCEvidence["verify"],
353
302
  expect: VerifyExpect,
354
- canaryToken?: string,
355
303
  opts?: ReplayOptions,
356
304
  ): Promise<HarnessResponseObservation> {
357
305
  let url: URL;
@@ -364,8 +312,6 @@ async function replayRequest(
364
312
  note: `verify.url unparseable (${verify.url})`,
365
313
  };
366
314
  }
367
- const observedUrl = () =>
368
- canaryToken ? url.toString().replaceAll(canaryToken, POC_CANARY_PLACEHOLDER) : url.toString();
369
315
  if (!(url.protocol === "http:" || url.protocol === "https:")) {
370
316
  return { attempted: false, url: verify.url, note: `verify.url protocol ${url.protocol}` };
371
317
  }
@@ -378,12 +324,12 @@ async function replayRequest(
378
324
  } catch (error) {
379
325
  return {
380
326
  attempted: false,
381
- url: observedUrl(),
327
+ url: verify.url,
382
328
  note: `verify.headers invalid: ${(error as Error).message}`,
383
329
  };
384
330
  }
385
331
  const signal = AbortSignal.timeout(opts?.timeoutMs ?? TIMEOUT_MS);
386
- const lockedHostname = url.hostname.toLowerCase().replace(/\.$/, "");
332
+ const lockedHostname = normHost(url.hostname);
387
333
  const fetchImpl = opts?.fetchImpl ?? harnessFetchForTest;
388
334
 
389
335
  for (let redirects = 0; redirects <= MAX_REDIRECTS; redirects++) {
@@ -393,7 +339,7 @@ async function replayRequest(
393
339
  if (!opts?.allowPrivate && localName) {
394
340
  return {
395
341
  attempted: false,
396
- url: observedUrl(),
342
+ url: url.toString(),
397
343
  note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
398
344
  };
399
345
  }
@@ -404,29 +350,27 @@ async function replayRequest(
404
350
  } catch (error) {
405
351
  return {
406
352
  attempted: true,
407
- url: observedUrl(),
353
+ url: url.toString(),
408
354
  note: `request errored (DNS): ${(error as Error).message}`,
409
355
  };
410
356
  }
411
357
  if (addresses.length === 0) {
412
358
  return {
413
359
  attempted: true,
414
- url: observedUrl(),
360
+ url: url.toString(),
415
361
  note: `request errored (DNS): ${url.hostname} resolved to no addresses`,
416
362
  };
417
363
  }
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
- ];
364
+ } else {
365
+ const host = normHost(url.hostname);
366
+ if (isIP(host)) {
367
+ addresses = [{ address: host, family: isIP(host) as 4 | 6 }];
368
+ }
425
369
  }
426
370
  if (!opts?.allowPrivate && addresses.some((address) => !isPublicIpAddress(address.address))) {
427
371
  return {
428
372
  attempted: false,
429
- url: observedUrl(),
373
+ url: url.toString(),
430
374
  note: `${url.hostname} is a private/internal host; operator authorization is required for harness replay`,
431
375
  };
432
376
  }
@@ -454,7 +398,7 @@ async function replayRequest(
454
398
  return {
455
399
  attempted: true,
456
400
  status: res.status,
457
- url: observedUrl(),
401
+ url: url.toString(),
458
402
  note: `redirect limit exceeded (${MAX_REDIRECTS})`,
459
403
  };
460
404
  }
@@ -466,18 +410,18 @@ async function replayRequest(
466
410
  return {
467
411
  attempted: true,
468
412
  status: res.status,
469
- url: observedUrl(),
413
+ url: url.toString(),
470
414
  note: `redirected to disallowed protocol ${next.protocol}`,
471
415
  };
472
416
  }
473
- if (next.hostname.toLowerCase().replace(/\.$/, "") !== lockedHostname) {
417
+ if (normHost(next.hostname) !== lockedHostname) {
474
418
  await res.body?.cancel().catch(() => undefined);
475
419
  await fetched.close().catch(() => undefined);
476
420
  closeFetched = undefined;
477
421
  return {
478
422
  attempted: true,
479
423
  status: res.status,
480
- url: observedUrl(),
424
+ url: url.toString(),
481
425
  note: `redirect left the bound host (${url.hostname} -> ${next.hostname})`,
482
426
  };
483
427
  }
@@ -509,22 +453,20 @@ async function replayRequest(
509
453
  return {
510
454
  attempted: true,
511
455
  status: res.status,
512
- url: observedUrl(),
456
+ url: url.toString(),
513
457
  bodySha256: observed.sha256,
514
458
  bodyBytes: observed.bytes,
515
459
  note: "response body exceeded the 2 MiB capture limit; matcher result is inconclusive",
516
460
  };
517
461
  }
518
462
  const failures = await evaluateExpect(expect, res.status, observed.text);
519
- const canaryObserved = canaryToken ? observed.text.includes(canaryToken) : undefined;
520
463
  return {
521
464
  attempted: true,
522
465
  matched: failures.length === 0,
523
466
  status: res.status,
524
- url: observedUrl(),
467
+ url: url.toString(),
525
468
  bodySha256: observed.sha256,
526
469
  bodyBytes: observed.bytes,
527
- canaryObserved,
528
470
  note:
529
471
  failures.length === 0
530
472
  ? `status ${res.status}, all predicates matched`
@@ -534,13 +476,13 @@ async function replayRequest(
534
476
  await closeFetched?.().catch(() => undefined);
535
477
  return {
536
478
  attempted: true,
537
- url: observedUrl(),
479
+ url: url.toString(),
538
480
  note: `request errored (DNS/TLS/timeout): ${(e as Error).message}`,
539
481
  };
540
482
  }
541
483
  }
542
484
 
543
- return { attempted: true, url: observedUrl(), note: "unreachable redirect state" };
485
+ return { attempted: true, url: url.toString(), note: "unreachable redirect state" };
544
486
  }
545
487
 
546
488
  /**
@@ -566,84 +508,14 @@ export function sameRequest(
566
508
  );
567
509
  }
568
510
 
569
- function injectCanary(
570
- verify: PoCEvidence["verify"],
571
- token: string | undefined,
572
- ): PoCEvidence["verify"] {
573
- if (!token) return verify;
574
- const replace = (value: string) => value.replace(POC_CANARY_PLACEHOLDER, token);
575
- return {
576
- ...verify,
577
- url: replace(verify.url),
578
- body: verify.body === undefined ? undefined : replace(verify.body),
579
- headers:
580
- verify.headers === undefined
581
- ? undefined
582
- : Object.fromEntries(
583
- Object.entries(verify.headers).map(([key, value]) => [key, replace(value)]),
584
- ),
585
- };
586
- }
587
-
588
- function canaryResult(
589
- token: string | undefined,
590
- target: HarnessResponseObservation,
591
- control?: HarnessResponseObservation,
592
- ): HarnessCanaryResult | undefined {
593
- if (!token) return;
594
- const attempted =
595
- target.canaryObserved !== undefined && (control ? control.canaryObserved !== undefined : true);
596
- const pass = control
597
- ? attempted && target.canaryObserved === true && control.canaryObserved === false
598
- : attempted && target.canaryObserved === true;
599
- return {
600
- mode: "reflection",
601
- attempted,
602
- pass,
603
- tokenSha256: createHash("sha256").update(token).digest("hex"),
604
- targetObserved: target.canaryObserved,
605
- controlObserved: control?.canaryObserved,
606
- note: control
607
- ? `canary ${pass ? "target-only" : "failed"}: target=${String(target.canaryObserved)}, control=${String(control.canaryObserved)}`
608
- : `canary ${pass ? "observed" : "not observed"} on target`,
609
- };
610
- }
611
-
612
- /**
613
- * Re-send the evidence's verify request with the harness's own client and
614
- * judge the response against verify.expect. Never throws — the outcome is a
615
- * structured result the ledger gate interprets.
616
- */
617
- export async function replayVerify(
618
- evidence: PoCEvidence,
619
- opts?: ReplayOptions,
620
- ): Promise<HarnessVerifyResult> {
621
- const token = evidence.verify.canary
622
- ? `poc_canary_${randomBytes(24).toString("hex")}`
623
- : undefined;
624
- const verify = injectCanary(evidence.verify, token);
625
- const target = await replayRequest(verify, verify.expect, token, opts);
626
- const canary = canaryResult(token, target);
627
- return {
628
- attempted: target.attempted,
629
- pass: target.matched === true,
630
- status: target.status,
631
- target,
632
- canary,
633
- proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
634
- note: `harness replay: ${target.note}${canary ? `; ${canary.note}` : ""}`,
635
- };
636
- }
637
-
638
511
  /**
639
512
  * Combine the two observations into the differential verdict. Shared by the
640
- * inter-host (target vs control host) and intra-target (attack vs same-host
641
- * baseline) replays — only the note labels differ.
513
+ * attack-vs-baseline replay: "target" is the attack request, "control" the
514
+ * legitimate baseline request.
642
515
  */
643
516
  function judgeDifferential(
644
517
  target: HarnessResponseObservation,
645
518
  control: HarnessResponseObservation,
646
- token: string | undefined,
647
519
  label: { kind: string; a: string; b: string },
648
520
  ): HarnessVerifyResult {
649
521
  const attempted = target.attempted && control.attempted;
@@ -657,12 +529,7 @@ function judgeDifferential(
657
529
  ? "control_only"
658
530
  : "neither"
659
531
  : undefined;
660
- const canary = canaryResult(token, target, control);
661
- const pass =
662
- attempted &&
663
- conclusive &&
664
- differential === "target_only" &&
665
- (canary === undefined || canary.pass === true);
532
+ const pass = attempted && conclusive && differential === "target_only";
666
533
  return {
667
534
  attempted,
668
535
  pass,
@@ -670,72 +537,16 @@ function judgeDifferential(
670
537
  target,
671
538
  control,
672
539
  differential,
673
- canary,
674
- proofStrength: canary?.pass ? "canary_differential" : "predicate_differential",
675
540
  note:
676
541
  `harness ${label.kind} ${differential ?? "inconclusive"}: ${label.a} (${target.note}); ` +
677
- `${label.b} (${control.note})${canary ? `; ${canary.note}` : ""}`,
542
+ `${label.b} (${control.note})`,
678
543
  };
679
544
  }
680
545
 
681
546
  /**
682
- * Execute one harness-owned request template against both the case target and
683
- * a distinct control origin. The PoC cannot weaken the control request: the
684
- * harness preserves method, path, query, headers, body, and target predicates,
685
- * changing only the origin to the declared control target.
686
- */
687
- export async function replayDifferential(
688
- evidence: PoCEvidence,
689
- caseTarget: string,
690
- controlTarget: string,
691
- opts?: ReplayOptions,
692
- ): Promise<HarnessVerifyResult> {
693
- const bindingError = verifyUrlBindingError(evidence.verify.url, caseTarget);
694
- if (bindingError) {
695
- return { attempted: false, pass: false, note: `target binding failed: ${bindingError}` };
696
- }
697
- if (sameTargetIdentity(caseTarget, controlTarget)) {
698
- return {
699
- attempted: false,
700
- pass: false,
701
- note: "control target resolves to the same network identity as the case target",
702
- };
703
- }
704
- const controlUrl = controlUrlFor(evidence.verify.url, controlTarget);
705
- if (!controlUrl) {
706
- return {
707
- attempted: false,
708
- pass: false,
709
- note: `control target is not an HTTP network target: ${controlTarget}`,
710
- };
711
- }
712
-
713
- const token = evidence.verify.canary
714
- ? `poc_canary_${randomBytes(24).toString("hex")}`
715
- : undefined;
716
- const targetVerify = injectCanary(evidence.verify, token);
717
- const target = await replayRequest(targetVerify, targetVerify.expect, token, opts);
718
- const control = await replayRequest(
719
- {
720
- ...targetVerify,
721
- url: injectCanary({ ...evidence.verify, url: controlUrl.toString() }, token).url,
722
- },
723
- targetVerify.expect,
724
- token,
725
- opts,
726
- );
727
- return judgeDifferential(target, control, token, {
728
- kind: "differential",
729
- a: "target",
730
- b: "control",
731
- });
732
- }
733
-
734
- /**
735
- * Same-host differential (Tier 2, intra-target). For access-control and
736
- * business-logic classes the discriminating variable is the attacker's
737
- * identity or a request parameter, NOT the host — so the sound baseline is a
738
- * legitimate request to the SAME target, not the same request to another host.
547
+ * Same-host differential (intra-target). The discriminating variable is the
548
+ * attacker's identity or a request parameter, not the host the sound
549
+ * baseline is a legitimate request to the SAME target.
739
550
  * The harness sends the attack request and the model-declared `evidence.baseline`
740
551
  * request to the case target, applies the attack's `verify.expect` predicates to
741
552
  * BOTH responses, and passes only when the proof appears on the attack response
@@ -772,13 +583,9 @@ export async function replayIntraTarget(
772
583
  };
773
584
  }
774
585
 
775
- const token = evidence.verify.canary
776
- ? `poc_canary_${randomBytes(24).toString("hex")}`
777
- : undefined;
778
- const attackVerify = injectCanary(evidence.verify, token);
779
- const attack = await replayRequest(attackVerify, attackVerify.expect, token, opts);
586
+ const attack = await replayRequest(evidence.verify, evidence.verify.expect, opts);
780
587
  // The baseline carries the attack's predicates: the proof must be ABSENT here.
781
- const baselineVerify = injectCanary(
588
+ const base = await replayRequest(
782
589
  {
783
590
  ...evidence.verify,
784
591
  method: baseline.method,
@@ -786,11 +593,11 @@ export async function replayIntraTarget(
786
593
  headers: baseline.headers,
787
594
  body: baseline.body,
788
595
  },
789
- token,
596
+ evidence.verify.expect,
597
+ opts,
790
598
  );
791
- const base = await replayRequest(baselineVerify, attackVerify.expect, token, opts);
792
599
 
793
- return judgeDifferential(attack, base, token, {
600
+ return judgeDifferential(attack, base, {
794
601
  kind: "intra-target",
795
602
  a: "attack",
796
603
  b: "baseline",