@xaccefy/pi-casefile 0.10.1 → 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.
package/src/evidence.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Evidence contract for PoC confirmation.
3
3
  *
4
4
  * A PoC must write `evidence.json` into $PI_POC_EVIDENCE_DIR: a nonce-bound,
5
- * schema-validated record of what it claims and the request spec the harness
6
- * executes against both target and control. The harness validates the file,
7
- * binds it to the run via $PI_POC_NONCE, and acquires the responses itself.
5
+ * schema-validated record of what it claims, the attack request spec, and a
6
+ * legitimate same-host baseline request. The harness validates the file,
7
+ * binds it to the run via $PI_POC_NONCE, and acquires both responses itself.
8
8
  * The main/coordinator agent remains the semantic reviewer after the machine differential.
9
9
  *
10
10
  * Exit zero is required run integrity, but never vulnerability proof.
@@ -21,43 +21,26 @@ export type VerifyExpect = {
21
21
  body_regex?: string[];
22
22
  };
23
23
 
24
- /** Replaced only inside the harness replay, after the PoC process has exited. */
25
- export const POC_CANARY_PLACEHOLDER = "{{PI_POC_CANARY}}";
26
-
27
- export type VerifyCanary = {
28
- /** Reflection is machine-checked: target must return the fresh token and control must not. */
29
- mode: "reflection";
30
- /** Fixed literal; callers cannot choose or predict the harness-generated token. */
31
- placeholder: typeof POC_CANARY_PLACEHOLDER;
32
- };
33
-
34
24
  export type PoCEvidence = {
35
25
  /** Must equal the run's PI_POC_NONCE (harness-verified). */
36
26
  nonce: string;
37
27
  /** What the exploit asserts, e.g. "read /etc/passwd of target". */
38
28
  claim: string;
39
- /** Request spec the harness executes in phase 1 and again from the main-agent phase-2 call. */
29
+ /** Attack request spec the harness executes to acquire machine evidence. */
40
30
  verify: {
41
31
  method: string;
42
32
  url: string;
43
33
  headers?: Record<string, string>;
44
34
  body?: string;
45
35
  expect: VerifyExpect;
46
- /** Optional stronger causality dimension, independent of the authored predicate. */
47
- canary?: VerifyCanary;
48
- /**
49
- * Differential shape. "inter_host" (default) = same request to target vs a
50
- * distinct patched control host (body-carried proof). "intra_target" = attack
51
- * request vs a legitimate same-host `baseline` request (access-control /
52
- * business-logic classes, where the discriminating variable is identity or a
53
- * parameter, not the host) — requires `baseline`.
54
- */
55
- mode?: "inter_host" | "intra_target";
56
36
  };
57
37
  /** What the script itself saw — corroboration only, never proof. */
58
38
  observations: string[];
59
- /** Optional baseline request for the main agent's differential review. */
60
- baseline?: {
39
+ /**
40
+ * Legitimate same-host request whose response must NOT satisfy the attack
41
+ * predicate — the differential control (identity or parameter varies, not host).
42
+ */
43
+ baseline: {
61
44
  method: string;
62
45
  url: string;
63
46
  headers?: Record<string, string>;
@@ -184,10 +167,6 @@ function hasDiscriminatingBodyExpectation(expect: Record<string, unknown>): bool
184
167
  );
185
168
  }
186
169
 
187
- function countOccurrences(value: string, needle: string): number {
188
- return value.split(needle).length - 1;
189
- }
190
-
191
170
  /**
192
171
  * Parse + validate a PoC's evidence.json. Returns the validated object or a
193
172
  * field-level error. Deliberately strict: an invalid evidence file means the
@@ -230,48 +209,18 @@ export function parsePoCEvidence(
230
209
  error: `evidence.json verify.body must be a string no longer than ${MAX_REQUEST_BODY_CHARS} characters`,
231
210
  };
232
211
  }
233
- if (verify.canary !== undefined) {
234
- if (
235
- !isRecord(verify.canary) ||
236
- verify.canary.mode !== "reflection" ||
237
- verify.canary.placeholder !== POC_CANARY_PLACEHOLDER
238
- ) {
239
- return {
240
- ok: false,
241
- error: `evidence.json verify.canary must be {"mode":"reflection","placeholder":"${POC_CANARY_PLACEHOLDER}"}`,
242
- };
243
- }
244
- const canaryLocations = [
245
- verify.url,
246
- typeof verify.body === "string" ? verify.body : "",
247
- ...(isRecord(verify.headers)
248
- ? Object.values(verify.headers).filter(
249
- (value): value is string => typeof value === "string",
250
- )
251
- : []),
252
- ];
253
- const count = canaryLocations.reduce(
254
- (total, value) => total + countOccurrences(value, POC_CANARY_PLACEHOLDER),
255
- 0,
256
- );
257
- if (count !== 1) {
258
- return {
259
- ok: false,
260
- error: `evidence.json verify.canary requires exactly one ${POC_CANARY_PLACEHOLDER} placeholder across url, body, or header values (got ${count})`,
261
- };
262
- }
263
- }
264
- if (verify.mode !== undefined && verify.mode !== "inter_host" && verify.mode !== "intra_target") {
212
+ if (verify.mode !== undefined) {
265
213
  return {
266
214
  ok: false,
267
- error: 'evidence.json verify.mode must be "inter_host" or "intra_target"',
215
+ error:
216
+ "evidence.json verify.mode is removed — the attack-vs-baseline differential is the only confirmation model. Drop verify.mode and declare evidence.baseline.",
268
217
  };
269
218
  }
270
- if (verify.mode === "intra_target" && !isRecord(raw.baseline)) {
219
+ if (verify.canary !== undefined) {
271
220
  return {
272
221
  ok: false,
273
222
  error:
274
- "evidence.json verify.mode intra_target requires baseline a legitimate same-host request whose response must NOT satisfy the attack predicate",
223
+ "evidence.json verify.canary is removedthe reflection canary tier was retired. Remove verify.canary and any {{PI_POC_CANARY}} placeholder.",
275
224
  };
276
225
  }
277
226
  const expect = verify.expect;
@@ -332,41 +281,45 @@ export function parsePoCEvidence(
332
281
  ) {
333
282
  return { ok: false, error: "evidence.json observations exceed the bounded string-array limit" };
334
283
  }
335
- if (raw.baseline !== undefined) {
336
- const b = raw.baseline;
337
- if (!isRecord(b)) return { ok: false, error: "evidence.json baseline must be an object" };
338
- if (
339
- !nonEmptyString(b.method) ||
340
- !HTTP_METHODS.includes(b.method.toUpperCase()) ||
341
- !httpUrl(b.url)
342
- ) {
343
- return {
344
- ok: false,
345
- error: "evidence.json baseline needs an http(s) url and a valid HTTP method",
346
- };
347
- }
348
- if (b.headers !== undefined && !headerRecord(b.headers)) {
349
- return {
350
- ok: false,
351
- error:
352
- "evidence.json baseline.headers must be bounded valid end-to-end HTTP headers; authority, framing, proxy, and hop-by-hop headers are forbidden",
353
- };
354
- }
355
- if (
356
- b.body !== undefined &&
357
- (typeof b.body !== "string" || b.body.length > MAX_REQUEST_BODY_CHARS)
358
- ) {
359
- return {
360
- ok: false,
361
- error: `evidence.json baseline.body must be no longer than ${MAX_REQUEST_BODY_CHARS} characters`,
362
- };
363
- }
364
- if (
365
- b.body_contains !== undefined &&
366
- !boundedStringArray(b.body_contains, MAX_EXPECT_VALUES, MAX_EXPECT_CHARS)
367
- ) {
368
- return { ok: false, error: "evidence.json baseline.body_contains exceeds limits" };
369
- }
284
+ const b = raw.baseline;
285
+ if (!isRecord(b)) {
286
+ return {
287
+ ok: false,
288
+ error:
289
+ "evidence.json requires baseline — a legitimate same-host request whose response must NOT satisfy the attack predicate",
290
+ };
291
+ }
292
+ if (
293
+ !nonEmptyString(b.method) ||
294
+ !HTTP_METHODS.includes(b.method.toUpperCase()) ||
295
+ !httpUrl(b.url)
296
+ ) {
297
+ return {
298
+ ok: false,
299
+ error: "evidence.json baseline needs an http(s) url and a valid HTTP method",
300
+ };
301
+ }
302
+ if (b.headers !== undefined && !headerRecord(b.headers)) {
303
+ return {
304
+ ok: false,
305
+ error:
306
+ "evidence.json baseline.headers must be bounded valid end-to-end HTTP headers; authority, framing, proxy, and hop-by-hop headers are forbidden",
307
+ };
308
+ }
309
+ if (
310
+ b.body !== undefined &&
311
+ (typeof b.body !== "string" || b.body.length > MAX_REQUEST_BODY_CHARS)
312
+ ) {
313
+ return {
314
+ ok: false,
315
+ error: `evidence.json baseline.body must be no longer than ${MAX_REQUEST_BODY_CHARS} characters`,
316
+ };
317
+ }
318
+ if (
319
+ b.body_contains !== undefined &&
320
+ !boundedStringArray(b.body_contains, MAX_EXPECT_VALUES, MAX_EXPECT_CHARS)
321
+ ) {
322
+ return { ok: false, error: "evidence.json baseline.body_contains exceeds limits" };
370
323
  }
371
324
  return { ok: true, evidence: raw as unknown as PoCEvidence };
372
325
  }
@@ -444,107 +397,20 @@ export const CONFIRM_DIFFERENTIAL_VALUES = [
444
397
  export type ConfirmDifferential = (typeof CONFIRM_DIFFERENTIAL_VALUES)[number];
445
398
 
446
399
  export const SEVERITY_MATCH_VALUES = ["under", "over", "ok"] as const;
447
- export const CANARY_ASSESSMENT_VALUES = ["verified", "not_applicable"] as const;
448
-
449
- // ── Quorum panel votes (advisory, CONFIRMED-blocking) ───────────────
450
-
451
- export const PANEL_VERDICT_VALUES = ["exploit", "not_exploit", "inconclusive"] as const;
452
- export type PanelVote = {
453
- verdict: (typeof PANEL_VERDICT_VALUES)[number];
454
- rationale: string;
455
- model: string;
456
- at?: string;
457
- };
458
-
459
- /** Bounded panel: enough voices for 2/3 quorum, small enough to stay cheap. */
460
- const MAX_PANEL_VOTES = 5;
461
-
462
- /**
463
- * Validate panel votes recorded on a promotion bundle. Votes are advisory —
464
- * they gate only the CONFIRMED commit (quorum or explicit override note) —
465
- * but their SHAPE is machine-checked so a malformed panel cannot silently
466
- * count as a quorum.
467
- */
468
- export function validatePanelVotes(
469
- raw: unknown,
470
- ): { ok: true; votes: PanelVote[] } | { ok: false; error: string } {
471
- if (!Array.isArray(raw)) return { ok: false, error: "panel_votes must be an array" };
472
- if (raw.length === 0) return { ok: false, error: "panel_votes must not be empty when provided" };
473
- if (raw.length > MAX_PANEL_VOTES) {
474
- return { ok: false, error: `panel_votes exceeds ${MAX_PANEL_VOTES} entries` };
475
- }
476
- for (const [index, v] of raw.entries()) {
477
- if (!isRecord(v)) return { ok: false, error: `panel_votes[${index}] must be an object` };
478
- if (
479
- !nonEmptyString(v.verdict) ||
480
- !(PANEL_VERDICT_VALUES as readonly string[]).includes(v.verdict)
481
- ) {
482
- return {
483
- ok: false,
484
- error: `panel_votes[${index}].verdict must be one of ${PANEL_VERDICT_VALUES.join(" | ")}`,
485
- };
486
- }
487
- if (!nonEmptyString(v.rationale)) {
488
- return { ok: false, error: `panel_votes[${index}].rationale must be a non-empty string` };
489
- }
490
- if (!nonEmptyString(v.model)) {
491
- return { ok: false, error: `panel_votes[${index}].model must be a non-empty string` };
492
- }
493
- if (v.at !== undefined) {
494
- if (!nonEmptyString(v.at) || !Number.isFinite(Date.parse(v.at))) {
495
- return { ok: false, error: `panel_votes[${index}].at must be a parseable timestamp` };
496
- }
497
- }
498
- }
499
- return { ok: true, votes: raw as unknown as PanelVote[] };
500
- }
501
-
502
- /**
503
- * Quorum rule: a panel of at least 3 votes with at least 2 exploit verdicts
504
- * and exploit strictly ahead of not_exploit. Anything else — no panel, a tied
505
- * panel, or a dissenting majority — requires the main agent's explicit
506
- * override note to CONFIRM.
507
- */
508
- export function panelQuorumReached(votes: PanelVote[] | undefined): {
509
- quorum: boolean;
510
- exploit: number;
511
- notExploit: number;
512
- total: number;
513
- } {
514
- const list = votes ?? [];
515
- const exploit = list.filter((v) => v.verdict === "exploit").length;
516
- const notExploit = list.filter((v) => v.verdict === "not_exploit").length;
517
- return {
518
- quorum: list.length >= 3 && exploit >= 2 && exploit > notExploit,
519
- exploit,
520
- notExploit,
521
- total: list.length,
522
- };
523
- }
524
400
 
525
401
  export type MainAgentVerdict = {
526
402
  verdict: ConfirmVerdict;
527
403
  reasoning: string;
528
404
  /** Files/evidence the main agent actually reviewed. */
529
405
  evidence_reviewed: string[];
530
- /** What the main agent observed during its review and fresh harness replay. */
406
+ /** What the main agent observed during review of the runs and transcripts. */
531
407
  re_execution_note?: string;
532
- /** Target vs control evidence comparison. CONFIRMED requires target_only. */
408
+ /** Attack vs baseline evidence comparison. CONFIRMED requires target_only. */
533
409
  differential: ConfirmDifferential;
534
410
  /** Claimed severity vs what the evidence shows. */
535
411
  severity_match?: (typeof SEVERITY_MATCH_VALUES)[number];
536
412
  /** The main agent's own failed attempt to disprove — becomes the case's disconfirmation. */
537
413
  disconfirmation_attempt?: string;
538
- /** Whether the machine replay carried a harness-generated causal canary. */
539
- canary_assessment?: (typeof CANARY_ASSESSMENT_VALUES)[number];
540
- /** Why no meaningful canary oracle exists for this exploit class. */
541
- canary_reason?: string;
542
- /**
543
- * Why CONFIRMED proceeds without a 2/3 exploit panel quorum (no panel
544
- * provisioned, panel unavailable, or documented disagreement). Required for
545
- * CONFIRMED whenever quorum was not reached.
546
- */
547
- panel_override_note?: string;
548
414
  /** Which model judged (recorded for the accuracy ledger). */
549
415
  model?: string;
550
416
  };
@@ -552,8 +418,8 @@ export type MainAgentVerdict = {
552
418
  /**
553
419
  * Validate the main-agent verdict. CONFIRMED additionally requires a target-only
554
420
  * differential, a concrete review note, and a disconfirmation attempt. The
555
- * ledger separately requires a fresh harness-owned phase-2 replay; there is no
556
- * caller-supplied `re_executed` checkbox.
421
+ * verdict is judged against the still-valid phase-1 evidence bundle; there is
422
+ * no caller-supplied `re_executed` checkbox.
557
423
  */
558
424
  export function validateMainAgentVerdict(
559
425
  raw: unknown,
@@ -584,21 +450,6 @@ export function validateMainAgentVerdict(
584
450
  error: `verdict differential must be one of ${CONFIRM_DIFFERENTIAL_VALUES.join(" | ")}`,
585
451
  };
586
452
  }
587
- if (
588
- raw.canary_assessment !== undefined &&
589
- !CANARY_ASSESSMENT_VALUES.includes(raw.canary_assessment as never)
590
- ) {
591
- return {
592
- ok: false,
593
- error: `verdict canary_assessment must be one of ${CANARY_ASSESSMENT_VALUES.join(" | ")}`,
594
- };
595
- }
596
- if (raw.canary_reason !== undefined && !nonEmptyString(raw.canary_reason)) {
597
- return { ok: false, error: "verdict canary_reason must be a non-empty string" };
598
- }
599
- if (raw.panel_override_note !== undefined && !nonEmptyString(raw.panel_override_note)) {
600
- return { ok: false, error: "verdict panel_override_note must be a non-empty string" };
601
- }
602
453
  if (
603
454
  raw.severity_match !== undefined &&
604
455
  !SEVERITY_MATCH_VALUES.includes(raw.severity_match as never)
@@ -613,14 +464,14 @@ export function validateMainAgentVerdict(
613
464
  return {
614
465
  ok: false,
615
466
  error:
616
- 'CONFIRMED requires differential "target_only" — the control run must not demonstrate the claimed impact',
467
+ 'CONFIRMED requires differential "target_only" — the same-host baseline must not demonstrate the claimed impact',
617
468
  };
618
469
  }
619
470
  if (!nonEmptyString(raw.re_execution_note)) {
620
471
  return {
621
472
  ok: false,
622
473
  error:
623
- "CONFIRMED requires re_execution_note — record what the main agent observed during review and the fresh harness replay",
474
+ "CONFIRMED requires re_execution_note — record what the main agent observed during review of the runs and transcripts",
624
475
  };
625
476
  }
626
477
  if (!nonEmptyString(raw.disconfirmation_attempt)) {
@@ -630,18 +481,6 @@ export function validateMainAgentVerdict(
630
481
  "CONFIRMED requires disconfirmation_attempt — the main agent's own failed attempt to disprove",
631
482
  };
632
483
  }
633
- if (!CANARY_ASSESSMENT_VALUES.includes(raw.canary_assessment as never)) {
634
- return {
635
- ok: false,
636
- error: `CONFIRMED requires canary_assessment (${CANARY_ASSESSMENT_VALUES.join(" | ")})`,
637
- };
638
- }
639
- if (raw.canary_assessment === "not_applicable" && !nonEmptyString(raw.canary_reason)) {
640
- return {
641
- ok: false,
642
- error: "CONFIRMED with canary_assessment not_applicable requires canary_reason",
643
- };
644
- }
645
484
  }
646
485
  return { ok: true, verdict: raw as unknown as MainAgentVerdict };
647
486
  }