@xaccefy/pi-casefile 0.9.1 → 0.9.2
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/LICENSE +1 -1
- package/README.md +12 -8
- package/package.json +2 -2
- package/skills/casefile/SKILL.md +3 -3
- package/src/evidence.ts +1 -0
- package/src/harness-verify.ts +3 -3
- package/src/index.ts +319 -299
- package/src/ledger.ts +12 -12
- package/src/pipeline-submit.ts +34 -42
- package/src/scratchpad.ts +18 -5
- package/src/workflow.ts +38 -38
package/src/index.ts
CHANGED
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
10
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { dirname, join } from "node:path";
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
|
-
import { Type } from "typebox";
|
|
14
|
+
import { type TSchema, Type } from "typebox";
|
|
15
15
|
import {
|
|
16
16
|
CANARY_ASSESSMENT_VALUES,
|
|
17
17
|
CONFIRM_DIFFERENTIAL_VALUES,
|
|
@@ -19,11 +19,7 @@ import {
|
|
|
19
19
|
SEVERITY_MATCH_VALUES,
|
|
20
20
|
validateMainAgentVerdict,
|
|
21
21
|
} from "./evidence.ts";
|
|
22
|
-
import {
|
|
23
|
-
controlTargetAuthorizationError,
|
|
24
|
-
type HarnessVerifyResult,
|
|
25
|
-
replayDifferential,
|
|
26
|
-
} from "./harness-verify.ts";
|
|
22
|
+
import { controlTargetAuthorizationError, replayDifferential } from "./harness-verify.ts";
|
|
27
23
|
import {
|
|
28
24
|
addCaseResult,
|
|
29
25
|
addEvidenceItemResult,
|
|
@@ -73,7 +69,7 @@ import { pipeline_submit, SUBMIT_STAGES, type SubmitStage } from "./pipeline-sub
|
|
|
73
69
|
import { type PocRun, type PocRunOptions, runPoc } from "./poc-runner.ts";
|
|
74
70
|
import {
|
|
75
71
|
detectWorkspaceRoot,
|
|
76
|
-
|
|
72
|
+
SCRATCHPAD_PHASES,
|
|
77
73
|
type ScratchpadPhase,
|
|
78
74
|
type ScratchpadResume,
|
|
79
75
|
scratchpad_checkpoint,
|
|
@@ -182,12 +178,13 @@ const EvidenceAddSchema = Type.Object(
|
|
|
182
178
|
|
|
183
179
|
// ── Tool: PromoteFinding (phase 1) / ConfirmFinding (phase 2) ──────────
|
|
184
180
|
//
|
|
185
|
-
// Confirmation is TWO-PHASE
|
|
186
|
-
//
|
|
187
|
-
// bundle
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
181
|
+
// Confirmation is TWO-PHASE and main-agent-owned: PromoteFinding runs the PoC
|
|
182
|
+
// 2x + control, validates nonce-bound evidence.json, and records the pending
|
|
183
|
+
// bundle; ConfirmFinding then performs the main coordinator's review/replay and
|
|
184
|
+
// commits or refuses the verdict. Subagents may gather or challenge evidence,
|
|
185
|
+
// but they cannot run validation or confirmation gates. Zero exit is necessary
|
|
186
|
+
// run integrity and markers are diagnostic only; the machine records
|
|
187
|
+
// predicate/canary differentials and the main agent owns the semantic judgment.
|
|
191
188
|
|
|
192
189
|
const PromoteSchema = Type.Object(
|
|
193
190
|
{
|
|
@@ -195,10 +192,12 @@ const PromoteSchema = Type.Object(
|
|
|
195
192
|
poc_path: Type.String({
|
|
196
193
|
description: "Absolute path to the PoC script on disk",
|
|
197
194
|
}),
|
|
198
|
-
control_path: Type.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
195
|
+
control_path: Type.Optional(
|
|
196
|
+
Type.String({
|
|
197
|
+
description:
|
|
198
|
+
"Optional absolute path to the SAME script as poc_path (sha256-equality is ENFORCED). Defaults to poc_path. The harness runs it with PI_POC_MODE=control and PI_POC_TARGET=control_target.",
|
|
199
|
+
}),
|
|
200
|
+
),
|
|
202
201
|
control_target: Type.String({
|
|
203
202
|
minLength: 1,
|
|
204
203
|
description:
|
|
@@ -358,9 +357,9 @@ const UnlinkSchema = Type.Object(
|
|
|
358
357
|
// artifacts; it does not re-run completed phases (idempotent).
|
|
359
358
|
|
|
360
359
|
const ScratchpadPhaseSchema = Type.String({
|
|
361
|
-
enum: [...
|
|
360
|
+
enum: [...SCRATCHPAD_PHASES],
|
|
362
361
|
description:
|
|
363
|
-
"Pipeline phase: recon | hunt |
|
|
362
|
+
"Pipeline phase: recon | hunt | trace | skeptic | validate | chain | patch | report (legacy gapfil is accepted for older runs)",
|
|
364
363
|
});
|
|
365
364
|
|
|
366
365
|
/** run_id-only schema, shared by Scratchpad Init / Resume / Clear. */
|
|
@@ -699,13 +698,13 @@ export function detectHost(): "omp" | "pi" {
|
|
|
699
698
|
* case list DOES change as cases are added, so it is refreshed every prompt.
|
|
700
699
|
*
|
|
701
700
|
* mode selects the workflow text: "lite" injects the single-agent workflow
|
|
702
|
-
* (no subagent dispatch),
|
|
701
|
+
* (no subagent dispatch), "swarm" gets the full subagent pipeline,
|
|
703
702
|
* rendered for the host's dispatch convention (pi-subagents vs OMP task).
|
|
704
703
|
*/
|
|
705
704
|
function buildAgentInjection(
|
|
706
705
|
active: CaseRecord[],
|
|
707
706
|
includeWorkflow: boolean,
|
|
708
|
-
mode: XpMode = "
|
|
707
|
+
mode: XpMode = "swarm",
|
|
709
708
|
): string {
|
|
710
709
|
const caseList = buildCaseListContext(active);
|
|
711
710
|
if (!includeWorkflow) return caseList;
|
|
@@ -722,13 +721,15 @@ function buildAgentInjection(
|
|
|
722
721
|
// ── XP (offensive / exploit) mode toggle ─────────────────────────────
|
|
723
722
|
// Casefile historically injected the cyber workflow into every prompt.
|
|
724
723
|
// For normal dev work that is just noise, so XP mode defaults OFF. Enable
|
|
725
|
-
//
|
|
726
|
-
//
|
|
727
|
-
// /xp
|
|
724
|
+
// swarm for the bounded multi-agent variant, or lite for the single-agent
|
|
725
|
+
// attacker discipline. Toggle with /xp (off <-> swarm), or set explicitly with
|
|
726
|
+
// /xp on|lite|swarm|off. "on" means the default enabled SWARM mode; use "lite"
|
|
727
|
+
// for no subagent dispatch.
|
|
728
|
+
// Override per-session with PI_XP_MODE.
|
|
728
729
|
// Pure helpers exported for unit tests.
|
|
729
730
|
|
|
730
731
|
export const XP_MODE_ENV = "PI_XP_MODE";
|
|
731
|
-
export type XpMode = "
|
|
732
|
+
export type XpMode = "swarm" | "off" | "lite";
|
|
732
733
|
|
|
733
734
|
export function getXpModeStatePath(): string {
|
|
734
735
|
return join(dirname(getCasefilePath()), "xp-mode");
|
|
@@ -739,13 +740,14 @@ export function readXpMode(
|
|
|
739
740
|
statePath: string = getXpModeStatePath(),
|
|
740
741
|
): XpMode {
|
|
741
742
|
const env = (envValue ?? "").trim().toLowerCase();
|
|
742
|
-
if (env === "
|
|
743
|
+
if (env === "swarm") return "swarm";
|
|
744
|
+
if (env === "on" || env === "1" || env === "true") return "swarm";
|
|
743
745
|
if (env === "lite") return "lite";
|
|
744
746
|
if (env === "off" || env === "0" || env === "false") return "off";
|
|
745
747
|
try {
|
|
746
748
|
if (existsSync(statePath)) {
|
|
747
749
|
const v = readFileSync(statePath, "utf8").trim().toLowerCase();
|
|
748
|
-
if (v === "on") return "
|
|
750
|
+
if (v === "swarm" || v === "on") return "swarm";
|
|
749
751
|
if (v === "lite") return "lite";
|
|
750
752
|
if (v === "off") return "off";
|
|
751
753
|
}
|
|
@@ -765,11 +767,12 @@ export function writeXpMode(state: XpMode, statePath: string = getXpModeStatePat
|
|
|
765
767
|
|
|
766
768
|
export function parseXpModeArg(args: string, current: XpMode): XpMode {
|
|
767
769
|
const arg = (args ?? "").trim().toLowerCase();
|
|
768
|
-
if (arg === "
|
|
770
|
+
if (arg === "swarm") return "swarm";
|
|
771
|
+
if (arg === "on") return "swarm";
|
|
769
772
|
if (arg === "off") return "off";
|
|
770
773
|
if (arg === "lite") return "lite";
|
|
771
|
-
// Bare /xp
|
|
772
|
-
return current === "
|
|
774
|
+
// Bare /xp is the low-ceremony path: toggle the default XP workflow on/off.
|
|
775
|
+
return current === "off" ? "swarm" : "off";
|
|
773
776
|
}
|
|
774
777
|
|
|
775
778
|
// ── Main extension ────────────────────────────────────────────────────
|
|
@@ -789,34 +792,37 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
789
792
|
setScratchpadRoot(workspaceRoot);
|
|
790
793
|
process.env.PI_POC_ROOT ??= workspaceRoot;
|
|
791
794
|
|
|
792
|
-
// ── Diagnostic Error Handler
|
|
793
|
-
const
|
|
794
|
-
|
|
795
|
+
// ── Diagnostic Error Handler ──
|
|
796
|
+
const registerCaseTool = <TParams extends TSchema, TDetails = unknown, TState = unknown>(
|
|
797
|
+
spec: ToolDefinition<TParams, TDetails, TState>,
|
|
798
|
+
) => {
|
|
795
799
|
const origExecute = spec.execute;
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
800
|
+
pi.registerTool({
|
|
801
|
+
...spec,
|
|
802
|
+
execute: async (...args: Parameters<typeof origExecute>) => {
|
|
803
|
+
try {
|
|
804
|
+
return await origExecute(...args);
|
|
805
|
+
} catch (err) {
|
|
806
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
807
|
+
let hint = "";
|
|
808
|
+
if (
|
|
809
|
+
message.includes("SQLITE") ||
|
|
810
|
+
message.includes("database") ||
|
|
811
|
+
message.includes("permission") ||
|
|
812
|
+
message.includes("readonly") ||
|
|
813
|
+
message.includes("lock")
|
|
814
|
+
) {
|
|
815
|
+
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`;
|
|
816
|
+
}
|
|
817
|
+
throw new Error(`${spec.name} failed: ${message}${hint}`, { cause: err });
|
|
810
818
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
};
|
|
814
|
-
originalRegisterTool(spec);
|
|
819
|
+
},
|
|
820
|
+
});
|
|
815
821
|
};
|
|
816
822
|
|
|
817
823
|
// ── Tool: CaseAdd ──
|
|
818
824
|
|
|
819
|
-
|
|
825
|
+
registerCaseTool({
|
|
820
826
|
name: "CaseAdd",
|
|
821
827
|
label: "Add Case",
|
|
822
828
|
description:
|
|
@@ -870,7 +876,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
870
876
|
|
|
871
877
|
// ── Tool: CaseUpdate ──
|
|
872
878
|
|
|
873
|
-
|
|
879
|
+
registerCaseTool({
|
|
874
880
|
name: "CaseUpdate",
|
|
875
881
|
label: "Update Case",
|
|
876
882
|
description:
|
|
@@ -925,7 +931,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
925
931
|
|
|
926
932
|
// ── Tool: EvidenceAdd ──
|
|
927
933
|
|
|
928
|
-
|
|
934
|
+
registerCaseTool({
|
|
929
935
|
name: "EvidenceAdd",
|
|
930
936
|
label: "Add Evidence Item",
|
|
931
937
|
description:
|
|
@@ -945,7 +951,8 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
945
951
|
summary: params.summary as string,
|
|
946
952
|
artifactPath: params.artifact_path as string | undefined,
|
|
947
953
|
});
|
|
948
|
-
const record = getCaseById(params.case_id as string)
|
|
954
|
+
const record = getCaseById(params.case_id as string);
|
|
955
|
+
if (!record) throw new Error(`Case not found after evidence insert: ${params.case_id}`);
|
|
949
956
|
return {
|
|
950
957
|
content: [
|
|
951
958
|
{
|
|
@@ -1018,7 +1025,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1018
1025
|
{ additionalProperties: false },
|
|
1019
1026
|
);
|
|
1020
1027
|
|
|
1021
|
-
|
|
1028
|
+
registerCaseTool({
|
|
1022
1029
|
name: "CoverageAdd",
|
|
1023
1030
|
label: "Record Coverage",
|
|
1024
1031
|
description:
|
|
@@ -1040,7 +1047,8 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1040
1047
|
note: params.note as string,
|
|
1041
1048
|
evidenceItemId: params.evidence_item_id as string | undefined,
|
|
1042
1049
|
});
|
|
1043
|
-
const record = getCaseById(params.case_id as string)
|
|
1050
|
+
const record = getCaseById(params.case_id as string);
|
|
1051
|
+
if (!record) throw new Error(`Case not found after coverage insert: ${params.case_id}`);
|
|
1044
1052
|
return {
|
|
1045
1053
|
content: [
|
|
1046
1054
|
{
|
|
@@ -1084,11 +1092,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1084
1092
|
{ additionalProperties: false },
|
|
1085
1093
|
);
|
|
1086
1094
|
|
|
1087
|
-
|
|
1095
|
+
registerCaseTool({
|
|
1088
1096
|
name: "CoverageReport",
|
|
1089
1097
|
label: "Coverage Matrix",
|
|
1090
1098
|
description:
|
|
1091
|
-
"Render the machine-checkable coverage matrix for a case: which (asset × attack-class) cells are tested, with wide-verdict propagation. Run before deciding
|
|
1099
|
+
"Render the machine-checkable coverage matrix for a case: which (asset × attack-class) cells are tested, with wide-verdict propagation. Run before deciding HUNT coverage is done — the plateau stop (zero new classes testable) must be visible in the matrix, not asserted in prose.",
|
|
1092
1100
|
promptSnippet: "Show which attack classes were tested where",
|
|
1093
1101
|
promptGuidelines: [
|
|
1094
1102
|
"Run CoverageReport before claiming 'every class is COVERED/SKIPPED/NOT_FOUND' — the claim must match the matrix.",
|
|
@@ -1133,239 +1141,249 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1133
1141
|
|
|
1134
1142
|
// ── Tool: PromoteFinding (phase 1) ──
|
|
1135
1143
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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).
|
|
1159
|
-
const caseId = params.id as string;
|
|
1160
|
-
const current = assertPromotable(caseId);
|
|
1161
|
-
|
|
1162
|
-
const fail = (text: string, _extra?: Record<string, unknown>): never => {
|
|
1163
|
-
throw new Error(text);
|
|
1164
|
-
};
|
|
1144
|
+
if (!startedAsSubagent)
|
|
1145
|
+
registerCaseTool({
|
|
1146
|
+
name: "PromoteFinding",
|
|
1147
|
+
label: "Run PoC Evidence",
|
|
1148
|
+
description:
|
|
1149
|
+
"Main-agent phase 1 of confirmation: run the same PoC twice against the case target and once against an operator-approved control_target, validate nonce-bound evidence.json with a response-body assertion, then have the harness execute one immutable HTTP request template against both target and control. control_path defaults to poc_path; if supplied, sha256 equality is enforced. The machine records a predicate differential, or a stronger canary differential when a reflection placeholder is requested and observed only on target; neither is automatically a vulnerability verdict. Exit 0 is necessary run integrity, never proof. Networked execution, controls, and private replay are operator-gated. Blind/OOB confirmation fails closed until source separation exists. Records a pending bundle for main-agent semantic review via ConfirmFinding. Worker/subagent processes are rejected.",
|
|
1150
|
+
promptSnippet:
|
|
1151
|
+
"Phase 1: run PoC evidence (target x2 + control) and record the pending bundle",
|
|
1152
|
+
promptGuidelines: [
|
|
1153
|
+
"Use PromoteFinding only from the main/coordinator agent when an investigating case has a concrete PoC script on disk and you are ready to subject its claim to the machine gate.",
|
|
1154
|
+
"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.",
|
|
1155
|
+
"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.",
|
|
1156
|
+
"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.",
|
|
1157
|
+
"control_path is optional and defaults to poc_path; if supplied, it must be the SAME script as poc_path. control_target must be pre-approved by the operator in PI_POC_CONTROL_TARGETS. The harness derives the control request from the target request, changes only its origin, and applies the same predicates to two conclusive responses.",
|
|
1158
|
+
"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.",
|
|
1159
|
+
"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.",
|
|
1160
|
+
"After the bundle is recorded, stay in the main agent: inspect the script/evidence, attempt disconfirmation, and call ConfirmFinding itself; that call performs a fresh harness-owned target/control replay. Never delegate validation/confirmation and never CaseUpdate status='confirmed' directly.",
|
|
1161
|
+
],
|
|
1162
|
+
parameters: PromoteSchema,
|
|
1165
1163
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
);
|
|
1179
|
-
}
|
|
1180
|
-
if (controlTarget === current.target) {
|
|
1181
|
-
return fail(
|
|
1182
|
-
"control_target must differ from the case target; a control run against the vulnerable target proves nothing.",
|
|
1183
|
-
{ controlTargetEqualsCaseTarget: true },
|
|
1184
|
-
);
|
|
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
|
-
}
|
|
1164
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1165
|
+
if (isSubagentProcess()) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
"PromoteFinding is reserved for the main/coordinator agent. A worker or subagent may gather evidence but cannot run validation or create a promotion bundle.",
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
// Validate promotability BEFORE running the PoC — each sandboxed run can
|
|
1171
|
+
// take 30s (plus first-time image pull), so fail cheap when the case
|
|
1172
|
+
// can't advance anyway (missing, wrong status, missing required fields,
|
|
1173
|
+
// missing artifact-backed observation evidence).
|
|
1174
|
+
const caseId = params.id as string;
|
|
1175
|
+
const current = assertPromotable(caseId);
|
|
1200
1176
|
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
let pocHash: string | undefined;
|
|
1205
|
-
let controlHash: string | undefined;
|
|
1206
|
-
try {
|
|
1207
|
-
pocHash = createHash("sha256").update(readFileSync(pocPath)).digest("hex");
|
|
1208
|
-
controlHash = createHash("sha256").update(readFileSync(controlPath)).digest("hex");
|
|
1209
|
-
} catch (e) {
|
|
1210
|
-
return fail(
|
|
1211
|
-
`Cannot read PoC/control scripts for the same-file check: ${(e as Error).message}`,
|
|
1212
|
-
{ sameFileCheckFailed: true },
|
|
1213
|
-
);
|
|
1214
|
-
}
|
|
1215
|
-
if (pocHash !== controlHash) {
|
|
1216
|
-
return fail(
|
|
1217
|
-
"CONTROL CHECK FAILED: control_path must be the SAME script as poc_path (sha256 mismatch). Case remains investigating.",
|
|
1218
|
-
{ controlHashMismatch: true },
|
|
1219
|
-
);
|
|
1220
|
-
}
|
|
1177
|
+
const fail = (text: string, _extra?: Record<string, unknown>): never => {
|
|
1178
|
+
throw new Error(text);
|
|
1179
|
+
};
|
|
1221
1180
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
"
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1181
|
+
const pocPath = (params.poc_path as string | undefined)?.trim() ?? "";
|
|
1182
|
+
const controlPath = (params.control_path as string | undefined)?.trim() || pocPath;
|
|
1183
|
+
const controlTarget = (params.control_target as string | undefined)?.trim() ?? "";
|
|
1184
|
+
if (!pocPath) {
|
|
1185
|
+
return fail("poc_path is REQUIRED: absolute path to the PoC script run by the harness.", {
|
|
1186
|
+
missingPocPath: true,
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
if (!controlTarget) {
|
|
1190
|
+
return fail(
|
|
1191
|
+
"control_target is REQUIRED: a distinct baseline target that lacks the vulnerability.",
|
|
1192
|
+
{ missingControlTarget: true },
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
if (controlTarget === current.target) {
|
|
1196
|
+
return fail(
|
|
1197
|
+
"control_target must differ from the case target; a control run against the vulnerable target proves nothing.",
|
|
1198
|
+
{ controlTargetEqualsCaseTarget: true },
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
if (params.local === true && process.env.PI_POC_ALLOW_NETWORK !== "1") {
|
|
1202
|
+
return fail(
|
|
1203
|
+
"Networked PoC execution is operator-gated. Set PI_POC_ALLOW_NETWORK=1 to authorize the host-network sandbox for this session.",
|
|
1204
|
+
{ networkNotAuthorized: true },
|
|
1205
|
+
);
|
|
1206
|
+
}
|
|
1207
|
+
const controlAuthorization = controlTargetAuthorizationError(controlTarget);
|
|
1208
|
+
if (controlAuthorization) {
|
|
1209
|
+
return fail(
|
|
1210
|
+
`CONTROL AUTHORIZATION FAILED: ${controlAuthorization}. ` +
|
|
1211
|
+
"The operator must set PI_POC_CONTROL_TARGETS to the exact approved control host/origin before this control can anchor confirmation.",
|
|
1212
|
+
{ controlNotAuthorized: true },
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1238
1215
|
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
const run2 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1248
|
-
const controlRun = runPoc(controlPath, runOptions("control", controlTarget));
|
|
1249
|
-
|
|
1250
|
-
const evidenceRun = (r: PocRun, mode: "poc" | "control", target: string): PocEvidenceRun => {
|
|
1251
|
-
if (!r.completed || !r.outputComplete) {
|
|
1216
|
+
// Same-file contract (anti-cheat): control must be the SAME bytes as the
|
|
1217
|
+
// PoC, differing only via the harness-set env. Check BEFORE any run.
|
|
1218
|
+
let pocHash: string | undefined;
|
|
1219
|
+
let controlHash: string | undefined;
|
|
1220
|
+
try {
|
|
1221
|
+
pocHash = createHash("sha256").update(readFileSync(pocPath)).digest("hex");
|
|
1222
|
+
controlHash = createHash("sha256").update(readFileSync(controlPath)).digest("hex");
|
|
1223
|
+
} catch (e) {
|
|
1252
1224
|
return fail(
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
". A crash is not evidence. Case remains investigating.",
|
|
1256
|
-
{ run: r, pocCrashed: true },
|
|
1225
|
+
`Cannot read PoC/control scripts for the same-file check: ${(e as Error).message}`,
|
|
1226
|
+
{ sameFileCheckFailed: true },
|
|
1257
1227
|
);
|
|
1258
1228
|
}
|
|
1259
|
-
if (
|
|
1229
|
+
if (pocHash !== controlHash) {
|
|
1260
1230
|
return fail(
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
"the file is bound to this run and validated by the harness. Case remains investigating.",
|
|
1264
|
-
{ run: r, evidenceError: r.evidenceError },
|
|
1231
|
+
"CONTROL CHECK FAILED: control_path must be the SAME script as poc_path (sha256 mismatch). Case remains investigating.",
|
|
1232
|
+
{ controlHashMismatch: true },
|
|
1265
1233
|
);
|
|
1266
1234
|
}
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1235
|
+
|
|
1236
|
+
// ── OOB callback (Tier 1, opt-in for blind classes) ──
|
|
1237
|
+
const oobRequested = params.oob === true;
|
|
1238
|
+
if (oobRequested) {
|
|
1239
|
+
return fail(
|
|
1240
|
+
"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.",
|
|
1241
|
+
{ oobSourceSeparationRequired: true },
|
|
1242
|
+
);
|
|
1271
1243
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1244
|
+
const runOptions = (pocMode: string, target: string): PocRunOptions => ({
|
|
1245
|
+
network: params.local === true ? "host" : "none",
|
|
1246
|
+
local: params.local === true,
|
|
1247
|
+
env: {
|
|
1248
|
+
PI_POC_MODE: pocMode,
|
|
1249
|
+
PI_POC_TARGET: target,
|
|
1250
|
+
},
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
const caseTarget = current.target ?? "";
|
|
1254
|
+
// Determinism: TWO target runs + one control run. Exit 0 is run
|
|
1255
|
+
// integrity only; nonce-bound body evidence and the harness-owned
|
|
1256
|
+
// target/control replay form the machine gate.
|
|
1257
|
+
const run1 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1258
|
+
const run2 = runPoc(pocPath, runOptions("poc", caseTarget));
|
|
1259
|
+
const controlRun = runPoc(controlPath, runOptions("control", controlTarget));
|
|
1260
|
+
|
|
1261
|
+
const evidenceRun = (
|
|
1262
|
+
r: PocRun,
|
|
1263
|
+
mode: "poc" | "control",
|
|
1264
|
+
target: string,
|
|
1265
|
+
): PocEvidenceRun => {
|
|
1266
|
+
if (!r.completed || !r.outputComplete) {
|
|
1267
|
+
return fail(
|
|
1268
|
+
`${mode} run did not complete or output capture was incomplete` +
|
|
1269
|
+
(r.infraError ? ` (infra: ${r.output.trim()})` : "") +
|
|
1270
|
+
". A crash is not evidence. Case remains investigating.",
|
|
1271
|
+
{ run: r, pocCrashed: true },
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
if (r.evidenceError) {
|
|
1275
|
+
return fail(
|
|
1276
|
+
`EVIDENCE CONTRACT FAILED (${mode} run): ${r.evidenceError}. ` +
|
|
1277
|
+
"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 — " +
|
|
1278
|
+
"the file is bound to this run and validated by the harness. Case remains investigating.",
|
|
1279
|
+
{ run: r, evidenceError: r.evidenceError },
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
if (!r.evidence || !r.evidenceSha256 || !r.nonce) {
|
|
1283
|
+
return fail(`${mode} run produced no evidence. Case remains investigating.`, {
|
|
1284
|
+
run: r,
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
return {
|
|
1288
|
+
mode,
|
|
1289
|
+
target,
|
|
1290
|
+
nonce: r.nonce,
|
|
1291
|
+
ranAt: r.ranAt,
|
|
1292
|
+
exitCode: r.exitCode,
|
|
1293
|
+
sandbox: r.sandbox,
|
|
1294
|
+
completed: r.completed,
|
|
1295
|
+
outputComplete: r.outputComplete,
|
|
1296
|
+
output: r.output ?? "",
|
|
1297
|
+
evidence: r.evidence,
|
|
1298
|
+
evidenceSha256: r.evidenceSha256,
|
|
1299
|
+
evidencePath: r.evidencePath,
|
|
1300
|
+
};
|
|
1285
1301
|
};
|
|
1286
|
-
};
|
|
1287
1302
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1303
|
+
const targetRuns: [PocEvidenceRun, PocEvidenceRun] = [
|
|
1304
|
+
evidenceRun(run1, "poc", caseTarget),
|
|
1305
|
+
evidenceRun(run2, "poc", caseTarget),
|
|
1306
|
+
];
|
|
1307
|
+
const control = evidenceRun(controlRun, "control", controlTarget);
|
|
1308
|
+
|
|
1309
|
+
// Tier 2 (docs/poc-trust-model.md): the harness executes the SAME
|
|
1310
|
+
// request template against target and operator-approved control, applying
|
|
1311
|
+
// the target's predicates to both. DNS is pinned at connect time.
|
|
1312
|
+
const harnessVerified = await replayDifferential(
|
|
1313
|
+
targetRuns[0].evidence,
|
|
1314
|
+
caseTarget,
|
|
1315
|
+
controlTarget,
|
|
1316
|
+
{ allowPrivate: process.env.PI_POC_ALLOW_PRIVATE_REPLAY === "1" },
|
|
1317
|
+
);
|
|
1300
1318
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1319
|
+
const bundle: PendingConfirmation = {
|
|
1320
|
+
caseId,
|
|
1321
|
+
ranAt: new Date().toISOString(),
|
|
1322
|
+
pocPath,
|
|
1323
|
+
pocSha256: pocHash,
|
|
1324
|
+
controlPath,
|
|
1325
|
+
controlTarget,
|
|
1326
|
+
targetRuns,
|
|
1327
|
+
controlRun: control,
|
|
1328
|
+
harnessVerified,
|
|
1329
|
+
};
|
|
1312
1330
|
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1331
|
+
let record: CaseRecord;
|
|
1332
|
+
try {
|
|
1333
|
+
record = storePendingConfirmation(caseId, bundle);
|
|
1334
|
+
} catch (e) {
|
|
1335
|
+
return fail(`Pending confirmation rejected: ${(e as Error).message}`, {
|
|
1336
|
+
storeRejected: true,
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1321
1339
|
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1340
|
+
return {
|
|
1341
|
+
content: [
|
|
1342
|
+
{
|
|
1343
|
+
type: "text",
|
|
1344
|
+
text:
|
|
1345
|
+
`Phase 1 complete — evidence bundle recorded on ${caseId} (expires in 1h).\n` +
|
|
1346
|
+
`Target runs: 2, Control run: 1 — all with validated nonce-bound evidence.json.\n` +
|
|
1347
|
+
`Evidence sha256: ${targetRuns[0].evidenceSha256}\n` +
|
|
1348
|
+
`PoC script sha256 (at run time): ${pocHash}\n` +
|
|
1349
|
+
`Harness verify replay: ${harnessVerified.attempted ? (harnessVerified.pass ? `PASS (status ${harnessVerified.status})` : `FAILED — ${harnessVerified.note}`) : harnessVerified.note}\n` +
|
|
1350
|
+
`\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.`,
|
|
1351
|
+
},
|
|
1352
|
+
],
|
|
1353
|
+
details: {
|
|
1354
|
+
record,
|
|
1355
|
+
bundle: {
|
|
1356
|
+
caseId,
|
|
1357
|
+
ranAt: bundle.ranAt,
|
|
1358
|
+
pocPath,
|
|
1359
|
+
controlPath,
|
|
1360
|
+
controlTarget,
|
|
1361
|
+
pocSha256: pocHash,
|
|
1362
|
+
evidenceSha256: targetRuns[0].evidenceSha256,
|
|
1363
|
+
harnessVerified,
|
|
1364
|
+
},
|
|
1346
1365
|
},
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
},
|
|
1366
|
+
};
|
|
1367
|
+
},
|
|
1350
1368
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1369
|
+
renderCall(args, theme) {
|
|
1370
|
+
return callLine(theme, "PromoteFinding", (args.id as string) ?? "");
|
|
1371
|
+
},
|
|
1354
1372
|
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1373
|
+
renderResult(result, _opts, theme) {
|
|
1374
|
+
const details = result.details as { bundle?: { evidenceSha256?: string } } | undefined;
|
|
1375
|
+
if (!details?.bundle) {
|
|
1376
|
+
return new Text(theme.fg("error", "✗ PromoteFinding failed"), 0, 0);
|
|
1377
|
+
}
|
|
1378
|
+
return new Text(
|
|
1379
|
+
theme.fg("success", "✓ ") +
|
|
1380
|
+
theme.fg("dim", "evidence bundle ") +
|
|
1381
|
+
theme.fg("muted", details.bundle.evidenceSha256?.slice(0, 12) ?? ""),
|
|
1382
|
+
0,
|
|
1383
|
+
0,
|
|
1384
|
+
);
|
|
1385
|
+
},
|
|
1386
|
+
});
|
|
1369
1387
|
|
|
1370
1388
|
// ── Tool: ConfirmFinding (phase 2) ──
|
|
1371
1389
|
|
|
@@ -1373,7 +1391,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1373
1391
|
// execute-time check remains as defense in depth if process state changes
|
|
1374
1392
|
// after registration or another integration forwards a stale tool handle.
|
|
1375
1393
|
if (!startedAsSubagent)
|
|
1376
|
-
|
|
1394
|
+
registerCaseTool({
|
|
1377
1395
|
name: "ConfirmFinding",
|
|
1378
1396
|
label: "Main-Agent Confirmation",
|
|
1379
1397
|
description:
|
|
@@ -1392,7 +1410,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1392
1410
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
1393
1411
|
if (isSubagentProcess()) {
|
|
1394
1412
|
throw new Error(
|
|
1395
|
-
"ConfirmFinding is reserved for the main/coordinator agent. A worker or subagent may
|
|
1413
|
+
"ConfirmFinding is reserved for the main/coordinator agent. A worker or subagent may gather or challenge evidence but cannot run validation or confirm a PoC.",
|
|
1396
1414
|
);
|
|
1397
1415
|
}
|
|
1398
1416
|
const caseId = params.id as string;
|
|
@@ -1461,7 +1479,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1461
1479
|
|
|
1462
1480
|
// ── Tool: CaseGet ──
|
|
1463
1481
|
|
|
1464
|
-
|
|
1482
|
+
registerCaseTool({
|
|
1465
1483
|
name: "CaseGet",
|
|
1466
1484
|
label: "Get Case",
|
|
1467
1485
|
description: "Get full details of a single case by ID.",
|
|
@@ -1490,7 +1508,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1490
1508
|
|
|
1491
1509
|
// ── Tool: CaseList ──
|
|
1492
1510
|
|
|
1493
|
-
|
|
1511
|
+
registerCaseTool({
|
|
1494
1512
|
name: "CaseList",
|
|
1495
1513
|
label: "List Cases",
|
|
1496
1514
|
description:
|
|
@@ -1520,7 +1538,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1520
1538
|
|
|
1521
1539
|
// ── Tool: CaseSearch ──
|
|
1522
1540
|
|
|
1523
|
-
|
|
1541
|
+
registerCaseTool({
|
|
1524
1542
|
name: "CaseSearch",
|
|
1525
1543
|
label: "Search Cases",
|
|
1526
1544
|
description:
|
|
@@ -1548,7 +1566,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1548
1566
|
|
|
1549
1567
|
// ── Tool: CaseLink ──
|
|
1550
1568
|
|
|
1551
|
-
|
|
1569
|
+
registerCaseTool({
|
|
1552
1570
|
name: "CaseLink",
|
|
1553
1571
|
label: "Link Cases",
|
|
1554
1572
|
description:
|
|
@@ -1620,7 +1638,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1620
1638
|
|
|
1621
1639
|
// ── Tool: CaseUnlink ──
|
|
1622
1640
|
|
|
1623
|
-
|
|
1641
|
+
registerCaseTool({
|
|
1624
1642
|
name: "CaseUnlink",
|
|
1625
1643
|
label: "Unlink Cases",
|
|
1626
1644
|
description: "Remove a bidirectional link between two cases.",
|
|
@@ -1687,7 +1705,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1687
1705
|
{ additionalProperties: false },
|
|
1688
1706
|
);
|
|
1689
1707
|
|
|
1690
|
-
|
|
1708
|
+
registerCaseTool({
|
|
1691
1709
|
name: "ChainSuggest",
|
|
1692
1710
|
label: "Suggest Exploit Chains",
|
|
1693
1711
|
description:
|
|
@@ -1736,15 +1754,15 @@ ${formatCaseDetail(record)}`,
|
|
|
1736
1754
|
|
|
1737
1755
|
// ── Tool: CaseContext ──
|
|
1738
1756
|
|
|
1739
|
-
|
|
1757
|
+
registerCaseTool({
|
|
1740
1758
|
name: "CaseContext",
|
|
1741
1759
|
label: "Generate Case Context",
|
|
1742
1760
|
description:
|
|
1743
|
-
"Generate the case context bundle for a confirmed or reported case under the casefile report directory (next to the casefile DB): full evidence, PoC verification log, disconfirmation attempt, links, and timeline, plus the target report path. The
|
|
1744
|
-
promptSnippet: "Generate case context for the report
|
|
1761
|
+
"Generate the case context bundle for a confirmed or reported case under the casefile report directory (next to the casefile DB): full evidence, PoC verification log, disconfirmation attempt, links, and timeline, plus the target report path. The main agent turns this context into the final polished H1-style report. Hypothesis/investigating/blocked/killed cases are rejected — promote to confirmed first.",
|
|
1762
|
+
promptSnippet: "Generate case context for the final report",
|
|
1745
1763
|
promptGuidelines: [
|
|
1746
1764
|
"Use CaseContext only for confirmed or already reported cases. Keep hypotheses and investigating cases in the ledger until proof is captured.",
|
|
1747
|
-
"After CaseContext,
|
|
1765
|
+
"After CaseContext, write the final report to the returned report path yourself, then CaseUpdate(status: 'reported').",
|
|
1748
1766
|
],
|
|
1749
1767
|
parameters: IdSchema,
|
|
1750
1768
|
|
|
@@ -1754,7 +1772,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1754
1772
|
content: [
|
|
1755
1773
|
{
|
|
1756
1774
|
type: "text",
|
|
1757
|
-
text: `Case context written: ${contextPath}\nReport path
|
|
1775
|
+
text: `Case context written: ${contextPath}\nReport path: ${path}\n${formatCase(record)}`,
|
|
1758
1776
|
},
|
|
1759
1777
|
],
|
|
1760
1778
|
details: { path, contextPath, record },
|
|
@@ -1779,7 +1797,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1779
1797
|
|
|
1780
1798
|
pi.registerCommand("xp", {
|
|
1781
1799
|
description:
|
|
1782
|
-
"Toggle casefile XP (offensive) mode.
|
|
1800
|
+
"Toggle casefile XP (offensive) mode. Bare /xp and /xp on select SWARM, the bounded multi-agent workflow. LITE keeps XP single-agent. OFF (default) keeps context quiet for normal dev work. Usage: /xp [on|lite|swarm|off]",
|
|
1783
1801
|
handler: async (args, ctx) => {
|
|
1784
1802
|
const next = parseXpModeArg(args ?? "", readXpMode());
|
|
1785
1803
|
writeXpMode(next);
|
|
@@ -1790,23 +1808,25 @@ ${formatCaseDetail(record)}`,
|
|
|
1790
1808
|
if (next !== "off") workflowInjected = false;
|
|
1791
1809
|
ctx.ui.notify(
|
|
1792
1810
|
`Casefile XP mode: ${next.toUpperCase()} (takes effect on the next prompt)`,
|
|
1793
|
-
next === "
|
|
1811
|
+
next === "off" ? "warning" : "info",
|
|
1794
1812
|
);
|
|
1795
1813
|
},
|
|
1796
1814
|
});
|
|
1797
1815
|
|
|
1798
1816
|
// ── Tool: PipelineSubmit ──
|
|
1799
1817
|
|
|
1800
|
-
|
|
1818
|
+
registerCaseTool({
|
|
1801
1819
|
name: "PipelineSubmit",
|
|
1802
1820
|
label: "Submit Stage Output",
|
|
1803
1821
|
description:
|
|
1804
1822
|
"Submit a pipeline stage's output (hunt, trace, skeptic, validate, chain, report) through the validation gate. Validates required fields against the stage spec (mirrors schemas/*.json), applies the deterministic pre-filter (test-path and file-existence filters on hunt findings, trivial dedup by file+class+line), and counts repair attempts (max 2, then rejected). A stage cannot advance on an invalid output — submit fixed output until accepted.",
|
|
1805
1823
|
promptSnippet: "Validate and submit a pipeline stage's output",
|
|
1806
1824
|
promptGuidelines: [
|
|
1807
|
-
"Every stage output
|
|
1825
|
+
"Every delegated stage output and every main-agent VALIDATE/REPORT output must go through PipelineSubmit before the next stage starts — do not eyeball schemas.",
|
|
1808
1826
|
"verdict repair → fix the listed fields and re-submit the same output; budget is 2 attempts per finding, then rejected.",
|
|
1809
|
-
"Skeptic: unparseable/schema-invalid =
|
|
1827
|
+
"Skeptic: unparseable/schema-invalid = no verdict (repair/re-dispatch); only schema-valid DISPROVEN kills, and schema-valid UNDETERMINED blocks validation.",
|
|
1828
|
+
"Tracer crash/invalid output = no trace verdict; repair or re-dispatch.",
|
|
1829
|
+
'Only schema-valid trace_result: "UNREACHABLE" blocks advancement as a proven unreachable path; schema-valid UNDETERMINED blocks validation until resolved.',
|
|
1810
1830
|
"Test-path findings and hallucinated files are rejected by the pre-filter, not repairable — the finding itself is noise.",
|
|
1811
1831
|
],
|
|
1812
1832
|
parameters: Type.Object(
|
|
@@ -1866,7 +1886,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1866
1886
|
|
|
1867
1887
|
// ── Tool: ScratchpadInit ──
|
|
1868
1888
|
|
|
1869
|
-
|
|
1889
|
+
registerCaseTool({
|
|
1870
1890
|
name: "ScratchpadInit",
|
|
1871
1891
|
label: "Init Scratchpad",
|
|
1872
1892
|
description:
|
|
@@ -1904,7 +1924,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1904
1924
|
|
|
1905
1925
|
// ── Tool: ScratchpadResume ──
|
|
1906
1926
|
|
|
1907
|
-
|
|
1927
|
+
registerCaseTool({
|
|
1908
1928
|
name: "ScratchpadResume",
|
|
1909
1929
|
label: "Resume Scratchpad",
|
|
1910
1930
|
description:
|
|
@@ -1963,7 +1983,7 @@ ${formatCaseDetail(record)}`,
|
|
|
1963
1983
|
|
|
1964
1984
|
// ── Tool: ScratchpadCheckpoint ──
|
|
1965
1985
|
|
|
1966
|
-
|
|
1986
|
+
registerCaseTool({
|
|
1967
1987
|
name: "ScratchpadCheckpoint",
|
|
1968
1988
|
label: "Checkpoint Phase",
|
|
1969
1989
|
description:
|
|
@@ -2013,7 +2033,7 @@ ${formatCaseDetail(record)}`,
|
|
|
2013
2033
|
|
|
2014
2034
|
// ── Tool: ScratchpadWrite ──
|
|
2015
2035
|
|
|
2016
|
-
|
|
2036
|
+
registerCaseTool({
|
|
2017
2037
|
name: "ScratchpadWrite",
|
|
2018
2038
|
label: "Write Artifact",
|
|
2019
2039
|
description:
|
|
@@ -2059,7 +2079,7 @@ ${formatCaseDetail(record)}`,
|
|
|
2059
2079
|
|
|
2060
2080
|
// ── Tool: ScratchpadRead ──
|
|
2061
2081
|
|
|
2062
|
-
|
|
2082
|
+
registerCaseTool({
|
|
2063
2083
|
name: "ScratchpadRead",
|
|
2064
2084
|
label: "Read Artifact",
|
|
2065
2085
|
description:
|
|
@@ -2116,7 +2136,7 @@ ${formatCaseDetail(record)}`,
|
|
|
2116
2136
|
|
|
2117
2137
|
// ── Tool: ScratchpadPhaseDone ──
|
|
2118
2138
|
|
|
2119
|
-
|
|
2139
|
+
registerCaseTool({
|
|
2120
2140
|
name: "ScratchpadPhaseDone",
|
|
2121
2141
|
label: "Phase Done?",
|
|
2122
2142
|
description:
|
|
@@ -2159,7 +2179,7 @@ ${formatCaseDetail(record)}`,
|
|
|
2159
2179
|
|
|
2160
2180
|
// ── Tool: ScratchpadClear ──
|
|
2161
2181
|
|
|
2162
|
-
|
|
2182
|
+
registerCaseTool({
|
|
2163
2183
|
name: "ScratchpadClear",
|
|
2164
2184
|
label: "Clear Run",
|
|
2165
2185
|
description:
|