@xaccefy/pi-casefile 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -6
- package/package.json +13 -4
- package/skills/casefile/SKILL.md +3 -3
- package/src/evidence.ts +501 -0
- package/src/harness-verify.ts +693 -0
- package/src/index.ts +202 -94
- package/src/ledger-worker-entry.ts +35 -0
- package/src/ledger-worker.ts +77 -0
- package/src/ledger.ts +491 -59
- package/src/pipeline-submit.ts +88 -33
- package/src/poc-runner.ts +67 -20
- package/src/safe-state.ts +108 -0
- package/src/scratchpad.ts +39 -24
- package/src/workflow.ts +25 -20
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Casefile — offensive security case tracker for Pi.
|
|
3
3
|
*
|
|
4
|
-
* Tools: CaseAdd, CaseUpdate, PromoteFinding, EvidenceAdd, CoverageAdd, CoverageReport, ChainSuggest, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, PipelineSubmit, ScratchpadInit, ScratchpadResume, ScratchpadCheckpoint, ScratchpadWrite, ScratchpadRead, ScratchpadPhaseDone, ScratchpadClear
|
|
4
|
+
* Tools: CaseAdd, CaseUpdate, PromoteFinding, ConfirmFinding, EvidenceAdd, CoverageAdd, CoverageReport, ChainSuggest, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, PipelineSubmit, ScratchpadInit, ScratchpadResume, ScratchpadCheckpoint, ScratchpadWrite, ScratchpadRead, ScratchpadPhaseDone, ScratchpadClear
|
|
5
5
|
* Command: /casefile — interactive dashboard
|
|
6
6
|
* Event: before_agent_start — injects cyber workflow once per session, refreshes the active case list per prompt
|
|
7
7
|
*/
|
|
@@ -13,11 +13,17 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
13
13
|
import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
import { Type } from "typebox";
|
|
15
15
|
import {
|
|
16
|
+
CANARY_ASSESSMENT_VALUES,
|
|
16
17
|
CONFIRM_DIFFERENTIAL_VALUES,
|
|
17
18
|
CONFIRM_VERDICT_VALUES,
|
|
18
|
-
type ConfirmerVerdict,
|
|
19
19
|
SEVERITY_MATCH_VALUES,
|
|
20
|
+
validateMainAgentVerdict,
|
|
20
21
|
} from "./evidence.ts";
|
|
22
|
+
import {
|
|
23
|
+
controlTargetAuthorizationError,
|
|
24
|
+
type HarnessVerifyResult,
|
|
25
|
+
replayDifferential,
|
|
26
|
+
} from "./harness-verify.ts";
|
|
21
27
|
import {
|
|
22
28
|
addCaseResult,
|
|
23
29
|
addEvidenceItemResult,
|
|
@@ -47,6 +53,7 @@ import {
|
|
|
47
53
|
getCasefilePath,
|
|
48
54
|
LINK_KIND_VALUES,
|
|
49
55
|
linkCasesResult,
|
|
56
|
+
type MainAgentVerification,
|
|
50
57
|
type PendingConfirmation,
|
|
51
58
|
type PocEvidenceRun,
|
|
52
59
|
PRIORITY_VALUES,
|
|
@@ -58,11 +65,10 @@ import {
|
|
|
58
65
|
STATUS_VALUES,
|
|
59
66
|
searchCases,
|
|
60
67
|
storePendingConfirmation,
|
|
61
|
-
suggestChains,
|
|
62
68
|
unlinkCasesResult,
|
|
63
69
|
updateCaseResult,
|
|
64
|
-
writeCaseContext,
|
|
65
70
|
} from "./ledger.ts";
|
|
71
|
+
import { suggestChainsAsync, writeCaseContextAsync } from "./ledger-worker.ts";
|
|
66
72
|
import { pipeline_submit, SUBMIT_STAGES, type SubmitStage } from "./pipeline-submit.ts";
|
|
67
73
|
import { type PocRun, type PocRunOptions, runPoc } from "./poc-runner.ts";
|
|
68
74
|
import {
|
|
@@ -167,8 +173,7 @@ const EvidenceAddSchema = Type.Object(
|
|
|
167
173
|
artifact_path: Type.Optional(
|
|
168
174
|
Type.String({
|
|
169
175
|
description:
|
|
170
|
-
"Path to
|
|
171
|
-
"(full path is never persisted).",
|
|
176
|
+
"Path to a regular, non-symlink artifact inside the workspace. The bytes are copied durably and stored as basename + SHA-256 (full source path is never persisted).",
|
|
172
177
|
}),
|
|
173
178
|
),
|
|
174
179
|
},
|
|
@@ -177,12 +182,12 @@ const EvidenceAddSchema = Type.Object(
|
|
|
177
182
|
|
|
178
183
|
// ── Tool: PromoteFinding (phase 1) / ConfirmFinding (phase 2) ──────────
|
|
179
184
|
//
|
|
180
|
-
// Confirmation is TWO-PHASE
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
185
|
+
// Confirmation is TWO-PHASE: a worker may run PromoteFinding (the harness runs
|
|
186
|
+
// the PoC 2x + control, validates nonce-bound evidence.json, and records the
|
|
187
|
+
// bundle), but only the MAIN coordinator agent may review/re-execute and commit
|
|
188
|
+
// a verdict via ConfirmFinding. Zero exit is necessary run integrity and
|
|
189
|
+
// markers are diagnostic only; the machine records predicate/canary
|
|
190
|
+
// differentials and the main agent owns the semantic vulnerability judgment.
|
|
186
191
|
|
|
187
192
|
const PromoteSchema = Type.Object(
|
|
188
193
|
{
|
|
@@ -197,12 +202,18 @@ const PromoteSchema = Type.Object(
|
|
|
197
202
|
control_target: Type.String({
|
|
198
203
|
minLength: 1,
|
|
199
204
|
description:
|
|
200
|
-
"REQUIRED: a distinct baseline target that lacks the vulnerability (patched replica, second account, baseline
|
|
205
|
+
"REQUIRED: a distinct baseline target that lacks the vulnerability and is operator-approved through PI_POC_CONTROL_TARGETS (patched replica, second account, baseline service).",
|
|
201
206
|
}),
|
|
202
207
|
local: Type.Optional(
|
|
203
208
|
Type.Boolean({
|
|
204
209
|
description:
|
|
205
|
-
"Run with network access
|
|
210
|
+
"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.",
|
|
211
|
+
}),
|
|
212
|
+
),
|
|
213
|
+
oob: Type.Optional(
|
|
214
|
+
Type.Boolean({
|
|
215
|
+
description:
|
|
216
|
+
"Reserved for source-separated out-of-band verification. Currently fails closed because a loopback listener reachable by the PoC cannot prove target causation.",
|
|
206
217
|
}),
|
|
207
218
|
),
|
|
208
219
|
},
|
|
@@ -219,14 +230,13 @@ const ConfirmSchema = Type.Object(
|
|
|
219
230
|
description: "Why the evidence does or does not demonstrate the claim",
|
|
220
231
|
}),
|
|
221
232
|
evidence_reviewed: Type.Array(Type.String(), {
|
|
222
|
-
description: "Files/evidence the
|
|
223
|
-
}),
|
|
224
|
-
re_executed: Type.Boolean({
|
|
225
|
-
description:
|
|
226
|
-
"True iff the confirmer re-sent the verify request itself. Mandatory for CONFIRMED.",
|
|
233
|
+
description: "Files/evidence the main agent actually reviewed",
|
|
227
234
|
}),
|
|
228
235
|
re_execution_note: Type.Optional(
|
|
229
|
-
Type.String({
|
|
236
|
+
Type.String({
|
|
237
|
+
description:
|
|
238
|
+
"What the main agent observed during review and the fresh harness-owned target/control replay. Mandatory for CONFIRMED.",
|
|
239
|
+
}),
|
|
230
240
|
),
|
|
231
241
|
differential: Type.String({
|
|
232
242
|
enum: [...CONFIRM_DIFFERENTIAL_VALUES],
|
|
@@ -241,7 +251,20 @@ const ConfirmSchema = Type.Object(
|
|
|
241
251
|
disconfirmation_attempt: Type.Optional(
|
|
242
252
|
Type.String({
|
|
243
253
|
description:
|
|
244
|
-
"The
|
|
254
|
+
"The main agent's own failed attempt to disprove — becomes the case's disconfirmation",
|
|
255
|
+
}),
|
|
256
|
+
),
|
|
257
|
+
canary_assessment: Type.Optional(
|
|
258
|
+
Type.String({
|
|
259
|
+
enum: [...CANARY_ASSESSMENT_VALUES],
|
|
260
|
+
description:
|
|
261
|
+
"verified when the replay carried a harness-generated reflection canary; otherwise not_applicable with a concrete reason",
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
canary_reason: Type.Optional(
|
|
265
|
+
Type.String({
|
|
266
|
+
description:
|
|
267
|
+
"Why a causal reflection canary is not meaningful for this exploit class. Required when canary_assessment=not_applicable.",
|
|
245
268
|
}),
|
|
246
269
|
),
|
|
247
270
|
model: Type.Optional(
|
|
@@ -752,6 +775,11 @@ export function parseXpModeArg(args: string, current: XpMode): XpMode {
|
|
|
752
775
|
// ── Main extension ────────────────────────────────────────────────────
|
|
753
776
|
|
|
754
777
|
export default function casefileExtension(pi: ExtensionAPI) {
|
|
778
|
+
// Process role is immutable for this extension instance. A worker may spawn
|
|
779
|
+
// shells, but unsetting PI_SUBAGENT_CHILD in a child shell cannot upgrade the
|
|
780
|
+
// already-loaded extension or reveal a tool that was omitted at startup.
|
|
781
|
+
const startedAsSubagent = process.env.PI_SUBAGENT_CHILD === "1";
|
|
782
|
+
const isSubagentProcess = () => startedAsSubagent || process.env.PI_SUBAGENT_CHILD === "1";
|
|
755
783
|
// Pin the workspace root ONCE at extension load. Every scratchpad / pipeline
|
|
756
784
|
// / PoC-path lookup otherwise re-walks the ambient cwd on each call — a
|
|
757
785
|
// mid-session `cd` would split state across two .scratchpad roots and
|
|
@@ -901,7 +929,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
901
929
|
name: "EvidenceAdd",
|
|
902
930
|
label: "Add Evidence Item",
|
|
903
931
|
description:
|
|
904
|
-
"Record a role-typed, artifact-backed evidence item on a case
|
|
932
|
+
"Record a role-typed, artifact-backed evidence item on a case. Artifact reads are restricted to regular, non-symlink files inside the workspace; bytes are copied durably and stored as basename + SHA-256. refutation items justify a kill; cleanup items track engagement cleanup before REPORT.",
|
|
905
933
|
promptSnippet: "Record a role-typed evidence item",
|
|
906
934
|
promptGuidelines: [
|
|
907
935
|
"Use EvidenceAdd for artifact-backed evidence: raw responses, logs, screenshots, disproof attempts — anything a claim should trace back to.",
|
|
@@ -1103,22 +1131,23 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1103
1131
|
},
|
|
1104
1132
|
});
|
|
1105
1133
|
|
|
1106
|
-
// ── Tool: PromoteFinding ──
|
|
1107
1134
|
// ── Tool: PromoteFinding (phase 1) ──
|
|
1108
1135
|
|
|
1109
1136
|
pi.registerTool({
|
|
1110
1137
|
name: "PromoteFinding",
|
|
1111
1138
|
label: "Run PoC Evidence",
|
|
1112
1139
|
description:
|
|
1113
|
-
"Phase 1 of confirmation: run the PoC twice against the case target
|
|
1140
|
+
"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. 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.",
|
|
1114
1141
|
promptSnippet: "Phase 1: run PoC evidence (target x2 + control) and record the pending bundle",
|
|
1115
1142
|
promptGuidelines: [
|
|
1116
|
-
"Use PromoteFinding when an investigating case has a concrete PoC script on disk and you are ready to
|
|
1117
|
-
"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 disconfirmation comes from the
|
|
1118
|
-
"The PoC MUST write evidence.json to $PI_POC_EVIDENCE_DIR: { nonce (echo $PI_POC_NONCE), claim, verify: { method, url, expect: { status
|
|
1119
|
-
"
|
|
1120
|
-
"
|
|
1121
|
-
"
|
|
1143
|
+
"Use PromoteFinding when an investigating case has a concrete PoC script on disk and you are ready to subject its claim to the machine gate.",
|
|
1144
|
+
"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.",
|
|
1145
|
+
"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.",
|
|
1146
|
+
"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.",
|
|
1147
|
+
"control_path (REQUIRED): 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.",
|
|
1148
|
+
"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.",
|
|
1149
|
+
"Blind/OOB classes are not promotable through the built-in loopback listener because the PoC can self-call it; obtain a direct-response or state oracle, otherwise keep the case investigating.",
|
|
1150
|
+
"After the bundle is recorded, return control to the main agent. The main agent must inspect the script/evidence, attempt disconfirmation, and call ConfirmFinding itself; that call performs a fresh harness-owned target/control replay. Never delegate phase 2 and never CaseUpdate status='confirmed' directly.",
|
|
1122
1151
|
],
|
|
1123
1152
|
parameters: PromoteSchema,
|
|
1124
1153
|
|
|
@@ -1154,6 +1183,20 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1154
1183
|
{ controlTargetEqualsCaseTarget: true },
|
|
1155
1184
|
);
|
|
1156
1185
|
}
|
|
1186
|
+
if (params.local === true && process.env.PI_POC_ALLOW_NETWORK !== "1") {
|
|
1187
|
+
return fail(
|
|
1188
|
+
"Networked PoC execution is operator-gated. Set PI_POC_ALLOW_NETWORK=1 to authorize the host-network sandbox for this session.",
|
|
1189
|
+
{ networkNotAuthorized: true },
|
|
1190
|
+
);
|
|
1191
|
+
}
|
|
1192
|
+
const controlAuthorization = controlTargetAuthorizationError(controlTarget);
|
|
1193
|
+
if (controlAuthorization) {
|
|
1194
|
+
return fail(
|
|
1195
|
+
`CONTROL AUTHORIZATION FAILED: ${controlAuthorization}. ` +
|
|
1196
|
+
"The operator must set PI_POC_CONTROL_TARGETS to the exact approved control host/origin before this control can anchor confirmation.",
|
|
1197
|
+
{ controlNotAuthorized: true },
|
|
1198
|
+
);
|
|
1199
|
+
}
|
|
1157
1200
|
|
|
1158
1201
|
// Same-file contract (anti-cheat): control must be the SAME bytes as the
|
|
1159
1202
|
// PoC, differing only via the harness-set env. Check BEFORE any run.
|
|
@@ -1176,16 +1219,30 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1176
1219
|
);
|
|
1177
1220
|
}
|
|
1178
1221
|
|
|
1222
|
+
// ── OOB callback (Tier 1, opt-in for blind classes) ──
|
|
1223
|
+
const oobRequested = params.oob === true;
|
|
1224
|
+
if (oobRequested) {
|
|
1225
|
+
return fail(
|
|
1226
|
+
"OOB confirmation is fail-closed: the built-in loopback listener is reachable by the PoC and cannot prove the target caused a callback. A source-separated, operator-owned callback service is required before blind findings can be promoted.",
|
|
1227
|
+
{ oobSourceSeparationRequired: true },
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1179
1230
|
const runOptions = (pocMode: string, target: string): PocRunOptions => ({
|
|
1180
1231
|
network: params.local === true ? "host" : "none",
|
|
1181
1232
|
local: params.local === true,
|
|
1182
|
-
env: {
|
|
1233
|
+
env: {
|
|
1234
|
+
PI_POC_MODE: pocMode,
|
|
1235
|
+
PI_POC_TARGET: target,
|
|
1236
|
+
},
|
|
1183
1237
|
});
|
|
1184
1238
|
|
|
1185
1239
|
const caseTarget = current.target ?? "";
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1240
|
+
let targetRuns!: [PocEvidenceRun, PocEvidenceRun];
|
|
1241
|
+
let control!: PocEvidenceRun;
|
|
1242
|
+
let harnessVerified!: HarnessVerifyResult;
|
|
1243
|
+
// Determinism: TWO target runs + one control run. Exit 0 is run
|
|
1244
|
+
// integrity only; nonce-bound body evidence and the harness-owned
|
|
1245
|
+
// target/control replay form the machine gate.
|
|
1189
1246
|
const run1 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1190
1247
|
const run2 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1191
1248
|
const controlRun = runPoc(controlPath, runOptions("control", controlTarget));
|
|
@@ -1202,13 +1259,15 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1202
1259
|
if (r.evidenceError) {
|
|
1203
1260
|
return fail(
|
|
1204
1261
|
`EVIDENCE CONTRACT FAILED (${mode} run): ${r.evidenceError}. ` +
|
|
1205
|
-
"The PoC must write evidence.json to $PI_POC_EVIDENCE_DIR — { nonce (echo $PI_POC_NONCE), claim, verify: { method, url, expect: { status
|
|
1262
|
+
"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 — " +
|
|
1206
1263
|
"the file is bound to this run and validated by the harness. Case remains investigating.",
|
|
1207
1264
|
{ run: r, evidenceError: r.evidenceError },
|
|
1208
1265
|
);
|
|
1209
1266
|
}
|
|
1210
1267
|
if (!r.evidence || !r.evidenceSha256 || !r.nonce) {
|
|
1211
|
-
return fail(`${mode} run produced no evidence. Case remains investigating.`, {
|
|
1268
|
+
return fail(`${mode} run produced no evidence. Case remains investigating.`, {
|
|
1269
|
+
run: r,
|
|
1270
|
+
});
|
|
1212
1271
|
}
|
|
1213
1272
|
return {
|
|
1214
1273
|
mode,
|
|
@@ -1226,11 +1285,18 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1226
1285
|
};
|
|
1227
1286
|
};
|
|
1228
1287
|
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1288
|
+
targetRuns = [evidenceRun(run1, "poc", caseTarget), evidenceRun(run2, "poc", caseTarget)];
|
|
1289
|
+
control = evidenceRun(controlRun, "control", controlTarget);
|
|
1290
|
+
|
|
1291
|
+
// Tier 2 (docs/poc-trust-model.md): the harness executes the SAME
|
|
1292
|
+
// request template against target and operator-approved control, applying
|
|
1293
|
+
// the target's predicates to both. DNS is pinned at connect time.
|
|
1294
|
+
harnessVerified = await replayDifferential(
|
|
1295
|
+
targetRuns[0].evidence,
|
|
1296
|
+
caseTarget,
|
|
1297
|
+
controlTarget,
|
|
1298
|
+
{ allowPrivate: process.env.PI_POC_ALLOW_PRIVATE_REPLAY === "1" },
|
|
1299
|
+
);
|
|
1234
1300
|
|
|
1235
1301
|
const bundle: PendingConfirmation = {
|
|
1236
1302
|
caseId,
|
|
@@ -1241,6 +1307,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1241
1307
|
controlTarget,
|
|
1242
1308
|
targetRuns,
|
|
1243
1309
|
controlRun: control,
|
|
1310
|
+
harnessVerified,
|
|
1244
1311
|
};
|
|
1245
1312
|
|
|
1246
1313
|
let record: CaseRecord;
|
|
@@ -1260,11 +1327,9 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1260
1327
|
`Phase 1 complete — evidence bundle recorded on ${caseId} (expires in 1h).\n` +
|
|
1261
1328
|
`Target runs: 2, Control run: 1 — all with validated nonce-bound evidence.json.\n` +
|
|
1262
1329
|
`Evidence sha256: ${targetRuns[0].evidenceSha256}\n` +
|
|
1263
|
-
`PoC script sha256 (at run time): ${pocHash}\n
|
|
1264
|
-
(
|
|
1265
|
-
|
|
1266
|
-
: `DISPATCH THE CONFIRMER now: subagent({ workflowScript: "return runs.run('confirm-${caseId}-1', { agent: 'confirmer', task: 'Verify the PoC evidence for case ${caseId} (poc_path=${pocPath}, control_target=${controlTarget}, evidence_sha256=${targetRuns[0].evidenceSha256}, poc_sha256=${pocHash}). Assume fabricated, prove real. Re-send the verify request yourself. Return the verdict.' })", context: 'fresh', async: true })\n`) +
|
|
1267
|
-
"Then commit the verdict with ConfirmFinding(case_id, verdict) — CONFIRMED promotes, NOT_CONFIRMED keeps investigating.",
|
|
1330
|
+
`PoC script sha256 (at run time): ${pocHash}\n` +
|
|
1331
|
+
`Harness verify replay: ${harnessVerified.attempted ? (harnessVerified.pass ? `PASS (status ${harnessVerified.status})` : `FAILED — ${harnessVerified.note}`) : harnessVerified.note}\n` +
|
|
1332
|
+
`\nMAIN-AGENT REVIEW REQUIRED (do not delegate): inspect case ${caseId}, PoC ${pocPath}, 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 target/control replay; NOT_CONFIRMED keeps the case investigating.`,
|
|
1268
1333
|
},
|
|
1269
1334
|
],
|
|
1270
1335
|
details: {
|
|
@@ -1277,6 +1342,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1277
1342
|
controlTarget,
|
|
1278
1343
|
pocSha256: pocHash,
|
|
1279
1344
|
evidenceSha256: targetRuns[0].evidenceSha256,
|
|
1345
|
+
harnessVerified,
|
|
1280
1346
|
},
|
|
1281
1347
|
},
|
|
1282
1348
|
};
|
|
@@ -1303,55 +1369,95 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1303
1369
|
|
|
1304
1370
|
// ── Tool: ConfirmFinding (phase 2) ──
|
|
1305
1371
|
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
"
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1372
|
+
// Do not expose the commit capability in a worker process at all. The
|
|
1373
|
+
// execute-time check remains as defense in depth if process state changes
|
|
1374
|
+
// after registration or another integration forwards a stale tool handle.
|
|
1375
|
+
if (!startedAsSubagent)
|
|
1376
|
+
pi.registerTool({
|
|
1377
|
+
name: "ConfirmFinding",
|
|
1378
|
+
label: "Main-Agent Confirmation",
|
|
1379
|
+
description:
|
|
1380
|
+
"Phase 2 of confirmation, reserved for the main/coordinator agent: commit or refuse promotion after personally reviewing the machine bundle. 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. NOT_CONFIRMED records the review and keeps the case investigating.",
|
|
1381
|
+
promptSnippet: "Main agent: independently review and commit or refuse PoC confirmation",
|
|
1382
|
+
promptGuidelines: [
|
|
1383
|
+
"Run only in the main/coordinator agent after PromoteFinding returns. Do not dispatch a worker to decide or author this verdict.",
|
|
1384
|
+
"Personally inspect the PoC and preserved evidence and try a concrete disconfirmation before deciding. ConfirmFinding itself re-sends the immutable verify request against target and operator-approved control so phase 2 has a harness-owned transcript.",
|
|
1385
|
+
"CONFIRMED requires differential: 'target_only', re_execution_note, and disconfirmation_attempt (the main agent's failed disproof). A verdict missing any of these is rejected.",
|
|
1386
|
+
"Set canary_assessment='verified' when the immutable request declared a canary; otherwise set not_applicable and explain why a reflection canary is not meaningful for this exploit class.",
|
|
1387
|
+
"NOT_CONFIRMED is final for that attempt — the case stays investigating with the main agent's reasoning recorded. A fresh PromoteFinding run is required for another attempt.",
|
|
1388
|
+
"Never CaseUpdate status='confirmed' directly — it is rejected. Always use PromoteFinding + ConfirmFinding.",
|
|
1389
|
+
],
|
|
1390
|
+
parameters: ConfirmSchema,
|
|
1391
|
+
|
|
1392
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1393
|
+
if (isSubagentProcess()) {
|
|
1394
|
+
throw new Error(
|
|
1395
|
+
"ConfirmFinding is reserved for the main/coordinator agent. A worker or subagent may produce evidence but cannot confirm a PoC.",
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
const caseId = params.id as string;
|
|
1399
|
+
const parsedVerdict = validateMainAgentVerdict(params.verdict);
|
|
1400
|
+
if (!parsedVerdict.ok) {
|
|
1401
|
+
throw new Error(`Invalid main-agent confirmation verdict: ${parsedVerdict.error}`);
|
|
1402
|
+
}
|
|
1403
|
+
let phase2Verification: MainAgentVerification | undefined;
|
|
1404
|
+
if (parsedVerdict.verdict.verdict === "CONFIRMED") {
|
|
1405
|
+
const current = getCaseById(caseId);
|
|
1406
|
+
if (!current) throw new Error(`Case not found: ${caseId}`);
|
|
1407
|
+
const bundle = current.pendingConfirmation;
|
|
1408
|
+
if (!bundle) {
|
|
1409
|
+
throw new Error("No pending confirmation on this case — run PromoteFinding first");
|
|
1410
|
+
}
|
|
1411
|
+
const controlAuthorizationError = controlTargetAuthorizationError(bundle.controlTarget);
|
|
1412
|
+
if (controlAuthorizationError) {
|
|
1413
|
+
throw new Error(`CONTROL AUTHORIZATION FAILED: ${controlAuthorizationError}`);
|
|
1414
|
+
}
|
|
1415
|
+
const replay = await replayDifferential(
|
|
1416
|
+
bundle.targetRuns[0].evidence,
|
|
1417
|
+
current.target ?? bundle.targetRuns[0].target,
|
|
1418
|
+
bundle.controlTarget,
|
|
1419
|
+
{ allowPrivate: process.env.PI_POC_ALLOW_PRIVATE_REPLAY === "1" },
|
|
1420
|
+
);
|
|
1421
|
+
phase2Verification = {
|
|
1422
|
+
at: new Date().toISOString(),
|
|
1423
|
+
result: replay,
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
const result = applyConfirmationResult(caseId, parsedVerdict.verdict, phase2Verification, {
|
|
1427
|
+
startedAsSubagent: isSubagentProcess(),
|
|
1428
|
+
});
|
|
1429
|
+
const record = result.record;
|
|
1430
|
+
const promoted = record.status === "confirmed";
|
|
1431
|
+
return {
|
|
1432
|
+
content: [
|
|
1433
|
+
{
|
|
1434
|
+
type: "text",
|
|
1435
|
+
text: promoted
|
|
1436
|
+
? `Main agent CONFIRMED. Case promoted:
|
|
1331
1437
|
${formatCaseDetail(record)}`
|
|
1332
|
-
|
|
1438
|
+
: `Main agent NOT_CONFIRMED — case stays investigating (attempt recorded):
|
|
1333
1439
|
${formatCaseDetail(record)}`,
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1440
|
+
},
|
|
1441
|
+
],
|
|
1442
|
+
details: { record, promoted, changed: result.changed },
|
|
1443
|
+
};
|
|
1444
|
+
},
|
|
1339
1445
|
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1446
|
+
renderCall(args, theme) {
|
|
1447
|
+
return callLine(theme, "ConfirmFinding", (args.id as string) ?? "");
|
|
1448
|
+
},
|
|
1343
1449
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1450
|
+
renderResult(result, _opts, theme) {
|
|
1451
|
+
const details = result.details as { promoted?: boolean } | undefined;
|
|
1452
|
+
return new Text(
|
|
1453
|
+
details?.promoted
|
|
1454
|
+
? theme.fg("success", "✓ Promoted")
|
|
1455
|
+
: theme.fg("warning", "↷ Not confirmed"),
|
|
1456
|
+
0,
|
|
1457
|
+
0,
|
|
1458
|
+
);
|
|
1459
|
+
},
|
|
1460
|
+
});
|
|
1355
1461
|
|
|
1356
1462
|
// ── Tool: CaseGet ──
|
|
1357
1463
|
|
|
@@ -1595,7 +1701,9 @@ ${formatCaseDetail(record)}`,
|
|
|
1595
1701
|
parameters: ChainSuggestSchema,
|
|
1596
1702
|
|
|
1597
1703
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1598
|
-
const suggestions =
|
|
1704
|
+
const suggestions = await suggestChainsAsync(
|
|
1705
|
+
(params.case_id as string | undefined) ?? undefined,
|
|
1706
|
+
);
|
|
1599
1707
|
const lines: string[] = [
|
|
1600
1708
|
suggestions.length
|
|
1601
1709
|
? `${suggestions.length} chain candidate(s):`
|
|
@@ -1641,7 +1749,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1641
1749
|
parameters: IdSchema,
|
|
1642
1750
|
|
|
1643
1751
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1644
|
-
const { path, contextPath, record } =
|
|
1752
|
+
const { path, contextPath, record } = await writeCaseContextAsync(params.id as string);
|
|
1645
1753
|
return {
|
|
1646
1754
|
content: [
|
|
1647
1755
|
{
|
|
@@ -2135,7 +2243,7 @@ ${formatCaseDetail(record)}`,
|
|
|
2135
2243
|
// the workflow + entire active-case ledger into every child dispatch is a
|
|
2136
2244
|
// token multiplier (N subagents × workflow + growing case list per turn) —
|
|
2137
2245
|
// workers get what they need via their task and tool guidelines.
|
|
2138
|
-
if (
|
|
2246
|
+
if (isSubagentProcess()) return;
|
|
2139
2247
|
|
|
2140
2248
|
const includeWorkflow = !workflowInjected;
|
|
2141
2249
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker-thread entry for heavy ledger reads (suggestChains, writeCaseContext).
|
|
3
|
+
*
|
|
4
|
+
* Runs in a dedicated thread so an O(n²) chain scan or a multi-megabyte
|
|
5
|
+
* context-bundle build never blocks the agent's event loop. Each call spawns
|
|
6
|
+
* a fresh worker — rare operations, no lifecycle to manage, no stale state.
|
|
7
|
+
* The worker opens its OWN connection to the same WAL database (read-mostly
|
|
8
|
+
* work + one small reportPath write); WAL + busy_timeout make multi-connection
|
|
9
|
+
* access safe.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { parentPort, workerData } from "node:worker_threads";
|
|
13
|
+
import { suggestChains, writeCaseContext } from "./ledger.ts";
|
|
14
|
+
import { setScratchpadRoot } from "./scratchpad.ts";
|
|
15
|
+
|
|
16
|
+
process.env.PI_CASEFILE_PATH = workerData.casefilePath as string;
|
|
17
|
+
setScratchpadRoot(workerData.workspaceRoot as string | undefined);
|
|
18
|
+
|
|
19
|
+
type WorkerRequest =
|
|
20
|
+
| { op: "suggestChains"; caseId?: string }
|
|
21
|
+
| { op: "writeCaseContext"; id: string };
|
|
22
|
+
|
|
23
|
+
parentPort?.on("message", (req: WorkerRequest) => {
|
|
24
|
+
try {
|
|
25
|
+
if (req.op === "suggestChains") {
|
|
26
|
+
parentPort?.postMessage({ ok: true, result: suggestChains(req.caseId) });
|
|
27
|
+
} else if (req.op === "writeCaseContext") {
|
|
28
|
+
parentPort?.postMessage({ ok: true, result: writeCaseContext(req.id) });
|
|
29
|
+
} else {
|
|
30
|
+
parentPort?.postMessage({ ok: false, error: `unknown ledger worker op` });
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
parentPort?.postMessage({ ok: false, error: (e as Error).message });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async offload for the two heavy ledger reads — main-thread side.
|
|
3
|
+
*
|
|
4
|
+
* suggestChains is O(rules × cases²) over the whole ledger; writeCaseContext
|
|
5
|
+
* reads every artifact of every matching scratchpad run and builds a
|
|
6
|
+
* multi-hundred-KB bundle. Both ran synchronously inside async tool handlers,
|
|
7
|
+
* stalling the event loop. Each call here spawns a short-lived worker thread
|
|
8
|
+
* (ledger-worker-entry.ts) and falls back to the inline sync function when
|
|
9
|
+
* the worker cannot run (Node < 22.18 has no default type stripping for the
|
|
10
|
+
* TS entry) or times out. Writes stay sync on the main thread: they are
|
|
11
|
+
* single-row upserts, bounded by the 5s busy_timeout.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { Worker } from "node:worker_threads";
|
|
15
|
+
import {
|
|
16
|
+
type CaseContextResult,
|
|
17
|
+
type ChainSuggestion,
|
|
18
|
+
getCasefilePath,
|
|
19
|
+
suggestChains,
|
|
20
|
+
writeCaseContext,
|
|
21
|
+
} from "./ledger.ts";
|
|
22
|
+
import { detectWorkspaceRoot } from "./scratchpad.ts";
|
|
23
|
+
|
|
24
|
+
type WorkerResponse = { ok: true; result: unknown } | { ok: false; error: string };
|
|
25
|
+
|
|
26
|
+
const WORKER_TIMEOUT_MS = 30_000;
|
|
27
|
+
|
|
28
|
+
function runInLedgerWorker(
|
|
29
|
+
request:
|
|
30
|
+
| {
|
|
31
|
+
op: "suggestChains";
|
|
32
|
+
caseId?: string;
|
|
33
|
+
}
|
|
34
|
+
| {
|
|
35
|
+
op: "writeCaseContext";
|
|
36
|
+
id: string;
|
|
37
|
+
},
|
|
38
|
+
): Promise<unknown> {
|
|
39
|
+
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
40
|
+
const worker = new Worker(new URL("./ledger-worker-entry.ts", import.meta.url), {
|
|
41
|
+
workerData: { casefilePath: getCasefilePath(), workspaceRoot: detectWorkspaceRoot() },
|
|
42
|
+
});
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
void worker.terminate();
|
|
45
|
+
reject(new Error("ledger worker timed out"));
|
|
46
|
+
}, WORKER_TIMEOUT_MS);
|
|
47
|
+
worker.once("message", (msg: WorkerResponse) => {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
void worker.terminate();
|
|
50
|
+
if (msg.ok) resolve(msg.result);
|
|
51
|
+
else reject(new Error(msg.error));
|
|
52
|
+
});
|
|
53
|
+
worker.once("error", (e) => {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
reject(e);
|
|
56
|
+
});
|
|
57
|
+
worker.postMessage(request);
|
|
58
|
+
return promise;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** suggestChains on a worker thread; inline fallback keeps behavior identical. */
|
|
62
|
+
export async function suggestChainsAsync(caseId?: string): Promise<ChainSuggestion[]> {
|
|
63
|
+
try {
|
|
64
|
+
return (await runInLedgerWorker({ op: "suggestChains", caseId })) as ChainSuggestion[];
|
|
65
|
+
} catch {
|
|
66
|
+
return suggestChains(caseId);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** writeCaseContext on a worker thread; inline fallback keeps behavior identical. */
|
|
71
|
+
export async function writeCaseContextAsync(id: string): Promise<CaseContextResult> {
|
|
72
|
+
try {
|
|
73
|
+
return (await runInLedgerWorker({ op: "writeCaseContext", id })) as CaseContextResult;
|
|
74
|
+
} catch {
|
|
75
|
+
return writeCaseContext(id);
|
|
76
|
+
}
|
|
77
|
+
}
|