@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/README.md +2 -3
- package/package.json +1 -2
- package/src/confirmation.ts +45 -351
- package/src/evidence.ts +59 -220
- package/src/harness-verify.ts +36 -206
- package/src/index.ts +44 -366
- package/src/ledger.ts +24 -71
- package/src/poc-runner.ts +3 -3
- package/src/oob-oracle.ts +0 -279
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
|
|
6
|
-
*
|
|
7
|
-
* binds it to the run via $PI_POC_NONCE, and acquires
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
60
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
219
|
+
if (verify.canary !== undefined) {
|
|
271
220
|
return {
|
|
272
221
|
ok: false,
|
|
273
222
|
error:
|
|
274
|
-
"evidence.json verify.
|
|
223
|
+
"evidence.json verify.canary is removed — the 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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
|
406
|
+
/** What the main agent observed during review of the runs and transcripts. */
|
|
531
407
|
re_execution_note?: string;
|
|
532
|
-
/**
|
|
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
|
-
*
|
|
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
|
|
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
|
|
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
|
}
|