@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.
package/src/index.ts CHANGED
@@ -1,30 +1,23 @@
1
1
  /**
2
2
  * Casefile — offensive security case tracker for Pi.
3
3
  *
4
- * Tools: CaseAdd, CaseUpdate, PromoteFinding, ConfirmFinding, EvidenceAdd, CoverageAdd, CoverageReport, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, ScratchpadInit, ScratchpadResume, ScratchpadCheckpoint, ScratchpadWrite, ScratchpadRead, ScratchpadPhaseDone, ScratchpadClear
4
+ * Tools: CaseAdd, CaseUpdate, PromoteFinding, ConfirmFinding, EvidenceAdd, CoverageAdd, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, ScratchpadWrite, ScratchpadRead, ScratchpadClear
5
5
  * Command: /casefile — interactive dashboard
6
6
  * Event: before_agent_start — injects the recon workflow once per session, refreshes the active case list per prompt
7
7
  */
8
8
 
9
9
  import { createHash } from "node:crypto";
10
10
  import { readFileSync } from "node:fs";
11
- import { join } from "node:path";
12
11
  import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
13
12
  import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
14
13
  import { type TSchema, Type } from "typebox";
15
14
  import {
16
- CANARY_ASSESSMENT_VALUES,
17
15
  CONFIRM_DIFFERENTIAL_VALUES,
18
16
  CONFIRM_VERDICT_VALUES,
19
17
  SEVERITY_MATCH_VALUES,
20
18
  validateMainAgentVerdict,
21
19
  } from "./evidence.ts";
22
- import {
23
- controlTargetAuthorizationError,
24
- type HarnessVerifyResult,
25
- replayDifferential,
26
- replayIntraTarget,
27
- } from "./harness-verify.ts";
20
+ import { type HarnessVerifyResult, replayIntraTarget } from "./harness-verify.ts";
28
21
  import {
29
22
  addCaseResult,
30
23
  addEvidenceItemResult,
@@ -43,7 +36,6 @@ import {
43
36
  type CoverageItem,
44
37
  type CoverageScope,
45
38
  countCases,
46
- coverageSummary,
47
39
  EVIDENCE_ROLE_VALUES,
48
40
  type EvidenceItem,
49
41
  type EvidenceRole,
@@ -54,8 +46,6 @@ import {
54
46
  getCasefilePath,
55
47
  LINK_KIND_VALUES,
56
48
  linkCasesResult,
57
- type MainAgentVerification,
58
- type OobVerification,
59
49
  type PendingConfirmation,
60
50
  type PocEvidenceRun,
61
51
  PRIORITY_VALUES,
@@ -69,27 +59,15 @@ import {
69
59
  storePendingConfirmation,
70
60
  unlinkCasesResult,
71
61
  updateCaseResult,
62
+ writeCaseContext,
72
63
  } from "./ledger.ts";
73
- import { writeCaseContext } from "./ledger.ts";
74
- import {
75
- type OobOracleConfig,
76
- type ProvisionedCallback,
77
- provisionCallback,
78
- readOobOracleConfig,
79
- verifyOobDifferential,
80
- } from "./oob-oracle.ts";
81
64
  import { type PocRun, type PocRunOptions, runPoc } from "./poc-runner.ts";
82
65
  import {
83
66
  detectWorkspaceRoot,
84
67
  SCRATCHPAD_PHASES,
85
68
  type ScratchpadPhase,
86
- type ScratchpadResume,
87
- scratchpad_checkpoint,
88
69
  scratchpad_clear,
89
- scratchpad_init,
90
- scratchpad_phase_done,
91
70
  scratchpad_read,
92
- scratchpad_resume,
93
71
  scratchpad_write,
94
72
  setScratchpadRoot,
95
73
  } from "./scratchpad.ts";
@@ -144,6 +122,21 @@ const CommonFields = {
144
122
  "The security invariant this finding violates — the rule broken (e.g. 'a user cannot read another user's orders'). Confirmation checks the invariant is actually violated, not just that a request returned 200.",
145
123
  }),
146
124
  ),
125
+ retry_policy: Type.Optional(
126
+ Type.Object(
127
+ {
128
+ max_attempts: Type.Number({
129
+ description: "Max attempts a phase may take for this case (integer 1–10)",
130
+ }),
131
+ fallback_models: Type.Optional(
132
+ Type.Array(Type.String(), {
133
+ description: "Fallback model identifiers to try when the primary model fails (≤8)",
134
+ }),
135
+ ),
136
+ },
137
+ { additionalProperties: false },
138
+ ),
139
+ ),
147
140
  };
148
141
 
149
142
  // ── Tool: CaseAdd ─────────────────────────────────────────────────────
@@ -190,15 +183,53 @@ const EvidenceAddSchema = Type.Object(
190
183
  { additionalProperties: false },
191
184
  );
192
185
 
186
+ // ── Tool: CoverageAdd ─────────────────────────────────────────────────
187
+
188
+ const CoverageAddSchema = Type.Object(
189
+ {
190
+ case_id: Type.String({
191
+ description: "Case ID (the finding case or target's main case) to record coverage under",
192
+ }),
193
+ asset: Type.String({
194
+ description:
195
+ "The asset tested — copy it verbatim from the case target where shown. For scope=wide use the deployment-wide identifier.",
196
+ }),
197
+ class: Type.String({
198
+ description:
199
+ "The attack class tested (e.g. sql-injection, xss, idor, ssti, ssrf, auth-bypass, ...).",
200
+ }),
201
+ // Provider-safe string enum (per the header rule): Type.Union(Type.Literal)
202
+ // serializes as anyOf/const, which some providers drop — scope would
203
+ // arrive undefined and every explicit 'wide' verdict would silently
204
+ // persist as 'local', under-reporting tested classes.
205
+ scope: Type.String({
206
+ enum: [...COVERAGE_SCOPE_VALUES],
207
+ description:
208
+ "'wide' if the verdict applies to the whole deployment/account/host (recorded ONCE, applies to every asset of the deployment — do NOT re-test it per asset); 'local' if specific to this one asset.",
209
+ }),
210
+ note: Type.String({
211
+ description: "Short note: techniques tried · result · key gap.",
212
+ }),
213
+ evidence_item_id: Type.Optional(
214
+ Type.String({
215
+ description:
216
+ "Optional artifact-backed evidence item (EvidenceAdd, on this case) backing the tested verdict. Cells without one render as unbacked.",
217
+ }),
218
+ ),
219
+ },
220
+ { additionalProperties: false },
221
+ );
222
+
193
223
  // ── Tool: PromoteFinding (phase 1) / ConfirmFinding (phase 2) ──────────
194
224
  //
195
225
  // Confirmation is TWO-PHASE and main-agent-owned: PromoteFinding runs the PoC
196
- // 2x + control, validates nonce-bound evidence.json, and records the pending
197
- // bundle; ConfirmFinding then performs the main coordinator's review/replay and
226
+ // twice against the case target, validates nonce-bound evidence.json, replays
227
+ // the attack and baseline requests itself, and records the pending bundle;
228
+ // ConfirmFinding then performs the main coordinator's semantic review and
198
229
  // commits or refuses the verdict. Subagents may gather or challenge evidence,
199
230
  // but they cannot run validation or confirmation gates. Zero exit is necessary
200
- // run integrity and markers are diagnostic only; the machine records
201
- // predicate/canary differentials and the main agent owns the semantic judgment.
231
+ // run integrity and markers are diagnostic only; the machine records the
232
+ // predicate differential and the main agent owns the semantic judgment.
202
233
 
