@xaccefy/pi-casefile 0.8.3 → 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 -5
- package/package.json +13 -4
- package/skills/casefile/SKILL.md +4 -3
- package/src/evidence.ts +501 -0
- package/src/harness-verify.ts +693 -0
- package/src/index.ts +391 -224
- package/src/ledger-worker-entry.ts +35 -0
- package/src/ledger-worker.ts +77 -0
- package/src/ledger.ts +853 -233
- package/src/pipeline-submit.ts +153 -43
- package/src/poc-runner.ts +216 -65
- package/src/safe-state.ts +108 -0
- package/src/scratchpad.ts +67 -28
- package/src/workflow.ts +91 -23
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
|
*/
|
|
@@ -12,10 +12,22 @@ 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
14
|
import { Type } from "typebox";
|
|
15
|
-
|
|
15
|
+
import {
|
|
16
|
+
CANARY_ASSESSMENT_VALUES,
|
|
17
|
+
CONFIRM_DIFFERENTIAL_VALUES,
|
|
18
|
+
CONFIRM_VERDICT_VALUES,
|
|
19
|
+
SEVERITY_MATCH_VALUES,
|
|
20
|
+
validateMainAgentVerdict,
|
|
21
|
+
} from "./evidence.ts";
|
|
22
|
+
import {
|
|
23
|
+
controlTargetAuthorizationError,
|
|
24
|
+
type HarnessVerifyResult,
|
|
25
|
+
replayDifferential,
|
|
26
|
+
} from "./harness-verify.ts";
|
|
16
27
|
import {
|
|
17
28
|
addCaseResult,
|
|
18
29
|
addEvidenceItemResult,
|
|
30
|
+
applyConfirmationResult,
|
|
19
31
|
assertPromotable,
|
|
20
32
|
type CaseConfidence,
|
|
21
33
|
type CaseInput,
|
|
@@ -41,8 +53,10 @@ import {
|
|
|
41
53
|
getCasefilePath,
|
|
42
54
|
LINK_KIND_VALUES,
|
|
43
55
|
linkCasesResult,
|
|
56
|
+
type MainAgentVerification,
|
|
57
|
+
type PendingConfirmation,
|
|
58
|
+
type PocEvidenceRun,
|
|
44
59
|
PRIORITY_VALUES,
|
|
45
|
-
promoteFindingResult,
|
|
46
60
|
readActiveCases,
|
|
47
61
|
readCasefile,
|
|
48
62
|
recordCoverageResult,
|
|
@@ -50,14 +64,15 @@ import {
|
|
|
50
64
|
SEVERITY_VALUES,
|
|
51
65
|
STATUS_VALUES,
|
|
52
66
|
searchCases,
|
|
53
|
-
|
|
67
|
+
storePendingConfirmation,
|
|
54
68
|
unlinkCasesResult,
|
|
55
69
|
updateCaseResult,
|
|
56
|
-
writeCaseContext,
|
|
57
70
|
} from "./ledger.ts";
|
|
71
|
+
import { suggestChainsAsync, writeCaseContextAsync } from "./ledger-worker.ts";
|
|
58
72
|
import { pipeline_submit, SUBMIT_STAGES, type SubmitStage } from "./pipeline-submit.ts";
|
|
59
73
|
import { type PocRun, type PocRunOptions, runPoc } from "./poc-runner.ts";
|
|
60
74
|
import {
|
|
75
|
+
detectWorkspaceRoot,
|
|
61
76
|
PHASE_ORDER,
|
|
62
77
|
type ScratchpadPhase,
|
|
63
78
|
type ScratchpadResume,
|
|
@@ -68,8 +83,13 @@ import {
|
|
|
68
83
|
scratchpad_read,
|
|
69
84
|
scratchpad_resume,
|
|
70
85
|
scratchpad_write,
|
|
86
|
+
setScratchpadRoot,
|
|
71
87
|
} from "./scratchpad.ts";
|
|
72
|
-
import {
|
|
88
|
+
import {
|
|
89
|
+
STATIC_CYBER_WORKFLOW,
|
|
90
|
+
STATIC_CYBER_WORKFLOW_LITE,
|
|
91
|
+
STATIC_CYBER_WORKFLOW_OMP,
|
|
92
|
+
} from "./workflow.ts";
|
|
73
93
|
|
|
74
94
|
// ── Schemas ───────────────────────────────────────────────────────────
|
|
75
95
|
|
|
@@ -153,15 +173,21 @@ const EvidenceAddSchema = Type.Object(
|
|
|
153
173
|
artifact_path: Type.Optional(
|
|
154
174
|
Type.String({
|
|
155
175
|
description:
|
|
156
|
-
"Path to
|
|
157
|
-
"(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).",
|
|
158
177
|
}),
|
|
159
178
|
),
|
|
160
179
|
},
|
|
161
180
|
{ additionalProperties: false },
|
|
162
181
|
);
|
|
163
182
|
|
|
164
|
-
// ── Tool: PromoteFinding
|
|
183
|
+
// ── Tool: PromoteFinding (phase 1) / ConfirmFinding (phase 2) ──────────
|
|
184
|
+
//
|
|
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.
|
|
165
191
|
|
|
166
192
|
const PromoteSchema = Type.Object(
|
|
167
193
|
{
|
|
@@ -169,39 +195,88 @@ const PromoteSchema = Type.Object(
|
|
|
169
195
|
poc_path: Type.String({
|
|
170
196
|
description: "Absolute path to the PoC script on disk",
|
|
171
197
|
}),
|
|
172
|
-
verification_marker: Type.String({
|
|
173
|
-
minLength: 1,
|
|
174
|
-
description:
|
|
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.",
|
|
176
|
-
}),
|
|
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
|
-
}),
|
|
181
198
|
control_path: Type.String({
|
|
182
199
|
description:
|
|
183
|
-
"REQUIRED
|
|
200
|
+
"REQUIRED: absolute path to 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.",
|
|
184
201
|
}),
|
|
185
202
|
control_target: Type.String({
|
|
186
203
|
minLength: 1,
|
|
187
204
|
description:
|
|
188
|
-
"REQUIRED
|
|
189
|
-
}),
|
|
190
|
-
control_liveness_marker: Type.String({
|
|
191
|
-
minLength: 1,
|
|
192
|
-
description:
|
|
193
|
-
"REQUIRED for EVERY promotion: a unique string the control script must print AFTER successfully reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). The harness blocks promotion if the control output lacks it — a control that never reached its target (unreachable host, wrong port, early exit) is not a clean verdict. Must differ from verification_marker.",
|
|
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).",
|
|
194
206
|
}),
|
|
195
207
|
local: Type.Optional(
|
|
196
208
|
Type.Boolean({
|
|
197
209
|
description:
|
|
198
|
-
"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.",
|
|
199
217
|
}),
|
|
200
218
|
),
|
|
201
219
|
},
|
|
202
220
|
{ additionalProperties: false },
|
|
203
221
|
);
|
|
204
222
|
|
|
223
|
+
const ConfirmSchema = Type.Object(
|
|
224
|
+
{
|
|
225
|
+
id: Type.String({ description: "Case ID with a pending confirmation" }),
|
|
226
|
+
verdict: Type.Object(
|
|
227
|
+
{
|
|
228
|
+
verdict: Type.String({ enum: [...CONFIRM_VERDICT_VALUES] }),
|
|
229
|
+
reasoning: Type.String({
|
|
230
|
+
description: "Why the evidence does or does not demonstrate the claim",
|
|
231
|
+
}),
|
|
232
|
+
evidence_reviewed: Type.Array(Type.String(), {
|
|
233
|
+
description: "Files/evidence the main agent actually reviewed",
|
|
234
|
+
}),
|
|
235
|
+
re_execution_note: Type.Optional(
|
|
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
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
differential: Type.String({
|
|
242
|
+
enum: [...CONFIRM_DIFFERENTIAL_VALUES],
|
|
243
|
+
description: "Target vs control evidence comparison. CONFIRMED requires target_only.",
|
|
244
|
+
}),
|
|
245
|
+
severity_match: Type.Optional(
|
|
246
|
+
Type.String({
|
|
247
|
+
enum: [...SEVERITY_MATCH_VALUES],
|
|
248
|
+
description: "Claimed severity vs what the evidence shows",
|
|
249
|
+
}),
|
|
250
|
+
),
|
|
251
|
+
disconfirmation_attempt: Type.Optional(
|
|
252
|
+
Type.String({
|
|
253
|
+
description:
|
|
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.",
|
|
268
|
+
}),
|
|
269
|
+
),
|
|
270
|
+
model: Type.Optional(
|
|
271
|
+
Type.String({ description: "Which model judged (recorded for the accuracy ledger)" }),
|
|
272
|
+
),
|
|
273
|
+
},
|
|
274
|
+
{ additionalProperties: false },
|
|
275
|
+
),
|
|
276
|
+
},
|
|
277
|
+
{ additionalProperties: false },
|
|
278
|
+
);
|
|
279
|
+
|
|
205
280
|
// ── Tool: CaseGet ─────────────────────────────────────────────────────
|
|
206
281
|
|
|
207
282
|
/** id-only schema, shared by CaseGet / CaseContext. */
|
|
@@ -604,6 +679,19 @@ function buildCaseListContext(records: CaseRecord[]): string {
|
|
|
604
679
|
return lines.join("\n");
|
|
605
680
|
}
|
|
606
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Detect the extension host. OMP is a fork of Pi: both load the same
|
|
684
|
+
* `pi`-manifest extensions, but subagent dispatch differs (pi-subagents'
|
|
685
|
+
* `subagent({workflowScript})` vs OMP's native `task`). The entry script path
|
|
686
|
+
* carries the host package: `@oh-my-pi/pi-coding-agent/dist/cli.js` under OMP,
|
|
687
|
+
* `@earendil-works/pi-coding-agent` under Pi.
|
|
688
|
+
*/
|
|
689
|
+
export function detectHost(): "omp" | "pi" {
|
|
690
|
+
const argv = process.argv.join(" ");
|
|
691
|
+
if (argv.includes("@oh-my-pi")) return "omp";
|
|
692
|
+
return "pi";
|
|
693
|
+
}
|
|
694
|
+
|
|
607
695
|
/**
|
|
608
696
|
* Builds the per-prompt injection. The cyber workflow is session-scope data —
|
|
609
697
|
* it never changes — so the caller passes includeWorkflow=true exactly once
|
|
@@ -611,7 +699,8 @@ function buildCaseListContext(records: CaseRecord[]): string {
|
|
|
611
699
|
* case list DOES change as cases are added, so it is refreshed every prompt.
|
|
612
700
|
*
|
|
613
701
|
* mode selects the workflow text: "lite" injects the single-agent workflow
|
|
614
|
-
* (no subagent dispatch), anything else gets the full subagent pipeline
|
|
702
|
+
* (no subagent dispatch), anything else gets the full subagent pipeline,
|
|
703
|
+
* rendered for the host's dispatch convention (pi-subagents vs OMP task).
|
|
615
704
|
*/
|
|
616
705
|
function buildAgentInjection(
|
|
617
706
|
active: CaseRecord[],
|
|
@@ -620,7 +709,12 @@ function buildAgentInjection(
|
|
|
620
709
|
): string {
|
|
621
710
|
const caseList = buildCaseListContext(active);
|
|
622
711
|
if (!includeWorkflow) return caseList;
|
|
623
|
-
const workflow =
|
|
712
|
+
const workflow =
|
|
713
|
+
mode === "lite"
|
|
714
|
+
? STATIC_CYBER_WORKFLOW_LITE
|
|
715
|
+
: detectHost() === "omp"
|
|
716
|
+
? STATIC_CYBER_WORKFLOW_OMP
|
|
717
|
+
: STATIC_CYBER_WORKFLOW;
|
|
624
718
|
// Workflow FIRST for prominence, then case list as reference data.
|
|
625
719
|
return caseList ? `${workflow}\n\n${caseList}` : workflow;
|
|
626
720
|
}
|
|
@@ -681,6 +775,20 @@ export function parseXpModeArg(args: string, current: XpMode): XpMode {
|
|
|
681
775
|
// ── Main extension ────────────────────────────────────────────────────
|
|
682
776
|
|
|
683
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";
|
|
783
|
+
// Pin the workspace root ONCE at extension load. Every scratchpad / pipeline
|
|
784
|
+
// / PoC-path lookup otherwise re-walks the ambient cwd on each call — a
|
|
785
|
+
// mid-session `cd` would split state across two .scratchpad roots and
|
|
786
|
+
// misroot the hunt file-existence filter. The PoC runner reads PI_POC_ROOT
|
|
787
|
+
// (set only when the operator hasn't pinned it explicitly).
|
|
788
|
+
const workspaceRoot = detectWorkspaceRoot();
|
|
789
|
+
setScratchpadRoot(workspaceRoot);
|
|
790
|
+
process.env.PI_POC_ROOT ??= workspaceRoot;
|
|
791
|
+
|
|
684
792
|
// ── Diagnostic Error Handler Middleware ──
|
|
685
793
|
const originalRegisterTool = pi.registerTool.bind(pi);
|
|
686
794
|
pi.registerTool = (spec: any) => {
|
|
@@ -821,7 +929,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
821
929
|
name: "EvidenceAdd",
|
|
822
930
|
label: "Add Evidence Item",
|
|
823
931
|
description:
|
|
824
|
-
"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.",
|
|
825
933
|
promptSnippet: "Record a role-typed evidence item",
|
|
826
934
|
promptGuidelines: [
|
|
827
935
|
"Use EvidenceAdd for artifact-backed evidence: raw responses, logs, screenshots, disproof attempts — anything a claim should trace back to.",
|
|
@@ -887,13 +995,15 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
887
995
|
description:
|
|
888
996
|
"The attack class tested (e.g. sql-injection, xss, idor, ssti, ssrf, auth-bypass, ...).",
|
|
889
997
|
}),
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
998
|
+
// Provider-safe string enum (per the header rule): Type.Union(Type.Literal)
|
|
999
|
+
// serializes to anyOf/const, which some providers drop — scope would
|
|
1000
|
+
// arrive undefined and every explicit 'wide' verdict would silently
|
|
1001
|
+
// persist as 'local', under-reporting tested classes.
|
|
1002
|
+
scope: Type.String({
|
|
1003
|
+
enum: [...COVERAGE_SCOPE_VALUES],
|
|
1004
|
+
description:
|
|
1005
|
+
"'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.",
|
|
1006
|
+
}),
|
|
897
1007
|
note: Type.String({
|
|
898
1008
|
description:
|
|
899
1009
|
"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.",
|
|
@@ -1021,71 +1131,49 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1021
1131
|
},
|
|
1022
1132
|
});
|
|
1023
1133
|
|
|
1024
|
-
// ── Tool: PromoteFinding ──
|
|
1134
|
+
// ── Tool: PromoteFinding (phase 1) ──
|
|
1025
1135
|
|
|
1026
1136
|
pi.registerTool({
|
|
1027
1137
|
name: "PromoteFinding",
|
|
1028
|
-
label: "
|
|
1138
|
+
label: "Run PoC Evidence",
|
|
1029
1139
|
description:
|
|
1030
|
-
"
|
|
1031
|
-
promptSnippet: "
|
|
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.",
|
|
1141
|
+
promptSnippet: "Phase 1: run PoC evidence (target x2 + control) and record the pending bundle",
|
|
1032
1142
|
promptGuidelines: [
|
|
1033
|
-
"Use PromoteFinding when an investigating case has a concrete PoC script on disk and you are ready to
|
|
1034
|
-
"Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target,
|
|
1035
|
-
"
|
|
1036
|
-
"
|
|
1037
|
-
"control_path (REQUIRED): the SAME script as poc_path
|
|
1038
|
-
"
|
|
1039
|
-
"
|
|
1040
|
-
"Never CaseUpdate status='confirmed' directly
|
|
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.",
|
|
1041
1151
|
],
|
|
1042
1152
|
parameters: PromoteSchema,
|
|
1043
1153
|
|
|
1044
1154
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1045
|
-
// Validate promotability BEFORE running the PoC —
|
|
1046
|
-
// 30s (plus first-time image pull), so fail cheap when the case
|
|
1047
|
-
// advance anyway (missing, wrong status, missing required fields,
|
|
1048
|
-
// artifact-backed observation evidence).
|
|
1155
|
+
// Validate promotability BEFORE running the PoC — each sandboxed run can
|
|
1156
|
+
// take 30s (plus first-time image pull), so fail cheap when the case
|
|
1157
|
+
// can't advance anyway (missing, wrong status, missing required fields,
|
|
1158
|
+
// missing artifact-backed observation evidence).
|
|
1049
1159
|
const caseId = params.id as string;
|
|
1050
1160
|
const current = assertPromotable(caseId);
|
|
1051
1161
|
|
|
1052
|
-
// Shared blocked-promotion shape: the case stays investigating and the
|
|
1053
|
-
// caller gets the record back for context.
|
|
1054
1162
|
const fail = (text: string, _extra?: Record<string, unknown>): never => {
|
|
1055
1163
|
throw new Error(text);
|
|
1056
1164
|
};
|
|
1057
1165
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
const marker = (params.verification_marker as string | undefined)?.trim();
|
|
1061
|
-
if (!marker) {
|
|
1062
|
-
return fail(
|
|
1063
|
-
"verification_marker is empty or whitespace. " +
|
|
1064
|
-
"A non-empty marker printed only AFTER the PoC confirms exploitation is required — " +
|
|
1065
|
-
"exit code 0 alone is not sufficient. Case remains investigating.",
|
|
1066
|
-
);
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
// Control-target anti-cheat is mandatory for EVERY promotion — sandboxed
|
|
1070
|
-
// and live alike. It is the only deterministic check that the marker is
|
|
1071
|
-
// target-dependent; skipping it for the default sandboxed mode would let
|
|
1072
|
-
// an unconditional-marker PoC pass untouched. Check BEFORE paying for the
|
|
1073
|
-
// PoC run.
|
|
1074
|
-
const controlPath = (params.control_path as string | undefined)?.trim();
|
|
1166
|
+
const controlPath = (params.control_path as string | undefined)?.trim() ?? "";
|
|
1167
|
+
const controlTarget = (params.control_target as string | undefined)?.trim() ?? "";
|
|
1075
1168
|
if (!controlPath) {
|
|
1076
1169
|
return fail(
|
|
1077
|
-
"control_path is REQUIRED
|
|
1078
|
-
"the SAME PoC against a control lacking the vuln (patched replica, second account, baseline endpoint). " +
|
|
1079
|
-
"The harness verifies the verification_marker is absent from the control run's output — that is what " +
|
|
1080
|
-
"proves the marker is target-dependent. Write the control script and retry.",
|
|
1170
|
+
"control_path is REQUIRED: the SAME script as poc_path (sha256-equality is ENFORCED), run by the harness with PI_POC_MODE=control and PI_POC_TARGET=control_target.",
|
|
1081
1171
|
{ missingControl: true },
|
|
1082
1172
|
);
|
|
1083
1173
|
}
|
|
1084
|
-
|
|
1085
|
-
const controlTarget = (params.control_target as string | undefined)?.trim();
|
|
1086
1174
|
if (!controlTarget) {
|
|
1087
1175
|
return fail(
|
|
1088
|
-
"control_target is REQUIRED
|
|
1176
|
+
"control_target is REQUIRED: a distinct baseline target that lacks the vulnerability.",
|
|
1089
1177
|
{ missingControlTarget: true },
|
|
1090
1178
|
);
|
|
1091
1179
|
}
|
|
@@ -1095,49 +1183,23 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1095
1183
|
{ controlTargetEqualsCaseTarget: true },
|
|
1096
1184
|
);
|
|
1097
1185
|
}
|
|
1098
|
-
|
|
1099
|
-
// The control must ALSO prove it reached its target: without a liveness
|
|
1100
|
-
// marker, a control pointed at an unreachable host / wrong port / early
|
|
1101
|
-
// exit would show "marker absent" for reasons unrelated to the vuln.
|
|
1102
|
-
const livenessMarker = (params.control_liveness_marker as string | undefined)?.trim();
|
|
1103
|
-
if (!livenessMarker) {
|
|
1104
|
-
return fail(
|
|
1105
|
-
"control_liveness_marker is REQUIRED for every promotion: a unique string the control script " +
|
|
1106
|
-
"prints ONLY AFTER reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). " +
|
|
1107
|
-
"The harness blocks promotion if the control output lacks it — a control that never reached its " +
|
|
1108
|
-
"target proves nothing. Must differ from verification_marker.",
|
|
1109
|
-
{ missingLivenessMarker: true },
|
|
1110
|
-
);
|
|
1111
|
-
}
|
|
1112
|
-
if (livenessMarker === marker) {
|
|
1186
|
+
if (params.local === true && process.env.PI_POC_ALLOW_NETWORK !== "1") {
|
|
1113
1187
|
return fail(
|
|
1114
|
-
"
|
|
1115
|
-
|
|
1116
|
-
{ livenessEqualsMarker: true },
|
|
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 },
|
|
1117
1190
|
);
|
|
1118
1191
|
}
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
// `disconfirmation` field cannot carry the disprove-attempt. Making it
|
|
1122
|
-
// unconditional (not severity-keyed) also kills the ordering attack:
|
|
1123
|
-
// a case filed as low/medium could previously skip the run, promote,
|
|
1124
|
-
// then be re-raised to high/critical with no executed disproof.
|
|
1125
|
-
const disconfirmationPath = (params.disconfirmation_path as string | undefined)?.trim();
|
|
1126
|
-
if (!disconfirmationPath) {
|
|
1192
|
+
const controlAuthorization = controlTargetAuthorizationError(controlTarget);
|
|
1193
|
+
if (controlAuthorization) {
|
|
1127
1194
|
return fail(
|
|
1128
|
-
|
|
1129
|
-
"
|
|
1130
|
-
|
|
1131
|
-
{ missingDisconfirmation: true },
|
|
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 },
|
|
1132
1198
|
);
|
|
1133
1199
|
}
|
|
1134
1200
|
|
|
1135
|
-
// Same-file contract (anti-cheat):
|
|
1136
|
-
//
|
|
1137
|
-
// agent-written files (poc prints the marker unconditionally, control
|
|
1138
|
-
// prints the liveness marker) pass every string check — the "same script"
|
|
1139
|
-
// recommendation in the docs is now enforced: sha256(control) must equal
|
|
1140
|
-
// sha256(poc). Hash BEFORE any run so a mismatch fails cheap.
|
|
1201
|
+
// Same-file contract (anti-cheat): control must be the SAME bytes as the
|
|
1202
|
+
// PoC, differing only via the harness-set env. Check BEFORE any run.
|
|
1141
1203
|
const pocPath = (params.poc_path as string | undefined)?.trim() ?? "";
|
|
1142
1204
|
let pocHash: string | undefined;
|
|
1143
1205
|
let controlHash: string | undefined;
|
|
@@ -1152,134 +1214,137 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1152
1214
|
}
|
|
1153
1215
|
if (pocHash !== controlHash) {
|
|
1154
1216
|
return fail(
|
|
1155
|
-
"CONTROL CHECK FAILED: control_path must be the SAME script as poc_path "
|
|
1156
|
-
|
|
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.",
|
|
1159
|
-
{ run: undefined, controlHashMismatch: true },
|
|
1217
|
+
"CONTROL CHECK FAILED: control_path must be the SAME script as poc_path (sha256 mismatch). Case remains investigating.",
|
|
1218
|
+
{ controlHashMismatch: true },
|
|
1160
1219
|
);
|
|
1161
1220
|
}
|
|
1162
1221
|
|
|
1163
|
-
//
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
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
|
+
}
|
|
1168
1230
|
const runOptions = (pocMode: string, target: string): PocRunOptions => ({
|
|
1169
1231
|
network: params.local === true ? "host" : "none",
|
|
1170
1232
|
local: params.local === true,
|
|
1171
|
-
env: {
|
|
1233
|
+
env: {
|
|
1234
|
+
PI_POC_MODE: pocMode,
|
|
1235
|
+
PI_POC_TARGET: target,
|
|
1236
|
+
},
|
|
1172
1237
|
});
|
|
1173
1238
|
|
|
1174
1239
|
const caseTarget = current.target ?? "";
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
// Defense-in-depth: exit 0 implies the run completed (sandbox wrapper /
|
|
1186
|
-
// local spawn semantics), but never trust a run the runner says crashed.
|
|
1187
|
-
if (!run.completed || !run.outputComplete) {
|
|
1188
|
-
return fail(
|
|
1189
|
-
`PoC did NOT complete or output capture was incomplete. Case remains investigating.\nOutput:\n${run.output}`,
|
|
1190
|
-
{ run, pocCrashed: true },
|
|
1191
|
-
);
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// Verification marker check: exit code 0 alone is NOT sufficient.
|
|
1195
|
-
// The PoC must print the verification_marker, proving the exploit
|
|
1196
|
-
// actually worked — not just that the script ran. The check runs on
|
|
1197
|
-
// rawOutput (untruncated) so a script printing its marker past the
|
|
1198
|
-
// 4000-char display window cannot hide it.
|
|
1199
|
-
const pocOut = run.rawOutput ?? run.output;
|
|
1200
|
-
if (!pocOut.includes(marker)) {
|
|
1201
|
-
return fail(
|
|
1202
|
-
`PoC exited 0 but the verification marker "${marker}" was NOT found in the output.\n` +
|
|
1203
|
-
`This means the script ran but did not prove exploitation. The marker must be printed only AFTER the PoC verifies the exploit worked (data extracted, callback received, payload reflected, etc.).\n` +
|
|
1204
|
-
`Do not print the marker unconditionally — print it only when the exploit is confirmed.\n\nOutput:\n${run.output}`,
|
|
1205
|
-
{ run, markerMissing: true },
|
|
1206
|
-
);
|
|
1207
|
-
}
|
|
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.
|
|
1246
|
+
const run1 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1247
|
+
const run2 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1248
|
+
const controlRun = runPoc(controlPath, runOptions("control", controlTarget));
|
|
1208
1249
|
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
if (disconfirmationPath) {
|
|
1212
|
-
disconfirmationRun = runPoc(disconfirmationPath, runOptions("disconfirmation", caseTarget));
|
|
1213
|
-
if (!disconfirmationRun.completed || !disconfirmationRun.outputComplete) {
|
|
1250
|
+
const evidenceRun = (r: PocRun, mode: "poc" | "control", target: string): PocEvidenceRun => {
|
|
1251
|
+
if (!r.completed || !r.outputComplete) {
|
|
1214
1252
|
return fail(
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
{ run
|
|
1253
|
+
`${mode} run did not complete or output capture was incomplete` +
|
|
1254
|
+
(r.infraError ? ` (infra: ${r.output.trim()})` : "") +
|
|
1255
|
+
". A crash is not evidence. Case remains investigating.",
|
|
1256
|
+
{ run: r, pocCrashed: true },
|
|
1219
1257
|
);
|
|
1220
1258
|
}
|
|
1221
|
-
if (
|
|
1259
|
+
if (r.evidenceError) {
|
|
1222
1260
|
return fail(
|
|
1223
|
-
`
|
|
1224
|
-
|
|
1225
|
-
|
|
1261
|
+
`EVIDENCE CONTRACT FAILED (${mode} run): ${r.evidenceError}. ` +
|
|
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 — " +
|
|
1263
|
+
"the file is bound to this run and validated by the harness. Case remains investigating.",
|
|
1264
|
+
{ run: r, evidenceError: r.evidenceError },
|
|
1226
1265
|
);
|
|
1227
1266
|
}
|
|
1228
|
-
|
|
1267
|
+
if (!r.evidence || !r.evidenceSha256 || !r.nonce) {
|
|
1268
|
+
return fail(`${mode} run produced no evidence. Case remains investigating.`, {
|
|
1269
|
+
run: r,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
return {
|
|
1273
|
+
mode,
|
|
1274
|
+
target,
|
|
1275
|
+
nonce: r.nonce,
|
|
1276
|
+
ranAt: r.ranAt,
|
|
1277
|
+
exitCode: r.exitCode,
|
|
1278
|
+
sandbox: r.sandbox,
|
|
1279
|
+
completed: r.completed,
|
|
1280
|
+
outputComplete: r.outputComplete,
|
|
1281
|
+
output: r.output ?? "",
|
|
1282
|
+
evidence: r.evidence,
|
|
1283
|
+
evidenceSha256: r.evidenceSha256,
|
|
1284
|
+
evidencePath: r.evidencePath,
|
|
1285
|
+
};
|
|
1286
|
+
};
|
|
1229
1287
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
{ run, controlRun, controlCrashed: true },
|
|
1243
|
-
);
|
|
1244
|
-
}
|
|
1245
|
-
// Marker-absence + liveness checks run on the UNTRUNCATED control output.
|
|
1246
|
-
const controlOut = controlRun.rawOutput ?? controlRun.output;
|
|
1247
|
-
if (controlOut.includes(marker)) {
|
|
1248
|
-
return fail(
|
|
1249
|
-
`CONTROL CHECK FAILED: the verification marker "${marker}" appeared in the control-target run. ` +
|
|
1250
|
-
`The PoC prints the marker without the vulnerable condition — a cheating PoC (unconditional marker) ` +
|
|
1251
|
-
`or a broken check. Case remains investigating.\nControl output:\n${controlRun.output}`,
|
|
1252
|
-
{ run, controlRun, controlCheated: true },
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
if (!controlOut.includes(livenessMarker)) {
|
|
1256
|
-
return fail(
|
|
1257
|
-
`CONTROL CHECK FAILED: the control-target run completed but the control_liveness_marker "${livenessMarker}" ` +
|
|
1258
|
-
`was NOT found in its output. The control must print the liveness marker only AFTER reaching/exercising ` +
|
|
1259
|
-
`the control target — an unreachable host, wrong port, or early exit is not a valid control verdict. ` +
|
|
1260
|
-
`Case remains investigating.\nControl output:\n${controlRun.output}`,
|
|
1261
|
-
{ run, controlRun, controlLivenessMissing: true },
|
|
1262
|
-
);
|
|
1263
|
-
}
|
|
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
|
+
);
|
|
1264
1300
|
|
|
1265
|
-
|
|
1266
|
-
const result = promoteFindingResult(
|
|
1301
|
+
const bundle: PendingConfirmation = {
|
|
1267
1302
|
caseId,
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1303
|
+
ranAt: new Date().toISOString(),
|
|
1304
|
+
pocPath,
|
|
1305
|
+
pocSha256: pocHash,
|
|
1306
|
+
controlPath,
|
|
1307
|
+
controlTarget,
|
|
1308
|
+
targetRuns,
|
|
1309
|
+
controlRun: control,
|
|
1310
|
+
harnessVerified,
|
|
1311
|
+
};
|
|
1312
|
+
|
|
1313
|
+
let record: CaseRecord;
|
|
1314
|
+
try {
|
|
1315
|
+
record = storePendingConfirmation(caseId, bundle);
|
|
1316
|
+
} catch (e) {
|
|
1317
|
+
return fail(`Pending confirmation rejected: ${(e as Error).message}`, {
|
|
1318
|
+
storeRejected: true,
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1275
1322
|
return {
|
|
1276
1323
|
content: [
|
|
1277
1324
|
{
|
|
1278
1325
|
type: "text",
|
|
1279
|
-
text:
|
|
1326
|
+
text:
|
|
1327
|
+
`Phase 1 complete — evidence bundle recorded on ${caseId} (expires in 1h).\n` +
|
|
1328
|
+
`Target runs: 2, Control run: 1 — all with validated nonce-bound evidence.json.\n` +
|
|
1329
|
+
`Evidence sha256: ${targetRuns[0].evidenceSha256}\n` +
|
|
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.`,
|
|
1280
1333
|
},
|
|
1281
1334
|
],
|
|
1282
|
-
details: {
|
|
1335
|
+
details: {
|
|
1336
|
+
record,
|
|
1337
|
+
bundle: {
|
|
1338
|
+
caseId,
|
|
1339
|
+
ranAt: bundle.ranAt,
|
|
1340
|
+
pocPath,
|
|
1341
|
+
controlPath,
|
|
1342
|
+
controlTarget,
|
|
1343
|
+
pocSha256: pocHash,
|
|
1344
|
+
evidenceSha256: targetRuns[0].evidenceSha256,
|
|
1345
|
+
harnessVerified,
|
|
1346
|
+
},
|
|
1347
|
+
},
|
|
1283
1348
|
};
|
|
1284
1349
|
},
|
|
1285
1350
|
|
|
@@ -1287,13 +1352,113 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1287
1352
|
return callLine(theme, "PromoteFinding", (args.id as string) ?? "");
|
|
1288
1353
|
},
|
|
1289
1354
|
|
|
1290
|
-
renderResult(result,
|
|
1291
|
-
const details = result.details as {
|
|
1292
|
-
|
|
1293
|
-
|
|
1355
|
+
renderResult(result, _opts, theme) {
|
|
1356
|
+
const details = result.details as { bundle?: { evidenceSha256?: string } } | undefined;
|
|
1357
|
+
if (!details?.bundle) {
|
|
1358
|
+
return new Text(theme.fg("error", "✗ PromoteFinding failed"), 0, 0);
|
|
1359
|
+
}
|
|
1360
|
+
return new Text(
|
|
1361
|
+
theme.fg("success", "✓ ") +
|
|
1362
|
+
theme.fg("dim", "evidence bundle ") +
|
|
1363
|
+
theme.fg("muted", details.bundle.evidenceSha256?.slice(0, 12) ?? ""),
|
|
1364
|
+
0,
|
|
1365
|
+
0,
|
|
1366
|
+
);
|
|
1294
1367
|
},
|
|
1295
1368
|
});
|
|
1296
1369
|
|
|
1370
|
+
// ── Tool: ConfirmFinding (phase 2) ──
|
|
1371
|
+
|
|
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:
|
|
1437
|
+
${formatCaseDetail(record)}`
|
|
1438
|
+
: `Main agent NOT_CONFIRMED — case stays investigating (attempt recorded):
|
|
1439
|
+
${formatCaseDetail(record)}`,
|
|
1440
|
+
},
|
|
1441
|
+
],
|
|
1442
|
+
details: { record, promoted, changed: result.changed },
|
|
1443
|
+
};
|
|
1444
|
+
},
|
|
1445
|
+
|
|
1446
|
+
renderCall(args, theme) {
|
|
1447
|
+
return callLine(theme, "ConfirmFinding", (args.id as string) ?? "");
|
|
1448
|
+
},
|
|
1449
|
+
|
|
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
|
+
});
|
|
1461
|
+
|
|
1297
1462
|
// ── Tool: CaseGet ──
|
|
1298
1463
|
|
|
1299
1464
|
pi.registerTool({
|
|
@@ -1536,7 +1701,9 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1536
1701
|
parameters: ChainSuggestSchema,
|
|
1537
1702
|
|
|
1538
1703
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1539
|
-
const suggestions =
|
|
1704
|
+
const suggestions = await suggestChainsAsync(
|
|
1705
|
+
(params.case_id as string | undefined) ?? undefined,
|
|
1706
|
+
);
|
|
1540
1707
|
const lines: string[] = [
|
|
1541
1708
|
suggestions.length
|
|
1542
1709
|
? `${suggestions.length} chain candidate(s):`
|
|
@@ -1582,7 +1749,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1582
1749
|
parameters: IdSchema,
|
|
1583
1750
|
|
|
1584
1751
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1585
|
-
const { path, contextPath, record } =
|
|
1752
|
+
const { path, contextPath, record } = await writeCaseContextAsync(params.id as string);
|
|
1586
1753
|
return {
|
|
1587
1754
|
content: [
|
|
1588
1755
|
{
|
|
@@ -2076,7 +2243,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
2076
2243
|
// the workflow + entire active-case ledger into every child dispatch is a
|
|
2077
2244
|
// token multiplier (N subagents × workflow + growing case list per turn) —
|
|
2078
2245
|
// workers get what they need via their task and tool guidelines.
|
|
2079
|
-
if (
|
|
2246
|
+
if (isSubagentProcess()) return;
|
|
2080
2247
|
|
|
2081
2248
|
const includeWorkflow = !workflowInjected;
|
|
2082
2249
|
|