@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 +3 -5
- package/src/index.ts +50 -40
- package/src/ledger.ts +331 -324
- package/src/pipeline-submit.ts +212 -14
- package/src/poc-runner.ts +21 -4
- package/src/scratchpad.ts +78 -12
- package/src/workflow.ts +8 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xaccefy/pi-casefile",
|
|
3
|
-
"version": "0.8.
|
|
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 "
|
|
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.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1039
|
-
"control_liveness_marker (REQUIRED):
|
|
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,
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
|
1147
|
-
"control file proves nothing
|
|
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:
|
|
1171
|
+
env: { PI_POC_MODE: pocMode, PI_POC_TARGET: target },
|
|
1163
1172
|
});
|
|
1164
1173
|
|
|
1165
|
-
const
|
|
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
|
|
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
|
},
|