@xaccefy/pi-casefile 0.8.2 → 0.8.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaccefy/pi-casefile",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "Offensive security case tracker for Pi Agent — bug bounties, CTFs, security audits",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -51,9 +51,6 @@
51
51
  "./skills"
52
52
  ]
53
53
  },
54
- "dependencies": {
55
- "@sinclair/typebox": "^0.32.34"
56
- },
57
54
  "devDependencies": {
58
55
  "@types/bun": "^1.3.14",
59
56
  "typescript": "^6.0.3"
@@ -61,6 +58,7 @@
61
58
  "peerDependencies": {
62
59
  "@earendil-works/pi-ai": "*",
63
60
  "@earendil-works/pi-coding-agent": "*",
64
- "@earendil-works/pi-tui": "*"
61
+ "@earendil-works/pi-tui": "*",
62
+ "typebox": "*"
65
63
  }
66
64
  }
package/src/index.ts CHANGED
@@ -11,7 +11,7 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
11
  import { dirname, join } from "node:path";
12
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
13
13
  import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
14
- import { Type } from "@sinclair/typebox";
14
+ import { Type } from "typebox";
15
15
 
16
16
  import {
17
17
  addCaseResult,
@@ -109,6 +109,11 @@ const CommonFields = {
109
109
  "Falsification conditions — what would disprove this hypothesis (REQUIRED on CaseAdd)",
110
110
  }),
111
111
  ),
112
+ disconfirmation: Type.Optional(
113
+ Type.String({
114
+ description: "Documented attempt to disprove the finding before confirmation",
115
+ }),
116
+ ),
112
117
  };
113
118
 
114
119
  // ── Tool: CaseAdd ─────────────────────────────────────────────────────
@@ -169,15 +174,18 @@ const PromoteSchema = Type.Object(
169
174
  description:
170
175
  "Unique string the PoC must print AFTER verifying the exploit worked (data extracted, callback received, payload reflected). The gate checks output contains this marker — exit code 0 alone is NOT sufficient; the marker prevents fluke exit 0 and mocked PoCs. Example: 'VULN_CONFIRMED_<case-id>'. Never print it unconditionally or before the exploit check.",
171
176
  }),
172
- disconfirmation_path: Type.Optional(
173
- Type.String({
174
- description:
175
- "Absolute path to a disconfirmation script that tries to disprove the finding; must exit non-zero (failure to disprove). REQUIRED for severity high/critical findings.",
176
- }),
177
- ),
177
+ disconfirmation_path: Type.String({
178
+ description:
179
+ "REQUIRED for EVERY promotion: absolute path to a disconfirmation script that tries to disprove the finding; it must complete and exit non-zero (finding survived disproof).",
180
+ }),
178
181
  control_path: Type.String({
179
182
  description:
180
- "REQUIRED for EVERY promotion (sandboxed and live alike): absolute path to a control-target script that runs the SAME PoC against a control that lacks the vulnerability (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control output an unconditional-marker/mock PoC cannot pass. Best form: the same parameterized script, branching on the PI_POC_MODE env var (poc | control) the harness sets on every run.",
183
+ "REQUIRED for EVERY promotion: absolute path to the SAME script as poc_path (sha256-enforced). The harness runs it in control mode against control_target and blocks if verification_marker appears.",
184
+ }),
185
+ control_target: Type.String({
186
+ minLength: 1,
187
+ description:
188
+ "REQUIRED for EVERY promotion: target passed to the same PoC in control mode. Must be distinct from the case target and lack the vulnerability (patched replica, second account, baseline endpoint).",
181
189
  }),