203
234
  const PromoteSchema = Type.Object(
204
235
  {
@@ -206,38 +237,12 @@ const PromoteSchema = Type.Object(
206
237
  poc_path: Type.String({
207
238
  description: "Absolute path to the PoC script on disk",
208
239
  }),
209
- control_path: Type.Optional(
210
- Type.String({
211
- description:
212
- "Optional absolute path to the SAME script as poc_path (sha256-equality is ENFORCED). Defaults to poc_path. The harness runs it with PI_POC_MODE=control and PI_POC_TARGET=control_target.",
213
- }),
214
- ),
215
- mode: Type.Optional(
216
- Type.String({
217
- enum: ["inter_host", "intra_target"],
218
- description:
219
- "Differential shape. 'inter_host' (default) proves target-dependence with a distinct patched control host — for body-carried proof (file read, injection exfil, info leak, reflection). 'intra_target' proves it with a legitimate same-host baseline request declared in the evidence — for access-control / business-logic classes (IDOR, auth bypass, privilege escalation, logic flaws) where the discriminating variable is identity or a parameter, not the host. In intra_target the evidence must set verify.mode='intra_target' and include a baseline; control_target/control_path are not used.",
220
- }),
221
- ),
222
- control_target: Type.Optional(
223
- Type.String({
224
- minLength: 1,
225
- description:
226
- "REQUIRED for mode='inter_host': a distinct baseline target that lacks the vulnerability and is operator-approved through PI_POC_CONTROL_TARGETS (patched replica, second account, baseline service). Not used for mode='intra_target'.",
227
- }),
228
- ),
229
240
  local: Type.Optional(
230
241
  Type.Boolean({
231
242
  description:
232
243
  "Run with network access instead of --network none. Requires operator authorization via PI_POC_ALLOW_NETWORK=1. True host fallback additionally requires PI_POC_ALLOW_LOCAL=1.",
233
244
  }),
234
245
  ),
235
- oob: Type.Optional(
236
- Type.Boolean({
237
- description:
238
- "Blind/OOB confirmation via the operator-run oracle (PI_OOB_ORACLE_URL). The harness provisions per-run callback tokens, injects PI_POC_CALLBACK_DOMAIN into the runs, and polls the oracle itself: promotion requires target-token interactions, ZERO control-token interactions, attested source separation (PI_OOB_SOURCE_SEPARATED=1), and self-source/missing-src_ip interactions are rejected. Without an oracle this fails closed.",
239
- }),
240
- ),
241
246
  },
242
247
  { additionalProperties: false },
243
248
  );
@@ -257,12 +262,12 @@ const ConfirmSchema = Type.Object(
257
262
  re_execution_note: Type.Optional(
258
263
  Type.String({
259
264
  description:
260
- "What the main agent observed during review and the fresh harness-owned target/control replay. Mandatory for CONFIRMED.",
265
+ "What the main agent observed during review of the runs and transcripts. Mandatory for CONFIRMED.",
261
266
  }),
262
267
  ),
263
268
  differential: Type.String({
264
269
  enum: [...CONFIRM_DIFFERENTIAL_VALUES],
265
- description: "Target vs control evidence comparison. CONFIRMED requires target_only.",
270
+ description: "Attack vs baseline evidence comparison. CONFIRMED requires target_only.",
266
271
  }),
267
272
  severity_match: Type.Optional(
268
273
  Type.String({
@@ -276,19 +281,6 @@ const ConfirmSchema = Type.Object(
276
281
  "The main agent's own failed attempt to disprove — becomes the case's disconfirmation",
277
282
  }),
278
283
  ),
279
- canary_assessment: Type.Optional(
280
- Type.String({
281
- enum: [...CANARY_ASSESSMENT_VALUES],
282
- description:
283
- "verified when the replay carried a harness-generated reflection canary; otherwise not_applicable with a concrete reason",
284
- }),
285
- ),
286
- canary_reason: Type.Optional(
287
- Type.String({
288
- description:
289
- "Why a causal reflection canary is not meaningful for this exploit class. Required when canary_assessment=not_applicable.",
290
- }),
291
- ),
292
284
  model: Type.Optional(
293
285
  Type.String({ description: "Which model judged (recorded for the accuracy ledger)" }),
294
286
  ),
@@ -385,7 +377,7 @@ const ScratchpadPhaseSchema = Type.String({
385
377
  "Pipeline phase: recon | hunt | trace | skeptic | validate | chain | patch | report (legacy gapfil is accepted for older runs)",
386
378
  });
387
379
 
388
- /** run_id-only schema, shared by Scratchpad Init / Resume / Clear. */
380
+ /** run_id-only schema, shared by Scratchpad tools. */
389
381
  const RunIdSchema = Type.Object(
390
382
  {
391
383
  run_id: Type.String({ description: "Pipeline run identifier" }),
@@ -393,20 +385,6 @@ const RunIdSchema = Type.Object(
393
385
  { additionalProperties: false },
394
386
  );
395
387
 
396
- const ScratchpadCheckpointSchema = Type.Object(
397
- {
398
- run_id: Type.String({ description: "Run identifier" }),
399
- phase: ScratchpadPhaseSchema,
400
- ids: Type.Optional(
401
- Type.Array(Type.String(), {
402
- description: "Key IDs produced by this phase (case IDs, finding IDs)",
403
- }),
404
- ),
405
- summary: Type.Optional(Type.String({ description: "One-line summary of phase completion" })),
406
- },
407
- { additionalProperties: false },
408
- );
409
-
410
388
  const ScratchpadWriteSchema = Type.Object(
411
389
  {
412
390
  run_id: Type.String({ description: "Run identifier" }),
@@ -428,14 +406,6 @@ const ScratchpadReadSchema = Type.Object(
428
406
  { additionalProperties: false },
429
407
  );
430
408
 
431
- const ScratchpadPhaseDoneSchema = Type.Object(
432
- {
433
- run_id: Type.String({ description: "Run identifier" }),
434
- phase: ScratchpadPhaseSchema,
435
- },
436
- { additionalProperties: false },
437
- );
438
-
439
409
  interface Theme {
440
410
  fg(color: string, text: string): string;
441
411
  bold(text: string): string;
@@ -733,7 +703,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
733
703
  // already-loaded extension or reveal a tool that was omitted at startup.
734
704
  const startedAsSubagent = process.env.PI_SUBAGENT_CHILD === "1";
735
705
  const isSubagentProcess = () => startedAsSubagent || process.env.PI_SUBAGENT_CHILD === "1";
736
- // Pin the workspace root ONCE at extension load. Every scratchpad / pipeline
706
+ // Pin the workspace root ONCE at extension load. Every scratchpad
737
707
  // / PoC-path lookup otherwise re-walks the ambient cwd on each call — a
738
708
  // mid-session `cd` would split state across two .scratchpad roots and
739
709
  // misroot the hunt file-existence filter. The PoC runner reads PI_POC_ROOT
@@ -790,7 +760,13 @@ export default function casefileExtension(pi: ExtensionAPI) {
790
760
  parameters: AddSchema,
791
761
 
792
762
  async execute(_id, params, _signal, _onUpdate, _ctx) {
793
- const result = addCaseResult(params as CaseInput);
763
+ const { retry_policy, ...rest } = params as Record<string, unknown>;
764
+ const result = addCaseResult({
765
+ ...(rest as CaseInput),
766
+ ...(retry_policy !== undefined
767
+ ? { retryPolicy: retry_policy as CaseInput["retryPolicy"] }
768
+ : {}),
769
+ });
794
770
  const record = result.record;
795
771
  return {
796
772
  content: [
@@ -842,8 +818,14 @@ export default function casefileExtension(pi: ExtensionAPI) {
842
818
  parameters: UpdateSchema,
843
819
 
844
820
  async execute(_id, params, _signal, _onUpdate, _ctx) {
845
- const { id, ...update } = params;
846
- const result = updateCaseResult(id as string, update as CaseUpdate);
821
+ const { id, retry_policy, ...rest } = params as Record<string, unknown>;
822
+ const update = {
823
+ ...(rest as CaseUpdate),
824
+ ...(retry_policy !== undefined
825
+ ? { retryPolicy: retry_policy as CaseUpdate["retryPolicy"] }
826
+ : {}),
827
+ };
828
+ const result = updateCaseResult(id as string, update);
847
829
  const record = result.record;
848
830
  return {
849
831
  content: [
@@ -908,7 +890,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
908
890
  content: [
909
891
  {
910
892
  type: "text",
911
- text: `Evidence item recorded:\n[${item.role}] ${item.summary}${item.artifactPath ? ` — ${item.artifactPath} sha256:${item.sha256?.slice(0, 12)}…` : ""}\n\n${formatCaseDetail(record)}`,
893
+ text: `Evidence item recorded:\n[${item.role}] ${item.summary}${item.artifactPath ? ` — ${item.artifactPath} sha256:${item.sha256?.slice(0, 12)}…` : ""}${item.containsSecret ? `\n⚠ Artifact contains suspected secrets (${item.secretFindings?.join(", ")}) — stored and hashed, but REDACT these values in any export or report.` : ""}\n\n${formatCaseDetail(record)}`,
912
894
  },
913
895
  ],
914
896
  details: { item, record },
@@ -938,158 +920,6 @@ export default function casefileExtension(pi: ExtensionAPI) {
938
920
  },
939
921
  });
940
922
 
941
- // ── Tool: CoverageAdd ──
942
-
943
- const CoverageAddSchema = Type.Object(
944
- {
945
- case_id: Type.String({
946
- description: "Case ID (the pipeline-run or finding case) to record coverage under",
947
- }),
948
- asset: Type.String({
949
- description:
950
- "The asset tested — copy it verbatim from the case target where shown. For scope=wide use the deployment-wide identifier.",
951
- }),
952
- class: Type.String({
953
- description:
954
- "The attack class tested (e.g. sql-injection, xss, idor, ssti, ssrf, auth-bypass, ...).",
955
- }),
956
- // Provider-safe string enum (per the header rule): Type.Union(Type.Literal)
957
- // serializes to anyOf/const, which some providers drop — scope would
958
- // arrive undefined and every explicit 'wide' verdict would silently
959
- // persist as 'local', under-reporting tested classes.
960
- scope: Type.String({
961
- enum: [...COVERAGE_SCOPE_VALUES],
962
- description:
963
- "'wide' if the verdict applies to the whole deployment/account/host (recorded ONCE, applies to every asset of the deployment — do NOT re-test it per asset); 'local' if specific to this one asset.",
964
- }),
965
- note: Type.String({
966
- description:
967
- "Short note of the tests ACTUALLY RUN and the verdict: techniques tried · result · key gap. A verdict guessed without testing can hide a real issue.",
968
- }),
969
- evidence_item_id: Type.Optional(
970
- Type.String({
971
- description:
972
- "Evidence item id backing this tested verdict (must be an artifact-backed EvidenceAdd item on this case). Cells without a backing item render as 'unbacked' in CoverageReport — 'tested' claims must be machine-checkable, not prose-only.",
973
- }),
974
- ),
975
- },
976
- { additionalProperties: false },
977
- );
978
-
979
- registerCaseTool({
980
- name: "CoverageAdd",
981
- label: "Record Coverage",
982
- description:
983
- "Record what you tested so it is not re-tested. Call AFTER finishing a CLASS of issue, for BOTH outcomes (found or clean — a clean result is just as important to record). scope=wide: the verdict is a property of the whole deployment, recorded once and applied to every later asset (do NOT re-test per asset); scope=local: specific to this one asset. The cell's existence marks that class tested for that asset.",
984
- promptSnippet: "Record a tested attack class (coverage)",
985
- promptGuidelines: [
986
- "Record a coverage cell after you finish testing a class on an asset — found OR clean. Clean results are what make 'every class is COVERED' machine-checkable.",
987
- "scope=wide for deployment-wide verdicts (record once, applies to every asset of the deployment — do NOT re-test it per asset). scope=local for single-asset verdicts.",
988
- "The note must describe tests you ACTUALLY RAN, not assumptions. A verdict guessed without testing can hide a real issue.",
989
- "Coverage cells live on the pipeline-run case (or the target's main case); CoverageReport shows the matrix.",
990
- ],
991
- parameters: CoverageAddSchema,
992
-
993
- async execute(_id, params, _signal, _onUpdate, _ctx) {
994
- const item = recordCoverageResult(params.case_id as string, {
995
- asset: params.asset as string,
996
- class: params.class as string,
997
- scope: (params.scope ?? "local") as CoverageScope,
998
- note: params.note as string,
999
- evidenceItemId: params.evidence_item_id as string | undefined,
1000
- });
1001
- const record = getCaseById(params.case_id as string);
1002
- if (!record) throw new Error(`Case not found after coverage insert: ${params.case_id}`);
1003
- return {
1004
- content: [
1005
- {
1006
- type: "text",
1007
- text: `Coverage recorded: [${item.scope}] ${item.asset} × ${item.class} — ${item.note}\n\n${formatCaseDetail(record)}`,
1008
- },
1009
- ],
1010
- details: { item, record },
1011
- };
1012
- },
1013
-
1014
- renderCall(args, theme) {
1015
- return callLine(
1016
- theme,
1017
- "CoverageAdd",
1018
- `${(args.asset as string) ?? ""} [${(args.class as string) ?? ""}]`,
1019
- );
1020
- },
1021
-
1022
- renderResult(result, _opts, theme) {
1023
- const details = result.details as { item?: CoverageItem } | undefined;
1024
- if (!details?.item) {
1025
- return new Text(theme.fg("error", "✗ CoverageAdd failed"), 0, 0);
1026
- }
1027
- return new Text(
1028
- theme.fg("success", "✓ ") +
1029
- theme.fg("dim", `[${details.item.scope}] `) +
1030
- truncateToWidth(`${details.item.asset} × ${details.item.class}`, 50),
1031
- 0,
1032
- 0,
1033
- );
1034
- },
1035
- });
1036
-
1037
- // ── Tool: CoverageReport ──
1038
-
1039
- const CoverageReportSchema = Type.Object(
1040
- {
1041
- case_id: Type.String({ description: "Case ID to render the coverage matrix for" }),
1042
- },
1043
- { additionalProperties: false },
1044
- );
1045
-
1046
- registerCaseTool({
1047
- name: "CoverageReport",
1048
- label: "Coverage Matrix",
1049
- description:
1050
- "Render the machine-checkable coverage matrix for a case: which (asset × attack-class) cells are tested, with wide-verdict propagation. Run before deciding HUNT coverage is done — the plateau stop (zero new classes testable) must be visible in the matrix, not asserted in prose.",
1051
- promptSnippet: "Show which attack classes were tested where",
1052
- promptGuidelines: [
1053
- "Run CoverageReport before claiming 'every class is COVERED/SKIPPED/NOT_FOUND' — the claim must match the matrix.",
1054
- "A class with a wide clean verdict covers every asset — do NOT re-test it per asset.",
1055
- "Classes tested with no cell recorded are invisible: record coverage as you finish each class (CoverageAdd).",
1056
- ],
1057
- parameters: CoverageReportSchema,
1058
-
1059
- async execute(_id, params, _signal, _onUpdate, _ctx) {
1060
- const summary = coverageSummary(params.case_id as string);
1061
- const lines: string[] = [`Coverage matrix for ${params.case_id}:`];
1062
- for (const asset of summary.assets) {
1063
- lines.push(`\n## ${asset}`);
1064
- for (const cell of summary.byAsset[asset] ?? []) {
1065
- lines.push(
1066
- `- [${cell.scope}] ${cell.class} — ${cell.note}${cell.testedBy ? ` (by ${cell.testedBy})` : ""}` +
1067
- (cell.evidenceItemId
1068
- ? ""
1069
- : " ⚠ unbacked (link an artifact-backed evidence item via CoverageAdd evidence_item_id)"),
1070
- );
1071
- }
1072
- }
1073
- if (summary.items.length === 0) {
1074
- lines.push("\n(no coverage recorded yet — run CoverageAdd as each class is tested)");
1075
- }
1076
- return {
1077
- content: [{ type: "text", text: lines.join("\n") }],
1078
- details: { summary },
1079
- };
1080
- },
1081
-
1082
- renderCall(args, theme) {
1083
- return callLine(theme, "CoverageReport", (args.case_id as string) ?? "");
1084
- },
1085
-
1086
- renderResult(result, _opts, theme) {
1087
- const details = result.details as { summary?: { items?: CoverageItem[] } } | undefined;
1088
- const n = details?.summary?.items?.length ?? 0;
1089
- return new Text(theme.fg("success", `✓ ${n} coverage cell(s)`), 0, 0);
1090
- },
1091
- });
1092
-
1093
923
  // ── Tool: PromoteFinding (phase 1) ──
1094
924
 
1095
925
  if (!startedAsSubagent)
@@ -1097,19 +927,15 @@ export default function casefileExtension(pi: ExtensionAPI) {
1097
927
  name: "PromoteFinding",
1098
928
  label: "Run PoC Evidence",
1099
929
  description:
1100
- "Main-agent phase 1 of confirmation: run the same PoC twice against the case target and once against an operator-approved control_target, validate nonce-bound evidence.json with a response-body assertion, then have the harness execute one immutable HTTP request template against both target and control. control_path defaults to poc_path; if supplied, sha256 equality is enforced. The machine records a predicate differential, or a stronger canary differential when a reflection placeholder is requested and observed only on target; neither is automatically a vulnerability verdict. Exit 0 is necessary run integrity, never proof. Networked execution, controls, and private replay are operator-gated. Blind/OOB confirmation fails closed until source separation exists. Records a pending bundle for main-agent semantic review via ConfirmFinding. Worker/subagent processes are rejected.",
1101
- promptSnippet:
1102
- "Phase 1: run PoC evidence (target x2 + control) and record the pending bundle",
930
+ "Main-agent phase 1 of confirmation: run the same PoC twice against the case target, validate nonce-bound evidence.json with a response-body assertion, then have the harness replay the attack request and a legitimate same-host baseline request itself. The machine records a predicate differential (attack matched, baseline did not); that is not automatically a vulnerability verdict. Exit 0 is necessary run integrity, never proof. Networked execution and private replay are operator-gated. Records a pending bundle for main-agent semantic review via ConfirmFinding. Worker/subagent processes are rejected.",
931
+ promptSnippet: "Phase 1: run PoC evidence (target x2) and record the pending bundle",
1103
932
  promptGuidelines: [
1104
933
  "Use PromoteFinding only from the main/coordinator agent when an investigating case has a concrete PoC script on disk and you are ready to subject its claim to the machine gate.",
1105
934
  "Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target, plus an artifact-backed EvidenceAdd 'observation' item on the case (the initial signal, with artifact_path). The final disconfirmation comes from the main agent at confirm time.",
1106
- "The PoC MUST write evidence.json to $PI_POC_EVIDENCE_DIR: { nonce (echo $PI_POC_NONCE), claim, verify: { method, url, expect: { status?, body_contains/body_regex } }, observations }. A non-empty body predicate is mandatory; status-only evidence is rejected. verify.url must belong to the case target.",
1107
- "For reflection-capable requests, place {{PI_POC_CANARY}} exactly once in verify.url/body/header values and declare verify.canary={mode:'reflection',placeholder:'{{PI_POC_CANARY}}'}. The harness substitutes an unpredictable value only after the PoC exits and requires target-only reflection; the raw token is not persisted.",
1108
- "control_path is optional and defaults to poc_path; if supplied, it must be the SAME script as poc_path. control_target must be pre-approved by the operator in PI_POC_CONTROL_TARGETS. The harness derives the control request from the target request, changes only its origin, and applies the same predicates to two conclusive responses.",
1109
- "oob=true unlocks blind/OOB classes (SSRF, blind XSS, XXE): the harness provisions per-run callback tokens via the operator's oracle (PI_OOB_ORACLE_URL), injects PI_POC_CALLBACK_DOMAIN into the runs, and polls the oracle itself — target-token interactions with ZERO control-token interactions are required. Promotion additionally requires attested source separation (PI_OOB_SOURCE_SEPARATED=1); without it the verification stays diagnostic.",
935
+ "The PoC MUST write evidence.json to $PI_POC_EVIDENCE_DIR: { nonce (echo $PI_POC_NONCE), claim, verify: { method, url, expect: { status?, body_contains/body_regex } }, observations, baseline }. A non-empty body predicate is mandatory; status-only evidence is rejected. verify.url and baseline.url must belong to the case target.",
936
+ "baseline is a legitimate same-host request whose response must NOT satisfy the attack predicate your own account's resource for IDOR, the request without the payload for injection. It must differ from the attack request (identity or a parameter, not just whitespace).",
1110
937
  "local:true requires PI_POC_ALLOW_NETWORK=1. Private/internal harness replay additionally requires PI_POC_ALLOW_PRIVATE_REPLAY=1. Neither silently falls back to a model verdict.",
1111
- "Blind/OOB classes fail closed unless the operator configured an OOB oracle; self-interactions (PI_OOB_SELF_IPS) are rejected and never counted as target hits.",
1112
- "After the bundle is recorded, stay in the main agent: inspect the script/evidence, attempt disconfirmation, and call ConfirmFinding itself; that call performs a fresh harness-owned target/control replay. Never delegate validation/confirmation and never CaseUpdate status='confirmed' directly.",
938
+ "After the bundle is recorded, stay in the main agent: inspect the script/evidence, attempt disconfirmation, and call ConfirmFinding yourself. Never delegate validation/confirmation and never CaseUpdate status='confirmed' directly.",
1113
939
  ],
1114
940
  parameters: PromoteSchema,
1115
941
 
@@ -1126,167 +952,65 @@ export default function casefileExtension(pi: ExtensionAPI) {
1126
952
  const caseId = params.id as string;
1127
953
  const current = assertPromotable(caseId);
1128
954
 
1129
- const fail = (text: string, _extra?: Record<string, unknown>): never => {
955
+ const fail = (text: string): never => {
1130
956
  throw new Error(text);
1131
957
  };
1132
958
 
1133
959
  const pocPath = (params.poc_path as string | undefined)?.trim() ?? "";
1134
- const controlPath = (params.control_path as string | undefined)?.trim() || pocPath;
1135
- const controlTarget = (params.control_target as string | undefined)?.trim() ?? "";
1136
- const mode: "inter_host" | "intra_target" =
1137
- (params.mode as string | undefined) === "intra_target" ? "intra_target" : "inter_host";
1138
- const isIntra = mode === "intra_target";
1139
960
  if (!pocPath) {
1140
- return fail("poc_path is REQUIRED: absolute path to the PoC script run by the harness.", {
1141
- missingPocPath: true,
1142
- });
961
+ return fail("poc_path is REQUIRED: absolute path to the PoC script run by the harness.");
1143
962
  }
1144
963
  const caseTarget = current.target ?? "";
1145
- // ── OOB callback (Tier 1, opt-in for blind classes) ──
1146
- // The operator-run oracle owns the evidence channel; the harness owns
1147
- // the secret (per-run token, provisioned before the runs and injected
1148
- // as env — the value does not exist when the script was written).
1149
- // Without an oracle this stays fail-closed.
1150
- const oobRequested = params.oob === true;
1151
- // Intra-target + OOB is rejected up front: intra_target's
1152
- // discriminating variable is identity/parameter on the SAME host;
1153
- // mixing it with a callback differential would make precedence
1154
- // ambiguous. OOB is for inter-host/blind classes.
1155
- if (isIntra && oobRequested) {
1156
- return fail(
1157
- "mode:'intra_target' cannot be combined with oob:true — intra-target proof uses a same-host baseline request, not a callback channel. Use one or the other.",
1158
- { intraOobConflict: true },
1159
- );
1160
- }
1161
- let oobConfig: OobOracleConfig | undefined;
1162
- let targetCallback: ProvisionedCallback | undefined;
1163
- let controlCallback: ProvisionedCallback | undefined;
1164
- if (oobRequested) {
1165
- const oracle = readOobOracleConfig();
1166
- if (!oracle.config) {
1167
- return fail(`OOB CONFIRMATION UNAVAILABLE: ${oracle.error}`, {
1168
- oobOracleNotConfigured: true,
1169
- });
1170
- }
1171
- oobConfig = oracle.config;
1172
- // Provision both identities concurrently — each is an oracle round trip.
1173
- [targetCallback, controlCallback] = await Promise.all([
1174
- provisionCallback(oobConfig),
1175
- provisionCallback(oobConfig),
1176
- ]);
1177
- }
1178
- // OOB-only bundles (blind classes, no operator-approved control host)
1179
- // prove target-dependence via the token differential instead.
1180
- const oobOnly = oobRequested && !controlTarget;
1181
- if (!isIntra && !oobOnly) {
1182
- if (!controlTarget) {
1183
- return fail(
1184
- "control_target is REQUIRED for inter-host mode: a distinct baseline target that lacks the vulnerability. For access-control/logic bugs use mode='intra_target' with an evidence baseline instead; for blind/OOB classes pass oob=true (with or without a control target).",
1185
- { missingControlTarget: true },
1186
- );
1187
- }
1188
- if (controlTarget === current.target) {
1189
- return fail(
1190
- "control_target must differ from the case target; a control run against the vulnerable target proves nothing.",
1191
- { controlTargetEqualsCaseTarget: true },
1192
- );
1193
- }
1194
- }
1195
964
  if (params.local === true && process.env.PI_POC_ALLOW_NETWORK !== "1") {
1196
965
  return fail(
1197
966
  "Networked PoC execution is operator-gated. Set PI_POC_ALLOW_NETWORK=1 to authorize the host-network sandbox for this session.",
1198
- { networkNotAuthorized: true },
1199
967
  );
1200
968
  }
1201
- if (!isIntra && !oobOnly) {
1202
- const controlAuthorization = controlTargetAuthorizationError(controlTarget);
1203
- if (controlAuthorization) {
1204
- return fail(
1205
- `CONTROL AUTHORIZATION FAILED: ${controlAuthorization}. ` +
1206
- "The operator must set PI_POC_CONTROL_TARGETS to the exact approved control host/origin before this control can anchor confirmation.",
1207
- { controlNotAuthorized: true },
1208
- );
1209
- }
1210
- }
1211
969
 
1212
- // Anti-cheat: hash the PoC (always) and, for inter-host, require the
1213
- // control script to be the SAME bytes (differing only via harness env).
970
+ // Anti-cheat: hash the PoC so the recorded bundle is bound to the exact
971
+ // bytes that ran (re-verified at confirm time).
1214
972
  let pocHash: string | undefined;
1215
973
  try {
1216
974
  pocHash = createHash("sha256").update(readFileSync(pocPath)).digest("hex");
1217
975
  } catch (e) {
1218
- return fail(`Cannot read PoC script: ${(e as Error).message}`, {
1219
- sameFileCheckFailed: true,
1220
- });
1221
- }
1222
- if (!isIntra && !oobOnly) {
1223
- let controlHash: string | undefined;
1224
- try {
1225
- controlHash = createHash("sha256").update(readFileSync(controlPath)).digest("hex");
1226
- } catch (e) {
1227
- return fail(
1228
- `Cannot read control script for the same-file check: ${(e as Error).message}`,
1229
- { sameFileCheckFailed: true },
1230
- );
1231
- }
1232
- if (pocHash !== controlHash) {
1233
- return fail(
1234
- "CONTROL CHECK FAILED: control_path must be the SAME script as poc_path (sha256 mismatch). Case remains investigating.",
1235
- { controlHashMismatch: true },
1236
- );
1237
- }
976
+ return fail(`Cannot read PoC script: ${(e as Error).message}`);
1238
977
  }
1239
978
 
1240
- // ── OOB callback tokens were provisioned above, before the runs ──
1241
- const runOptions = (pocMode: string, target: string): PocRunOptions => ({
979
+ const runOptions = (target: string): PocRunOptions => ({
1242
980
  network: params.local === true ? "host" : "none",
1243
981
  local: params.local === true,
1244
982
  env: {
1245
- PI_POC_MODE: pocMode,
983
+ PI_POC_MODE: "poc",
1246
984
  PI_POC_TARGET: target,
1247
- ...(oobRequested && targetCallback && controlCallback
1248
- ? {
1249
- PI_POC_CALLBACK_DOMAIN:
1250
- pocMode === "control" ? controlCallback.domain : targetCallback.domain,
1251
- }
1252
- : {}),
1253
985
  },
1254
986
  });
1255
987
 
1256
988
  // Determinism: TWO target runs. Exit 0 is run integrity only; nonce-bound
1257
- // body evidence plus the harness-owned differential replay (inter-host
1258
- // control, or intra-target same-host baseline) form the machine gate.
1259
- const run1 = runPoc(pocPath, runOptions("poc", caseTarget));
1260
- const run2 = runPoc(pocPath, runOptions("poc", caseTarget));
1261
-
1262
- const evidenceRun = (
1263
- r: PocRun,
1264
- mode: "poc" | "control",
1265
- target: string,
1266
- ): PocEvidenceRun => {
989
+ // body evidence plus the harness-owned attack/baseline replay form the
990
+ // machine gate.
991
+ const run1 = runPoc(pocPath, runOptions(caseTarget));
992
+ const run2 = runPoc(pocPath, runOptions(caseTarget));
993
+
994
+ const evidenceRun = (r: PocRun, target: string): PocEvidenceRun => {
1267
995
  if (!r.completed || !r.outputComplete) {
1268
996
  return fail(
1269
- `${mode} run did not complete or output capture was incomplete` +
997
+ `PoC run did not complete or output capture was incomplete` +
1270
998
  (r.infraError ? ` (infra: ${r.output.trim()})` : "") +
1271
999
  ". A crash is not evidence. Case remains investigating.",
1272
- { run: r, pocCrashed: true },
1273
1000
  );
1274
1001
  }
1275
1002
  if (r.evidenceError) {
1276
1003
  return fail(
1277
- `EVIDENCE CONTRACT FAILED (${mode} run): ${r.evidenceError}. ` +
1278
- "The PoC must write evidence.json to $PI_POC_EVIDENCE_DIR — { nonce (echo $PI_POC_NONCE), claim, verify: { method, url, expect: { status?, body_contains / body_regex } }, observations }; a response-body assertion is mandatory — " +
1004
+ `EVIDENCE CONTRACT FAILED: ${r.evidenceError}. ` +
1005
+ "The PoC must write evidence.json to $PI_POC_EVIDENCE_DIR — { nonce (echo $PI_POC_NONCE), claim, verify: { method, url, expect: { status?, body_contains / body_regex } }, observations, baseline }; a response-body assertion is mandatory — " +
1279
1006
  "the file is bound to this run and validated by the harness. Case remains investigating.",
1280
- { run: r, evidenceError: r.evidenceError },
1281
1007
  );
1282
1008
  }
1283
1009
  if (!r.evidence || !r.evidenceSha256 || !r.nonce) {
1284
- return fail(`${mode} run produced no evidence. Case remains investigating.`, {
1285
- run: r,
1286
- });
1010
+ return fail("PoC run produced no evidence. Case remains investigating.");
1287
1011
  }
1288
1012
  return {
1289
- mode,
1013
+ mode: "poc",
1290
1014
  target,
1291
1015
  nonce: r.nonce,
1292
1016
  ranAt: r.ranAt,
@@ -1302,100 +1026,33 @@ export default function casefileExtension(pi: ExtensionAPI) {
1302
1026
  };
1303
1027
 
1304
1028
  const targetRuns: [PocEvidenceRun, PocEvidenceRun] = [
1305
- evidenceRun(run1, "poc", caseTarget),
1306
- evidenceRun(run2, "poc", caseTarget),
1029
+ evidenceRun(run1, caseTarget),
1030
+ evidenceRun(run2, caseTarget),
1307
1031
  ];
1308
1032
 
1309
- // Reflection canary + OOB is rejected after run 1 (the canary is
1310
- // declared inside evidence.json): the canary path requires a harness
1311
- // response transcript, which OOB-only bundles never produce — the
1312
- // per-run callback token IS the causality signal there.
1313
- if (oobRequested && targetRuns.some((r) => r.evidence.verify.canary !== undefined)) {
1314
- return fail(
1315
- "verify.canary cannot be combined with oob:true — the per-run callback token already provides a harness-owned causality signal. Remove the {{PI_POC_CANARY}} placeholder and verify.canary from evidence.json, then re-promote.",
1316
- { canaryOobConflict: true },
1317
- );
1318
- }
1319
1033
  const allowPrivateReplay = process.env.PI_POC_ALLOW_PRIVATE_REPLAY === "1";
1320
- let harnessVerified: HarnessVerifyResult | undefined;
1321
- let controlRun: PocEvidenceRun | undefined;
1322
- if (isIntra) {
1323
- // Intra-target: prove target-dependence with the evidence's same-host
1324
- // baseline request — no separate control run. The harness sends attack +
1325
- // baseline to the case target and requires the proof on attack only.
1326
- const ev0 = targetRuns[0].evidence;
1327
- if (ev0.verify.mode !== "intra_target") {
1328
- return fail(
1329
- "INTRA-TARGET FAILED: the PoC's evidence.json must set verify.mode='intra_target' when promoting in intra-target mode.",
1330
- { intraModeMismatch: true },
1331
- );
1332
- }
1333
- if (!ev0.baseline) {
1334
- return fail(
1335
- "INTRA-TARGET FAILED: evidence.json must include a baseline — a legitimate same-host request whose response must NOT satisfy the attack predicate.",
1336
- { intraBaselineMissing: true },
1337
- );
1338
- }
1339
- harnessVerified = await replayIntraTarget(ev0, caseTarget, {
1340
- allowPrivate: allowPrivateReplay,
1341
- });
1342
- } else if (!oobOnly) {
1343
- // Inter-host (Tier 2): the harness executes the SAME request template
1344
- // against target and operator-approved control, applying the target's
1345
- // predicates to both. DNS is pinned at connect time.
1346
- controlRun = evidenceRun(
1347
- runPoc(controlPath, runOptions("control", controlTarget)),
1348
- "control",
1349
- controlTarget,
1350
- );
1351
- harnessVerified = await replayDifferential(
1352
- targetRuns[0].evidence,
1353
- caseTarget,
1354
- controlTarget,
1355
- { allowPrivate: allowPrivateReplay },
1356
- );
1357
- }
1358
- // OOB differential: poll the oracle for both run tokens. The ledger's
1359
- // assertMachineConfirmation consumes this BEFORE the response-diff
1360
- // requirement — blind classes pass via this path when the oracle saw
1361
- // the target token and NOT the control token under attested source
1362
- // separation.
1363
- let callbackVerified: OobVerification | undefined;
1364
- if (oobConfig && targetCallback && controlCallback) {
1365
- callbackVerified = (
1366
- await verifyOobDifferential({
1367
- targetToken: targetCallback.token,
1368
- controlToken: controlCallback.token,
1369
- })
1370
- ).verification;
1371
- }
1034
+ // Intra-target differential: prove target-dependence with the evidence's
1035
+ // same-host baseline request. The harness sends attack + baseline to the
1036
+ // case target and requires the proof on attack only.
1037
+ const harnessVerified: HarnessVerifyResult = await replayIntraTarget(
1038
+ targetRuns[0].evidence,
1039
+ caseTarget,
1040
+ { allowPrivate: allowPrivateReplay },
1041
+ );
1372
1042
  const bundle: PendingConfirmation = {
1373
1043
  caseId,
1374
1044
  ranAt: new Date().toISOString(),
1375
1045
  pocPath,
1376
1046
  pocSha256: pocHash,
1377
- mode,
1378
1047
  targetRuns,
1379
1048
  harnessVerified,
1380
- ...(callbackVerified && targetCallback && controlCallback
1381
- ? {
1382
- callbackVerified,
1383
- oobTokens: {
1384
- targetToken: targetCallback.token,
1385
- controlToken: controlCallback.token,
1386
- },
1387
- }
1388
- : {}),
1389
- ...(!(isIntra || oobOnly) ? { controlPath, controlTarget, controlRun } : {}),
1390
1049
  };
1391
1050
 
1392
1051
  let record: CaseRecord;
1393
1052
  try {
1394
1053
  record = storePendingConfirmation(caseId, bundle);
1395
1054
  } catch (e) {
1396
- return fail(`Pending confirmation rejected: ${(e as Error).message}`, {
1397
- storeRejected: true,
1398
- });
1055
+ return fail(`Pending confirmation rejected: ${(e as Error).message}`);
1399
1056
  }
1400
1057
 
1401
1058
  return {
@@ -1404,15 +1061,12 @@ export default function casefileExtension(pi: ExtensionAPI) {
1404
1061
  type: "text",
1405
1062
  text:
1406
1063
  `Phase 1 complete — evidence bundle recorded on ${caseId} (expires in 1h).\n` +
1407
- `Mode: ${mode}. ${isIntra ? "Target runs: 2, same-host baseline differential" : "Target runs: 2, Control run: 1"} — all with validated nonce-bound evidence.json.\n` +
1064
+ `Target runs: 2, same-host baseline differential — all with validated nonce-bound evidence.json.\n` +
1408
1065
  `Evidence sha256: ${targetRuns[0].evidenceSha256}\n` +
1409
1066
  `PoC script sha256 (at run time): ${pocHash}\n` +
1410
1067
  `Harness verify replay: ${harnessVerified?.attempted ? (harnessVerified.pass ? `PASS (status ${harnessVerified.status})` : `FAILED — ${harnessVerified.note}`) : (harnessVerified?.note ?? "not run")}
1411
1068
  ` +
1412
- (callbackVerified
1413
- ? `OOB oracle: target-token hits ${callbackVerified.targetHits}, control-token hits ${callbackVerified.controlHits}, source-separated: ${String(callbackVerified.sourceSeparated)} — ${callbackVerified.note}\n`
1414
- : "") +
1415
- `\nMAIN-AGENT REVIEW REQUIRED (do not delegate): inspect case ${caseId}, PoC ${pocPath}, ${isIntra ? "same-host baseline" : `control ${controlTarget}`}, evidence ${targetRuns[0].evidenceSha256}, and PoC hash ${pocHash}. Hunt for a trivial predicate or fabricated differential and perform a concrete disconfirmation attempt, then call ConfirmFinding yourself. A CONFIRMED call performs and stores a fresh harness-owned ${isIntra ? "attack/baseline" : "target/control"} replay; NOT_CONFIRMED keeps the case investigating.`,
1069
+ `\nMAIN-AGENT REVIEW REQUIRED (do not delegate): inspect case ${caseId}, PoC ${pocPath}, the same-host baseline, evidence ${targetRuns[0].evidenceSha256}, and PoC hash ${pocHash}. Hunt for a trivial predicate or fabricated differential and perform a concrete disconfirmation attempt, then call ConfirmFinding yourself. NOT_CONFIRMED keeps the case investigating.`,
1416
1070
  },
1417
1071
  ],
1418
1072
  details: {
@@ -1420,10 +1074,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
1420
1074
  bundle: {
1421
1075
  caseId,
1422
1076
  ranAt: bundle.ranAt,
1423
- mode,
1424
1077
  pocPath,
1425
- controlPath: isIntra ? undefined : controlPath,
1426
- controlTarget: isIntra ? undefined : controlTarget,
1427
1078
  pocSha256: pocHash,
1428
1079
  evidenceSha256: targetRuns[0].evidenceSha256,
1429
1080
  harnessVerified,
@@ -1461,15 +1112,15 @@ export default function casefileExtension(pi: ExtensionAPI) {
1461
1112
  name: "ConfirmFinding",
1462
1113
  label: "Main-Agent Confirmation",
1463
1114
  description:
1464
- "Phase 2 of confirmation, reserved for the main/coordinator agent: commit or refuse promotion after independently re-testing the finding. On CONFIRMED, this tool performs a fresh harness-owned target/control replay; the verdict requires a target-only differential, a concrete re_execution_note and disconfirmation_attempt, a canary assessment, and the still-valid PromoteFinding bundle. The machine transcript is evidence, not the semantic vulnerability verdict. Worker/subagent processes are rejected. Three verdicts: CONFIRMED (you reproduced real impact), NOT_CONFIRMED (you POSITIVELY disproved it), INCONCLUSIVE (you could neither reproduce nor disprove — the case is preserved for manual review, never dropped).",
1115
+ "Phase 2 of confirmation, reserved for the main/coordinator agent: commit or refuse promotion after independently reviewing the recorded evidence. The verdict requires a target-only differential, a concrete re_execution_note and disconfirmation_attempt, and the still-valid PromoteFinding bundle. The machine floor is the promote-time harness replay plus confirm-time bundle re-validation — there is no fresh network replay at verdict time, so weigh bundle age (1h TTL) in your review. The machine transcript is evidence, not the semantic vulnerability verdict. Worker/subagent processes are rejected. Three verdicts: CONFIRMED (you reproduced real impact), NOT_CONFIRMED (you POSITIVELY disproved it), INCONCLUSIVE (you could neither reproduce nor disprove — the case is preserved for manual review, never dropped).",
1465
1116
  promptSnippet: "Main agent: independently re-test, then commit or refuse PoC confirmation",
1466
1117
  promptGuidelines: [
1467
1118
  "Run only in the main/coordinator agent after PromoteFinding returns. Do not dispatch a worker to decide or author this verdict.",
1468
- "Verify with DISBELIEF: assume the finding is a false positive until your OWN re-test proves otherwise. Reproduce the exact observable yourself from the primary evidence (not the hunter's narrative), with a negative/baseline control — a difference you cannot tie to the control is not proof. ConfirmFinding itself re-sends the immutable verify request against target and operator-approved control so phase 2 has a harness-owned transcript.",
1119
+ "Verify with DISBELIEF: assume the finding is a false positive until the recorded evidence proves otherwise. Read the PoC script, both run transcripts, the attack/baseline replay, and the evidence artifacts (not the hunter's narrative) — a difference you cannot tie to the baseline is not proof.",
1469
1120
  "Provenance: the proof must exercise THIS finding's own mechanism. Evidence obtained through a DIFFERENT bug (e.g. 'SQLi' proven by dumping the DB via an RCE) does not confirm it — that is INCONCLUSIVE at best.",
1470
1121
  "Kill the cheapest benign explanation: is this the technology's intended behavior? Did the attacker supply the 'secret' themselves (circular)? Is the claimed C/I/A impact actually demonstrated?",
1471
1122
  "Want a second pair of eyes? Dispatch a read-only skeptic subagent to re-test — it CANNOT confirm (only the main agent commits). You review its verdict and commit it here.",
1472
- "CONFIRMED requires differential: 'target_only', re_execution_note, and disconfirmation_attempt (your failed disproof). A verdict missing any of these is rejected. Set canary_assessment='verified' when the immutable request declared a canary; otherwise not_applicable with a reason.",
1123
+ "CONFIRMED requires differential: 'target_only', re_execution_note, and disconfirmation_attempt (your failed disproof). A verdict missing any of these is rejected.",
1473
1124
  "NOT_CONFIRMED means you POSITIVELY disproved it (by-design, circular, mislabeled, no impact). Never mark NOT_CONFIRMED merely because you could not reproduce it.",
1474
1125
  "INCONCLUSIVE when you could neither reproduce nor disprove (needs auth, a second account, specific state, timing, or a blind/stored trigger you cannot observe). The case stays investigating and is preserved for manual review — dropping a real finding is worse than keeping an unproven one.",
1475
1126
  "Every verdict consumes the attempt: a fresh PromoteFinding run is required to try again. Never CaseUpdate status='confirmed' directly — always PromoteFinding + ConfirmFinding.",
@@ -1487,83 +1138,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
1487
1138
  if (!parsedVerdict.ok) {
1488
1139
  throw new Error(`Invalid main-agent confirmation verdict: ${parsedVerdict.error}`);
1489
1140
  }
1490
- let phase2Verification: MainAgentVerification | undefined;
1491
- if (parsedVerdict.verdict.verdict === "CONFIRMED") {
1492
- const current = getCaseById(caseId);
1493
- if (!current) throw new Error(`Case not found: ${caseId}`);
1494
- const bundle = current.pendingConfirmation;
1495
- if (!bundle) {
1496
- throw new Error("No pending confirmation on this case — run PromoteFinding first");
1497
- }
1498
- const allowPrivate = process.env.PI_POC_ALLOW_PRIVATE_REPLAY === "1";
1499
- const caseTargetForReplay = current.target ?? bundle.targetRuns[0].target;
1500
- let replay: HarnessVerifyResult;
1501
- if (bundle.mode === "intra_target") {
1502
- // Same-host attack-vs-baseline replay; no control target to authorize.
1503
- replay = await replayIntraTarget(bundle.targetRuns[0].evidence, caseTargetForReplay, {
1504
- allowPrivate,
1505
- });
1506
- } else if (bundle.callbackVerified?.attempted && bundle.oobTokens) {
1507
- // OOB differential: fresh harness-owned re-poll of BOTH run tokens.
1508
- // Re-polling at confirm time catches interactions that landed after
1509
- // phase 1 (e.g. a delayed control-token hit) — the verdict is bound
1510
- // to this fresh observation, not the stored one.
1511
- const { verification } = await verifyOobDifferential({
1512
- targetToken: bundle.oobTokens.targetToken,
1513
- controlToken: bundle.oobTokens.controlToken,
1514
- });
1515
- const oobPass =
1516
- verification.targetHits > 0 &&
1517
- verification.controlHits === 0 &&
1518
- verification.sourceSeparated === true;
1519
- replay = {
1520
- attempted: true,
1521
- pass: oobPass,
1522
- target: {
1523
- attempted: true,
1524
- matched: verification.targetHits > 0,
1525
- url: bundle.targetRuns[0].evidence.verify.url,
1526
- note: verification.note,
1527
- },
1528
- control: {
1529
- attempted: true,
1530
- matched: verification.controlHits > 0,
1531
- url: bundle.targetRuns[0].evidence.verify.url,
1532
- note: `${verification.controlHits} control-token interaction(s)`,
1533
- },
1534
- differential:
1535
- verification.targetHits > 0
1536
- ? verification.controlHits === 0
1537
- ? "target_only"
1538
- : "both"
1539
- : "neither",
1540
- note: `harness OOB re-poll: ${verification.note}`,
1541
- };
1542
- } else if (bundle.callbackVerified?.attempted) {
1543
- throw new Error(
1544
- "OOB bundle lacks its provisioned tokens (pre-token-storage ledger) — re-run PromoteFinding for a fresh bundle",
1545
- );
1546
- } else {
1547
- if (!bundle.controlTarget) {
1548
- throw new Error("inter-host confirmation requires a control target");
1549
- }
1550
- const controlAuthorizationError = controlTargetAuthorizationError(bundle.controlTarget);
1551
- if (controlAuthorizationError) {
1552
- throw new Error(`CONTROL AUTHORIZATION FAILED: ${controlAuthorizationError}`);
1553
- }
1554
- replay = await replayDifferential(
1555
- bundle.targetRuns[0].evidence,
1556
- caseTargetForReplay,
1557
- bundle.controlTarget,
1558
- { allowPrivate },
1559
- );
1560
- }
1561
- phase2Verification = {
1562
- at: new Date().toISOString(),
1563
- result: replay,
1564
- };
1565
- }
1566
- const result = applyConfirmationResult(caseId, parsedVerdict.verdict, phase2Verification, {
1141
+ const result = applyConfirmationResult(caseId, parsedVerdict.verdict, {
1567
1142
  startedAsSubagent: isSubagentProcess(),
1568
1143
  });
1569
1144
  const record = result.record;
@@ -1602,6 +1177,65 @@ ${formatCaseDetail(record)}`,
1602
1177
  },
1603
1178
  });
1604
1179
 
1180
+ // ── Tool: CoverageAdd ──
1181
+
1182
+ registerCaseTool({
1183
+ name: "CoverageAdd",
1184
+ label: "Record Coverage Cell",
1185
+ description:
1186
+ "Record a tested (asset × attack-class) coverage cell on a case — for BOTH outcomes (found or clean). Clean results make 'every class is covered' machine-checkable. scope='wide' records a deployment-wide verdict ONCE (do not re-test per asset); 'local' is asset-specific. Cells can carry an artifact-backed evidence item; unbacked cells render as such in the report contract gate.",
1187
+ promptSnippet: "Record a tested coverage cell (found or clean)",
1188
+ promptGuidelines: [
1189
+ "Use CoverageAdd whenever you finish testing a class on an asset — a clean 'no injection on /api/orders' verdict is just as load-bearing as a finding.",
1190
+ "scope='wide' when the verdict is a property of the whole deployment (record once — do NOT re-test per asset); scope='local' for one asset.",
1191
+ "Reference an artifact-backed EvidenceAdd item via evidence_item_id so the tested verdict is machine-checkable, not prose-only.",
1192
+ ],
1193
+ parameters: CoverageAddSchema,
1194
+
1195
+ async execute(_id, params, _signal, _onUpdate, _ctx) {
1196
+ const item = recordCoverageResult(params.case_id as string, {
1197
+ asset: params.asset as string,
1198
+ class: params.class as string,
1199
+ scope: params.scope as CoverageScope,
1200
+ note: params.note as string,
1201
+ evidenceItemId: params.evidence_item_id as string | undefined,
1202
+ });
1203
+ const record = getCaseById(params.case_id as string);
1204
+ if (!record) throw new Error(`Case not found after coverage insert: ${params.case_id}`);
1205
+ return {
1206
+ content: [
1207
+ {
1208
+ type: "text",
1209
+ text: `Coverage cell recorded:\n[${item.scope}] ${item.asset} × ${item.class} — ${item.note}${item.evidenceItemId ? ` (backed by ${item.evidenceItemId})` : " (unbacked — attach an EvidenceAdd item to make it machine-checkable)"}\n\n${formatCaseDetail(record)}`,
1210
+ },
1211
+ ],
1212
+ details: { item, record },
1213
+ };
1214
+ },
1215
+
1216
+ renderCall(args, theme) {
1217
+ return callLine(
1218
+ theme,
1219
+ "CoverageAdd",
1220
+ `${(args.case_id as string) ?? ""} ${(args.asset as string) ?? ""}×${(args.class as string) ?? ""}`,
1221
+ );
1222
+ },
1223
+
1224
+ renderResult(result, _opts, theme) {
1225
+ const details = result.details as { item?: CoverageItem } | undefined;
1226
+ if (!details?.item) {
1227
+ return new Text(theme.fg("error", "✗ CoverageAdd failed"), 0, 0);
1228
+ }
1229
+ return new Text(
1230
+ theme.fg("success", "✓ ") +
1231
+ theme.fg("dim", `[${details.item.scope}] `) +
1232
+ truncateToWidth(`${details.item.asset} × ${details.item.class}`, 60),
1233
+ 0,
1234
+ 0,
1235
+ );
1236
+ },
1237
+ });
1238
+
1605
1239
  // ── Tool: CaseGet ──
1606
1240
 
1607
1241
  registerCaseTool({
@@ -1831,15 +1465,15 @@ ${formatCaseDetail(record)}`,
1831
1465
  parameters: IdSchema,
1832
1466
 
1833
1467
  async execute(_id, params, _signal, _onUpdate, _ctx) {
1834
- const { path, contextPath, record } = writeCaseContext(params.id as string);
1468
+ const { path, contextPath, contractPath, record } = writeCaseContext(params.id as string);
1835
1469
  return {
1836
1470
  content: [
1837
1471
  {
1838
1472
  type: "text",
1839
- text: `Case context written: ${contextPath}\nReport path: ${path}\n${formatCase(record)}`,
1473
+ text: `Case context written: ${contextPath}\nReport path: ${path}\nReport contract path: ${contractPath} — write the closed-schema JSON contract there (evidence_ids + coverage_refs must reference only this case's items); status='reported' is rejected until it validates.\n${formatCase(record)}`,
1840
1474
  },
1841
1475
  ],
1842
- details: { path, contextPath, record },
1476
+ details: { path, contextPath, contractPath, record },
1843
1477
  };
1844
1478
  },
1845
1479
 
@@ -1857,153 +1491,6 @@ ${formatCaseDetail(record)}`,
1857
1491
  },
1858
1492
  });
1859
1493
 
1860
- // ── Tool: ScratchpadInit ──
1861
-
1862
- registerCaseTool({
1863
- name: "ScratchpadInit",
1864
- label: "Init Scratchpad",
1865
- description:
1866
- "Initialize a crash-recoverable artifact store for a pipeline run. Creates the directory structure and an initial state.json checkpoint. Idempotent — safe to call on resume without --fresh; returns the existing checkpoint if the run already exists.",
1867
- promptSnippet: "Initialize the pipeline artifact store for a run",
1868
- promptGuidelines: [
1869
- "Call ScratchpadInit once at the start of a pipeline run (or on resume before ScratchpadResume).",
1870
- "The run_id is arbitrary but should be unique per pipeline run — typically <target>-<timestamp>.",
1871
- "On resume, ScratchpadInit returns the existing checkpoint without wiping it; pair with ScratchpadResume to skip completed phases.",
1872
- ],
1873
- parameters: RunIdSchema,
1874
-
1875
- async execute(_id, params, _signal, _onUpdate, _ctx) {
1876
- const cp = scratchpad_init(params.run_id as string);
1877
- return {
1878
- content: [
1879
- {
1880
- type: "text",
1881
- text: `Scratchpad initialized for run ${cp.run_id}.\nCompleted phases: ${cp.completed_phases.length ? cp.completed_phases.join(", ") : "none"}`,
1882
- },
1883
- ],
1884
- details: { checkpoint: cp },
1885
- };
1886
- },
1887
-
1888
- renderCall(args, theme) {
1889
- return callLine(theme, "ScratchpadInit", (args.run_id as string) ?? "");
1890
- },
1891
-
1892
- renderResult(result, _opts, theme) {
1893
- const cp = (result.details as { checkpoint: { run_id: string } } | undefined)?.checkpoint;
1894
- return new Text(`${theme.fg("success", "✓ ")}ScratchpadInit ${cp?.run_id ?? ""}`, 0, 0);
1895
- },
1896
- });
1897
-
1898
- // ── Tool: ScratchpadResume ──
1899
-
1900
- registerCaseTool({
1901
- name: "ScratchpadResume",
1902
- label: "Resume Scratchpad",
1903
- description:
1904
- "Read the checkpoint and artifact listing for a pipeline run to decide where to resume. Returns the next phase to run (or null if done) and which phases already completed. Returns null if the run does not exist.",
1905
- promptSnippet: "Check pipeline resume state — which phases are done",
1906
- promptGuidelines: [
1907
- "Call ScratchpadResume at pipeline start to determine where to resume. If it returns a checkpoint, skip completed phases (check ScratchpadPhaseDone before each dispatch) and continue from next_phase.",
1908
- "If ScratchpadResume returns null, the run has no checkpoint — call ScratchpadInit to start fresh.",
1909
- "Use ScratchpadPhaseDone before dispatching each stage to avoid re-running completed phases (idempotent resume).",
1910
- ],
1911
- parameters: RunIdSchema,
1912
-
1913
- async execute(_id, params, _signal, _onUpdate, _ctx) {
1914
- const resume = scratchpad_resume(params.run_id as string);
1915
- if (!resume) {
1916
- return {
1917
- content: [
1918
- {
1919
- type: "text",
1920
- text: `No scratchpad found for run ${params.run_id}. Call ScratchpadInit to start a new run.`,
1921
- },
1922
- ],
1923
- details: { resume: null },
1924
- };
1925
- }
1926
- const cp = resume.checkpoint;
1927
- return {
1928
- content: [
1929
- {
1930
- type: "text",
1931
- text:
1932
- `Resume run ${cp.run_id}:\n` +
1933
- `Completed phases: ${cp.completed_phases.length ? cp.completed_phases.join(", ") : "none"}\n` +
1934
- `Next phase: ${resume.next_phase ?? "none (run is done)"}`,
1935
- },
1936
- ],
1937
- details: { resume },
1938
- };
1939
- },
1940
-
1941
- renderCall(args, theme) {
1942
- return callLine(theme, "ScratchpadResume", (args.run_id as string) ?? "");
1943
- },
1944
-
1945
- renderResult(result, _opts, theme) {
1946
- const resume = (result.details as { resume: ScratchpadResume | null } | undefined)?.resume;
1947
- if (!resume) return new Text(theme.fg("warning", "↷ ScratchpadResume — no run found"), 0, 0);
1948
- return new Text(
1949
- theme.fg("success", "✓ ") +
1950
- `ScratchpadResume ${resume.checkpoint.run_id} → next: ${resume.next_phase ?? "done"}`,
1951
- 0,
1952
- 0,
1953
- );
1954
- },
1955
- });
1956
-
1957
- // ── Tool: ScratchpadCheckpoint ──
1958
-
1959
- registerCaseTool({
1960
- name: "ScratchpadCheckpoint",
1961
- label: "Checkpoint Phase",
1962
- description:
1963
- "Mark a pipeline phase as complete in the scratchpad state.json. Records the completion timestamp, key IDs, and an optional summary. Idempotent — re-checkpointing a phase overwrites its summary/IDs without duplicating the completed_phases entry.",
1964
- promptSnippet: "Record a pipeline phase as complete",
1965
- promptGuidelines: [
1966
- "Call ScratchpadCheckpoint after every phase completes: ScratchpadCheckpoint(run_id, phase, { ids, summary }).",
1967
- "ids are the key case/finding IDs the phase produced — used by resume to reconstruct state.",
1968
- "Keep completed_phases in pipeline order; the checkpoint sorts automatically.",
1969
- ],
1970
- parameters: ScratchpadCheckpointSchema,
1971
-
1972
- async execute(_id, params, _signal, _onUpdate, _ctx) {
1973
- const cp = scratchpad_checkpoint(params.run_id as string, params.phase as ScratchpadPhase, {
1974
- ids: params.ids as string[] | undefined,
1975
- summary: params.summary as string | undefined,
1976
- });
1977
- return {
1978
- content: [
1979
- {
1980
- type: "text",
1981
- text:
1982
- `Phase ${params.phase} checkpointed for run ${cp.run_id}.\n` +
1983
- `Completed phases: ${cp.completed_phases.join(", ")}`,
1984
- },
1985
- ],
1986
- details: { checkpoint: cp },
1987
- };
1988
- },
1989
-
1990
- renderCall(args, theme) {
1991
- return callLine(theme, "ScratchpadCheckpoint", `${args.run_id ?? ""} ${args.phase ?? ""}`);
1992
- },
1993
-
1994
- renderResult(result, _opts, theme) {
1995
- const cp = (
1996
- result.details as { checkpoint: { run_id: string; completed_phases: string[] } } | undefined
1997
- )?.checkpoint;
1998
- return new Text(
1999
- theme.fg("success", "✓ ") +
2000
- `ScratchpadCheckpoint ${cp?.run_id ?? ""} — ${cp?.completed_phases.length ?? 0} phases done`,
2001
- 0,
2002
- 0,
2003
- );
2004
- },
2005
- });
2006
-
2007
1494
  // ── Tool: ScratchpadWrite ──
2008
1495
 
2009
1496
  registerCaseTool({
@@ -2011,7 +1498,7 @@ ${formatCaseDetail(record)}`,
2011
1498
  label: "Write Artifact",
2012
1499
  description:
2013
1500
  "Write an intermediate artifact (recon map, trace output, verification log) to a phase's subdirectory in the scratchpad. Overwrites if the name exists. Artifact names are sanitized — path traversal is blocked.",
2014
- promptSnippet: "Save a pipeline artifact to the scratchpad",
1501
+ promptSnippet: "Save a run artifact to the scratchpad",
2015
1502
  promptGuidelines: [
2016
1503
  "Agents write artifacts to the scratchpad, not to each other's output files (prevents an echo chamber).",
2017
1504
  "The casefile owns state transitions; the scratchpad owns artifacts. Use ScratchpadWrite for bulky intermediate outputs, not CaseUpdate.",
@@ -2057,7 +1544,7 @@ ${formatCaseDetail(record)}`,
2057
1544
  label: "Read Artifact",
2058
1545
  description:
2059
1546
  "Read an artifact from a phase's subdirectory in the scratchpad. Returns null if the artifact is missing. Use to resume a phase from a prior run's intermediate output.",
2060
- promptSnippet: "Read a pipeline artifact from the scratchpad",
1547
+ promptSnippet: "Read a run artifact from the scratchpad",
2061
1548
  promptGuidelines: [
2062
1549
  "On resume, ScratchpadRead retrieves a prior phase's intermediate output so the next phase can proceed without re-running it.",
2063
1550
  "Returns null for missing artifacts — treat as 'not yet produced' rather than an error.",
@@ -2107,60 +1594,16 @@ ${formatCaseDetail(record)}`,
2107
1594
  },
2108
1595
  });
2109
1596
 
2110
- // ── Tool: ScratchpadPhaseDone ──
2111
-
2112
- registerCaseTool({
2113
- name: "ScratchpadPhaseDone",
2114
- label: "Phase Done?",
2115
- description:
2116
- "Check whether a phase has already been checkpointed in the scratchpad — for idempotent re-run. Returns true if the phase is complete; skip re-dispatching it on resume.",
2117
- promptSnippet: "Check if a pipeline phase is already complete",
2118
- promptGuidelines: [
2119
- "Call ScratchpadPhaseDone before dispatching each stage to avoid re-running completed phases on resume.",
2120
- "A completed phase with a checkpoint is a no-op on re-run — skip it and continue to the next incomplete phase.",
2121
- ],
2122
- parameters: ScratchpadPhaseDoneSchema,
2123
-
2124
- async execute(_id, params, _signal, _onUpdate, _ctx) {
2125
- const done = scratchpad_phase_done(params.run_id as string, params.phase as ScratchpadPhase);
2126
- return {
2127
- content: [
2128
- {
2129
- type: "text",
2130
- text: `Phase ${params.phase} for run ${params.run_id}: ${done ? "DONE (skip on resume)" : "not done"}`,
2131
- },
2132
- ],
2133
- details: { phase: params.phase, done },
2134
- };
2135
- },
2136
-
2137
- renderCall(args, theme) {
2138
- return callLine(theme, "ScratchpadPhaseDone", `${args.run_id ?? ""} ${args.phase ?? ""}`);
2139
- },
2140
-
2141
- renderResult(result, _opts, theme) {
2142
- const done = (result.details as { done?: boolean } | undefined)?.done;
2143
- return new Text(
2144
- done
2145
- ? theme.fg("success", "✓ ScratchpadPhaseDone — done")
2146
- : theme.fg("warning", "↷ ScratchpadPhaseDone — not done"),
2147
- 0,
2148
- 0,
2149
- );
2150
- },
2151
- });
2152
-
2153
1597
  // ── Tool: ScratchpadClear ──
2154
1598
 
2155
1599
  registerCaseTool({
2156
1600
  name: "ScratchpadClear",
2157
1601
  label: "Clear Run",
2158
1602
  description:
2159
- "Clear a single pipeline run's scratchpad directory. Used by --fresh for one run. Does not touch other runs. The run must be re-initialized with ScratchpadInit afterward.",
2160
- promptSnippet: "Clear one pipeline run's artifacts",
1603
+ "Clear a single run's scratchpad directory to force a fresh start for that run. Does not touch other runs. Directories are recreated automatically on the next write.",
1604
+ promptSnippet: "Clear one run's artifacts",
2161
1605
  promptGuidelines: [
2162
- "Use ScratchpadClear to force a fresh start for a single run (--fresh). It deletes that run's directory only.",
2163
- "After clearing, call ScratchpadInit to recreate the directory structure before writing artifacts.",
1606
+ "Use ScratchpadClear to force a fresh start for a single run. It deletes that run's directory only.",
2164
1607
  ],
2165
1608
  parameters: RunIdSchema,
2166
1609
 
@@ -2170,7 +1613,7 @@ ${formatCaseDetail(record)}`,
2170
1613
  content: [
2171
1614
  {
2172
1615
  type: "text",
2173
- text: `Scratchpad cleared for run ${params.run_id}. Call ScratchpadInit to start a new run.`,
1616
+ text: `Scratchpad cleared for run ${params.run_id}.`,
2174
1617
  },
2175
1618
  ],
2176
1619
  details: { run_id: params.run_id, cleared: true },