182
190
  control_liveness_marker: Type.String({
183
191
  minLength: 1,
@@ -692,16 +700,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
692
700
  ) {
693
701
  hint = `\n\nHint: A database access error occurred on the casefile SQLite ledger.\nTo troubleshoot:\n 1. Check filesystem read/write permissions for the database path: ${getCasefilePath()}.\n 2. If using a locked folder, you can override the ledger location by setting:\n export PI_CASEFILE_PATH=/your/writable/directory/casefile.db`;
694
702
  }
695
- return {
696
- content: [
697
- {
698
- type: "text" as const,
699
- text: `${spec.name} failed: ${message}${hint}`,
700
- },
701
- ],
702
- isError: true,
703
- details: { error: message },
704
- };
703
+ throw new Error(`${spec.name} failed: ${message}${hint}`, { cause: err });
705
704
  }
706
705
  };
707
706
  originalRegisterTool(spec);
@@ -1028,15 +1027,15 @@ export default function casefileExtension(pi: ExtensionAPI) {
1028
1027
  name: "PromoteFinding",
1029
1028
  label: "Promote Finding",
1030
1029
  description:
1031
- "Run an on-disk PoC script (Docker sandbox or host-network sandbox) and, on exit 0 + verification marker present in output, promote an investigating case to confirmed. The verification_marker proves the exploit actually worked — exit code 0 alone is NOT sufficient. EVERY promotion (sandboxed and live alike) REQUIRES: (1) a disconfirmation_path script that must exit non-zero (the finding survived the attempt to disprove it); (2) a control_path that is the SAME script as the PoC (sha256-enforced a separately written control file is rejected), run in control mode via PI_POC_MODE; (3) a control_liveness_marker the control must print after reaching its target. The harness blocks promotion if the vuln marker appears in the (untruncated) control output, if the liveness marker is absent, or if the control/disconfirmation scripts crash. Host execution is never agent-selectable — local:true uses a host-network Docker sandbox; true host runs need the operator's PI_POC_ALLOW_LOCAL=1.",
1030
+ "Run an on-disk PoC script (Docker sandbox or host-network sandbox) and, on exit 0 + verification marker present in output, promote an investigating case to confirmed. The verification_marker proves the exploit worked — exit code 0 alone is NOT sufficient. EVERY promotion REQUIRES disconfirmation_path, same-script control_path, distinct control_target, and control_liveness_marker. The harness blocks promotion if output capture is incomplete, if the control prints the vuln marker, if liveness is absent, or if control/disconfirmation crash. Host execution is never agent-selectable — local:true uses a host-network Docker sandbox; true host runs need PI_POC_ALLOW_LOCAL=1.",
1032
1031
  promptSnippet: "Run a PoC and promote an investigating case to confirmed",
1033
1032
  promptGuidelines: [
1034
1033
  "Use PromoteFinding when an investigating case has a concrete PoC script on disk and you are ready to prove it.",
1035
1034
  "Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target, disconfirmation, plus an artifact-backed EvidenceAdd 'observation' item on the case (the initial signal, with artifact_path) — the PoC gate auto-records the reproduction item.",
1036
1035
  "Default sandbox: docker run --rm --network none. Use local:true for network-dependent bugs (host-network sandbox; host execution needs operator PI_POC_ALLOW_LOCAL=1).",
1037
1036
  "Gate: exit 0 AND verification_marker in the PoC output. The marker (e.g. 'VULN_CONFIRMED_<case-id>') must be printed only AFTER the exploit is verified (data extracted, callback received, payload reflected) — never unconditionally or before the exploit check. The marker check prevents fluke exit 0 (script crashed early) and mocked PoCs (target faked) from passing.",
1038
- "control_path (REQUIRED for EVERY promotion): the SAME script as poc_path (sha256-equality is ENFORCED — a separately written control file is rejected). One parameterized script read the target from the PI_POC_TARGET env var and branch on PI_POC_MODE (poc | control) the control run is literally the same script in control mode against a control lacking the vuln (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control run's output (checked on the untruncated output) — an unconditional-marker PoC cannot pass this.",
1039
- "control_liveness_marker (REQUIRED): a unique string the control script prints only AFTER reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). The harness blocks promotion if the control output lacks it a control pointed at an unreachable host, wrong port, or exiting before the check is NOT a clean verdict.",
1037
+ "control_path (REQUIRED): the SAME script as poc_path (sha256-equality is ENFORCED). The harness runs it with PI_POC_MODE=control and PI_POC_TARGET=control_target. The control must not print the verification marker and must print the liveness marker.",
1038
+ "control_target + control_liveness_marker (REQUIRED): control_target is the distinct baseline target. The liveness marker is printed only AFTER reaching/exercising it; absent liveness blocks promotion.",
1040
1039
  "disconfirmation_path: a script that tries to disprove the finding; if it exits 0, promotion is blocked. REQUIRED for EVERY promotion — the prose disconfirmation field is not enough at any severity (a case filed low/medium must not skip the run and be re-raised afterwards).",
1041
1040
  "Never CaseUpdate status='confirmed' directly — it is rejected. Always use PromoteFinding.",
1042
1041
  ],
@@ -1052,11 +1051,9 @@ export default function casefileExtension(pi: ExtensionAPI) {
1052
1051
 
1053
1052
  // Shared blocked-promotion shape: the case stays investigating and the
1054
1053
  // caller gets the record back for context.
1055
- const fail = (text: string, extra?: Record<string, unknown>) => ({
1056
- content: [{ type: "text" as const, text }],
1057
- isError: true,
1058
- details: { record: getCaseById(caseId), ...extra },
1059
- });
1054
+ const fail = (text: string, _extra?: Record<string, unknown>): never => {
1055
+ throw new Error(text);
1056
+ };
1060
1057
 
1061
1058
  // Reject empty/whitespace markers BEFORE any PoC run — it's a param
1062
1059
  // error, so fail cheap instead of burning a (up to 30s) sandboxed run.
@@ -1085,6 +1082,20 @@ export default function casefileExtension(pi: ExtensionAPI) {
1085
1082
  );
1086
1083
  }
1087
1084
 
1085
+ const controlTarget = (params.control_target as string | undefined)?.trim();
1086
+ if (!controlTarget) {
1087
+ return fail(
1088
+ "control_target is REQUIRED for every promotion: a distinct baseline target that lacks the vulnerability.",
1089
+ { missingControlTarget: true },
1090
+ );
1091
+ }
1092
+ if (controlTarget === current.target) {
1093
+ return fail(
1094
+ "control_target must differ from the case target; a control run against the vulnerable target proves nothing.",
1095
+ { controlTargetEqualsCaseTarget: true },
1096
+ );
1097
+ }
1098
+
1088
1099
  // The control must ALSO prove it reached its target: without a liveness
1089
1100
  // marker, a control pointed at an unreachable host / wrong port / early
1090
1101
  // exit would show "marker absent" for reasons unrelated to the vuln.
@@ -1143,10 +1154,8 @@ export default function casefileExtension(pi: ExtensionAPI) {
1143
1154
  return fail(
1144
1155
  "CONTROL CHECK FAILED: control_path must be the SAME script as poc_path " +
1145
1156
  "(sha256 mismatch). The control run is only meaningful as the same PoC " +
1146
- "pointed at a control target via PI_POC_MODE=control a separately written " +
1147
- "control file proves nothing (the model writes both files). Parameterize " +
1148
- "one script: branch on PI_POC_MODE (poc | control) and read the target from " +
1149
- "PI_POC_TARGET. Case remains investigating.",
1157
+ "pointed at a distinct control_target via PI_POC_MODE=control and PI_POC_TARGET. " +
1158
+ "A separately written control file proves nothing. Case remains investigating.",
1150
1159
  { run: undefined, controlHashMismatch: true },
1151
1160
  );
1152
1161
  }
@@ -1156,13 +1165,14 @@ export default function casefileExtension(pi: ExtensionAPI) {
1156
1165
  // opt-in it uses a Docker sandbox with --network host (same FS/cap/user
1157
1166
  // isolation) and fails closed when Docker is unavailable too. Host
1158
1167
  // execution is never agent-selectable on its own.
1159
- const runOptions = (pocMode: string): PocRunOptions => ({
1168
+ const runOptions = (pocMode: string, target: string): PocRunOptions => ({
1160
1169
  network: params.local === true ? "host" : "none",
1161
1170
  local: params.local === true,
1162
- env: { PI_POC_MODE: pocMode, PI_POC_TARGET: current.target ?? "" },
1171
+ env: { PI_POC_MODE: pocMode, PI_POC_TARGET: target },
1163
1172
  });
1164
1173
 
1165
- const run = runPoc(pocPath, runOptions("poc"));
1174
+ const caseTarget = current.target ?? "";
1175
+ const run = runPoc(pocPath, runOptions("poc", caseTarget));
1166
1176
 
1167
1177
  // Fail closed without throwing: non-zero PoC must leave the case investigating.
1168
1178
  if (run.exitCode !== 0) {
@@ -1174,9 +1184,9 @@ export default function casefileExtension(pi: ExtensionAPI) {
1174
1184
 
1175
1185
  // Defense-in-depth: exit 0 implies the run completed (sandbox wrapper /
1176
1186
  // local spawn semantics), but never trust a run the runner says crashed.
1177
- if (!run.completed) {
1187
+ if (!run.completed || !run.outputComplete) {
1178
1188
  return fail(
1179
- `PoC did NOT complete (spawn error, killed, or timeout). Case remains investigating.\nOutput:\n${run.output}`,
1189
+ `PoC did NOT complete or output capture was incomplete. Case remains investigating.\nOutput:\n${run.output}`,
1180
1190
  { run, pocCrashed: true },
1181
1191
  );
1182
1192
  }
@@ -1199,8 +1209,8 @@ export default function casefileExtension(pi: ExtensionAPI) {
1199
1209
  // Run disconfirmation script — must exit NON-0 (finding survived the attempt to disprove).
1200
1210
  let disconfirmationRun: PocRun | undefined;
1201
1211
  if (disconfirmationPath) {
1202
- disconfirmationRun = runPoc(disconfirmationPath, runOptions("disconfirmation"));
1203
- if (!disconfirmationRun.completed) {
1212
+ disconfirmationRun = runPoc(disconfirmationPath, runOptions("disconfirmation", caseTarget));
1213
+ if (!disconfirmationRun.completed || !disconfirmationRun.outputComplete) {
1204
1214
  return fail(
1205
1215
  `Disconfirmation script did NOT complete (spawn error, killed, or timeout — no completion marker). ` +
1206
1216
  `A crash is not a survived disproof: fix the disconfirmation script and retry.\n` +
@@ -1223,8 +1233,8 @@ export default function casefileExtension(pi: ExtensionAPI) {
1223
1233
  // deterministic — the model cannot pass it by asserting success. The
1224
1234
  // liveness-marker check closes the "control pointed at an unreachable
1225
1235
  // host / exited early" hole: the control must prove it reached its target.
1226
- const controlRun = runPoc(controlPath, runOptions("control"));
1227
- if (!controlRun.completed) {
1236
+ const controlRun = runPoc(controlPath, runOptions("control", controlTarget));
1237
+ if (!controlRun.completed || !controlRun.outputComplete) {
1228
1238
  return fail(
1229
1239
  `CONTROL CHECK FAILED: the control-target script did NOT complete (spawn error, killed, or timeout). ` +
1230
1240
  `A control run that never executed proves nothing about the marker — fix the control script and retry.\n` +
@@ -1660,9 +1670,9 @@ export default function casefileExtension(pi: ExtensionAPI) {
1660
1670
  : result.verdict === "repair"
1661
1671
  ? `REPAIR (attempt ${result.repair_attempt}/2) — fix these and re-submit:\n - ${result.errors.join("\n - ")}`
1662
1672
  : `REJECTED — ${result.errors.join("\n")}`;
1673
+ if (result.verdict !== "accepted") throw new Error(statusLine);
1663
1674
  return {
1664
1675
  content: [{ type: "text", text: statusLine }],
1665
- isError: result.verdict !== "accepted",
1666
1676
  details: result as unknown as Record<string, unknown>,
1667
1677
  };
1668
1678
  },