@xaccefy/pi-casefile 0.8.0 → 0.8.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/README.md CHANGED
@@ -62,11 +62,11 @@ hypothesis → investigating → confirmed → reported
62
62
  | `CaseAdd` | Open a case (`title` + `disproveIf` required; start as `hypothesis` or `investigating`) |
63
63
  | `CaseUpdate` | Evidence, impact, severity, status (not direct confirm) |
64
64
  | `EvidenceAdd` | Role-typed, hashed evidence item on a case (refutation justifies kills; cleanup tracks cleanup) |
65
- | `PromoteFinding` | Run on-disk PoC (Docker sandbox by default; `local:true` for host) → confirm on exit 0 + marker; `control_path` required for live findings |
65
+ | `PromoteFinding` | Run on-disk PoC (Docker sandbox by default; `local:true` = host-network sandbox, host execution operator-gated via `PI_POC_ALLOW_LOCAL=1`) → confirm on exit 0 + marker; `control_path` + `control_liveness_marker` REQUIRED for every promotion |
66
66
  | `CaseGet` / `CaseList` / `CaseSearch` | Read / filter / search |
67
67
  | `CaseLink` / `CaseUnlink` | Bidirectional exploit chains |
68
68
  | `ChainSuggest` | Scan cases for exploitable chain combinations (credential+endpoint→ATO, redirect+OAuth→token theft, XSS+state-change→CSRF, IDOR+user-data, SSTI→RCE, race+payment, info-disclosure+SSRF), ranked by confidence |
69
- | `CoverageAdd` | Record a tested (asset × attack-class) cell — `scope: wide` (deployment-wide verdict, applies to every later asset) or `local`; both found and clean results count |
69
+ | `CoverageAdd` | Record a tested (asset × attack-class) cell — `scope: wide` (deployment-wide verdict, applies to every later asset) or `local`; both found and clean results count. Optionally link the cell to an artifact-backed evidence item (`evidence_item_id`); unbacked cells render as ⚠ unbacked in the report |
70
70
  | `CoverageReport` | Render the machine-checkable coverage matrix (which classes are tested where; plateau claims must match it) |
71
71
  | `CaseContext` | Context bundle for a confirmed/reported case (full record, logs, links, artifacts) + report path |
72
72
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xaccefy/pi-casefile",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Offensive security case tracker for Pi Agent — bug bounties, CTFs, security audits",
5
5
  "keywords": [
6
6
  "pi-package",
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * Event: before_agent_start — injects cyber workflow once per session, refreshes the active case list per prompt
7
7
  */
8
8
 
9
+ import { createHash } from "node:crypto";
9
10
  import { existsSync, readFileSync, writeFileSync } from "node:fs";
10
11
  import { dirname, join } from "node:path";
11
12
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
@@ -55,7 +56,7 @@ import {
55
56
  writeCaseContext,
56
57
  } from "./ledger.ts";
57
58
  import { pipeline_submit, SUBMIT_STAGES, type SubmitStage } from "./pipeline-submit.ts";
58
- import { type PocRun, runPoc } from "./poc-runner.ts";
59
+ import { type PocRun, type PocRunOptions, runPoc } from "./poc-runner.ts";
59
60
  import {
60
61
  PHASE_ORDER,
61
62
  type ScratchpadPhase,
@@ -171,16 +172,24 @@ const PromoteSchema = Type.Object(
171
172
  disconfirmation_path: Type.Optional(
172
173
  Type.String({
173
174
  description:
174
- "Absolute path to a disconfirmation script that tries to disprove the finding; must exit non-zero (failure to disprove)",
175
+ "Absolute path to a disconfirmation script that tries to disprove the finding; must exit non-zero (failure to disprove). REQUIRED for severity high/critical findings.",
175
176
  }),
176
177
  ),
177
- control_path: Type.Optional(
178
- Type.String({
178
+ control_path: Type.String({
179
+ description:
180
+ "REQUIRED for EVERY promotion (sandboxed and live alike): absolute path to a control-target script that runs the SAME PoC against a control that lacks the vulnerability (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control output — an unconditional-marker/mock PoC cannot pass. Best form: the same parameterized script, branching on the PI_POC_MODE env var (poc | control) the harness sets on every run.",
181
+ }),
182
+ control_liveness_marker: Type.String({
183
+ minLength: 1,
184
+ description:
185
+ "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.",
186
+ }),
187
+ local: Type.Optional(
188
+ Type.Boolean({
179
189
  description:
180
- "Absolute path to a control-target script: runs the SAME PoC against a control that lacks the vulnerability. The harness checks the control run's output if the verification_marker appears there, promotion is BLOCKED (the PoC prints the marker without the vulnerable condition). REQUIRED when local:true (live findings).",
190
+ "Run with network access (Docker sandbox with --network host still read-only FS, dropped capabilities, unprivileged user) instead of the isolated --network none sandbox. True host execution is NOT agent-selectable: it requires the operator to set PI_POC_ALLOW_LOCAL=1, and is used only as a fallback when Docker is unavailable.",
181
191
  }),
182
192
  ),
183
- local: Type.Optional(Type.Boolean({ description: "Run locally instead of in Docker sandbox" })),
184
193
  },
185
194
  { additionalProperties: false },
186
195
  );
@@ -890,6 +899,12 @@ export default function casefileExtension(pi: ExtensionAPI) {
890
899
  description:
891
900
  "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.",
892
901
  }),
902
+ evidence_item_id: Type.Optional(
903
+ Type.String({
904
+ description:
905
+ "Evidence item id backing this tested verdict (must be an artifact-backed EvidenceAdd item on this case). Cells without a backing item render as 'unbacked' in CoverageReport — 'tested' claims must be machine-checkable, not prose-only.",
906
+ }),
907
+ ),
893
908
  },
894
909
  { additionalProperties: false },
895
910
  );
@@ -914,6 +929,7 @@ export default function casefileExtension(pi: ExtensionAPI) {
914
929
  class: params.class as string,
915
930
  scope: (params.scope ?? "local") as CoverageScope,
916
931
  note: params.note as string,
932
+ evidenceItemId: params.evidence_item_id as string | undefined,
917
933
  });
918
934
  const record = getCaseById(params.case_id as string)!;
919
935
  return {
@@ -979,7 +995,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
979
995
  lines.push(`\n## ${asset}`);
980
996
  for (const cell of summary.byAsset[asset] ?? []) {
981
997
  lines.push(
982
- `- [${cell.scope}] ${cell.class} — ${cell.note}${cell.testedBy ? ` (by ${cell.testedBy})` : ""}`,
998
+ `- [${cell.scope}] ${cell.class} — ${cell.note}${cell.testedBy ? ` (by ${cell.testedBy})` : ""}` +
999
+ (cell.evidenceItemId
1000
+ ? ""
1001
+ : " ⚠ unbacked (link an artifact-backed evidence item via CoverageAdd evidence_item_id)"),
983
1002
  );
984
1003
  }
985
1004
  }
@@ -1009,15 +1028,16 @@ export default function casefileExtension(pi: ExtensionAPI) {
1009
1028
  name: "PromoteFinding",
1010
1029
  label: "Promote Finding",
1011
1030
  description:
1012
- "Run an on-disk PoC script (Docker sandbox or local) and, on exit 0 + verification marker present in output, promote an investigating case to confirmed. The verification_marker proves the exploit actually worked — exit code 0 alone is NOT sufficient. For live findings (local:true) a control_path script is REQUIRED: the same PoC run against a control lacking the vuln must NOT print the marker this blocks unconditional-marker and mock-target cheats. Optionally run a disconfirmation script that must exit non-0 (finding survived the attempt to disprove).",
1031
+ "Run an on-disk PoC script (Docker sandbox or host-network sandbox) and, on exit 0 + verification marker present in output, promote an investigating case to confirmed. The verification_marker proves the exploit actually worked — exit code 0 alone is NOT sufficient. EVERY promotion (sandboxed and live alike) REQUIRES: (1) a disconfirmation_path script that must exit non-zero (the finding survived the attempt to disprove it); (2) a control_path that is the SAME script as the PoC (sha256-enforced a separately written control file is rejected), run in control mode via PI_POC_MODE; (3) a control_liveness_marker the control must print after reaching its target. The harness blocks promotion if the vuln marker appears in the (untruncated) control output, if the liveness marker is absent, or if the control/disconfirmation scripts crash. Host execution is never agent-selectable local:true uses a host-network Docker sandbox; true host runs need the operator's PI_POC_ALLOW_LOCAL=1.",
1013
1032
  promptSnippet: "Run a PoC and promote an investigating case to confirmed",
1014
1033
  promptGuidelines: [
1015
1034
  "Use PromoteFinding when an investigating case has a concrete PoC script on disk and you are ready to prove it.",
1016
- "Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target, disconfirmation, plus an EvidenceAdd 'observation' item on the case (the initial signal) — the PoC gate auto-records the reproduction item.",
1017
- "Default sandbox: docker run --rm --network none. Use local:true for network-dependent bugs.",
1035
+ "Prerequisites: status='investigating' and non-empty poc, evidence, impact, severity, target, disconfirmation, plus an artifact-backed EvidenceAdd 'observation' item on the case (the initial signal, with artifact_path) — the PoC gate auto-records the reproduction item.",
1036
+ "Default sandbox: docker run --rm --network none. Use local:true for network-dependent bugs (host-network sandbox; host execution needs operator PI_POC_ALLOW_LOCAL=1).",
1018
1037
  "Gate: exit 0 AND verification_marker in the PoC output. The marker (e.g. 'VULN_CONFIRMED_<case-id>') must be printed only AFTER the exploit is verified (data extracted, callback received, payload reflected) — never unconditionally or before the exploit check. The marker check prevents fluke exit 0 (script crashed early) and mocked PoCs (target faked) from passing.",
1019
- "control_path (REQUIRED for local:true): a script that runs the SAME PoC against a control lacking the vuln (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control run's output — an unconditional-marker PoC cannot pass this.",
1020
- "disconfirmation_path: a script that tries to disprove the finding; if it exits 0, promotion is blocked.",
1038
+ "control_path (REQUIRED for EVERY promotion): the SAME script as poc_path (sha256-equality is ENFORCED — a separately written control file is rejected). One parameterized script read the target from the PI_POC_TARGET env var and branch on PI_POC_MODE (poc | control) — the control run is literally the same script in control mode against a control lacking the vuln (patched replica, second account, baseline endpoint). The harness blocks promotion if the verification_marker appears in the control run's output (checked on the untruncated output) — an unconditional-marker PoC cannot pass this.",
1039
+ "control_liveness_marker (REQUIRED): a unique string the control script prints only AFTER reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). The harness blocks promotion if the control output lacks it a control pointed at an unreachable host, wrong port, or exiting before the check is NOT a clean verdict.",
1040
+ "disconfirmation_path: a script that tries to disprove the finding; if it exits 0, promotion is blocked. REQUIRED for EVERY promotion — the prose disconfirmation field is not enough at any severity (a case filed low/medium must not skip the run and be re-raised afterwards).",
1021
1041
  "Never CaseUpdate status='confirmed' directly — it is rejected. Always use PromoteFinding.",
1022
1042
  ],
1023
1043
  parameters: PromoteSchema,
@@ -1025,9 +1045,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
1025
1045
  async execute(_id, params, _signal, _onUpdate, _ctx) {
1026
1046
  // Validate promotability BEFORE running the PoC — a sandboxed run can take
1027
1047
  // 30s (plus first-time image pull), so fail cheap when the case can't
1028
- // advance anyway (missing, wrong status, missing required fields).
1048
+ // advance anyway (missing, wrong status, missing required fields, missing
1049
+ // artifact-backed observation evidence).
1029
1050
  const caseId = params.id as string;
1030
- assertPromotable(caseId);
1051
+ const current = assertPromotable(caseId);
1031
1052
 
1032
1053
  // Shared blocked-promotion shape: the case stays investigating and the
1033
1054
  // caller gets the record back for context.
@@ -1048,20 +1069,100 @@ export default function casefileExtension(pi: ExtensionAPI) {
1048
1069
  );
1049
1070
  }
1050
1071
 
1051
- // Live findings require control_path check BEFORE paying for the PoC
1052
- // run (an agent that forgot the control script shouldn't burn a 30s
1053
- // sandboxed run to be told).
1054
- if (params.local === true && !params.control_path) {
1072
+ // Control-target anti-cheat is mandatory for EVERY promotion sandboxed
1073
+ // and live alike. It is the only deterministic check that the marker is
1074
+ // target-dependent; skipping it for the default sandboxed mode would let
1075
+ // an unconditional-marker PoC pass untouched. Check BEFORE paying for the
1076
+ // PoC run.
1077
+ const controlPath = (params.control_path as string | undefined)?.trim();
1078
+ if (!controlPath) {
1055
1079
  return fail(
1056
- "Live findings (local:true) require control_path: a script that runs the same PoC " +
1057
- "against a control lacking the vuln. The harness verifies the verification_marker is " +
1058
- "absent from the control run's output — that is what proves the marker is target-dependent. " +
1059
- "Write the control script and retry.",
1080
+ "control_path is REQUIRED for every promotion (sandboxed and live alike): a script that runs " +
1081
+ "the SAME PoC against a control lacking the vuln (patched replica, second account, baseline endpoint). " +
1082
+ "The harness verifies the verification_marker is absent from the control run's output — that is what " +
1083
+ "proves the marker is target-dependent. Write the control script and retry.",
1060
1084
  { missingControl: true },
1061
1085
  );
1062
1086
  }
1063
1087
 
1064
- const run = runPoc(params.poc_path as string, params.local !== true);
1088
+ // The control must ALSO prove it reached its target: without a liveness
1089
+ // marker, a control pointed at an unreachable host / wrong port / early
1090
+ // exit would show "marker absent" for reasons unrelated to the vuln.
1091
+ const livenessMarker = (params.control_liveness_marker as string | undefined)?.trim();
1092
+ if (!livenessMarker) {
1093
+ return fail(
1094
+ "control_liveness_marker is REQUIRED for every promotion: a unique string the control script " +
1095
+ "prints ONLY AFTER reaching/exercising the control target (e.g. 'CONTROL_REACHED_<case-id>'). " +
1096
+ "The harness blocks promotion if the control output lacks it — a control that never reached its " +
1097
+ "target proves nothing. Must differ from verification_marker.",
1098
+ { missingLivenessMarker: true },
1099
+ );
1100
+ }
1101
+ if (livenessMarker === marker) {
1102
+ return fail(
1103
+ "control_liveness_marker must differ from verification_marker — the liveness marker proves the " +
1104
+ "control reached its target, the verification marker proves the vuln fired. Use distinct strings.",
1105
+ { livenessEqualsMarker: true },
1106
+ );
1107
+ }
1108
+
1109
+ // EXECUTED disconfirmation is required for EVERY promotion — the prose
1110
+ // `disconfirmation` field cannot carry the disprove-attempt. Making it
1111
+ // unconditional (not severity-keyed) also kills the ordering attack:
1112
+ // a case filed as low/medium could previously skip the run, promote,
1113
+ // then be re-raised to high/critical with no executed disproof.
1114
+ const disconfirmationPath = (params.disconfirmation_path as string | undefined)?.trim();
1115
+ if (!disconfirmationPath) {
1116
+ return fail(
1117
+ "disconfirmation_path is REQUIRED for every promotion: a script that tries to " +
1118
+ "disprove the finding and must exit non-zero (the finding survived the attempt). " +
1119
+ "The prose disconfirmation field is not enough. Write the disconfirmation script and retry.",
1120
+ { missingDisconfirmation: true },
1121
+ );
1122
+ }
1123
+
1124
+ // Same-file contract (anti-cheat): the control must be the SAME script as
1125
+ // the PoC, differing only via the harness-set PI_POC_MODE env var. Two
1126
+ // agent-written files (poc prints the marker unconditionally, control
1127
+ // prints the liveness marker) pass every string check — the "same script"
1128
+ // recommendation in the docs is now enforced: sha256(control) must equal
1129
+ // sha256(poc). Hash BEFORE any run so a mismatch fails cheap.
1130
+ const pocPath = (params.poc_path as string | undefined)?.trim() ?? "";
1131
+ let pocHash: string | undefined;
1132
+ let controlHash: string | undefined;
1133
+ try {
1134
+ pocHash = createHash("sha256").update(readFileSync(pocPath)).digest("hex");
1135
+ controlHash = createHash("sha256").update(readFileSync(controlPath)).digest("hex");
1136
+ } catch (e) {
1137
+ return fail(
1138
+ `Cannot read PoC/control scripts for the same-file check: ${(e as Error).message}`,
1139
+ { sameFileCheckFailed: true },
1140
+ );
1141
+ }
1142
+ if (pocHash !== controlHash) {
1143
+ return fail(
1144
+ "CONTROL CHECK FAILED: control_path must be the SAME script as poc_path " +
1145
+ "(sha256 mismatch). The control run is only meaningful as the same PoC " +
1146
+ "pointed at a control target via PI_POC_MODE=control — a separately written " +
1147
+ "control file proves nothing (the model writes both files). Parameterize " +
1148
+ "one script: branch on PI_POC_MODE (poc | control) and read the target from " +
1149
+ "PI_POC_TARGET. Case remains investigating.",
1150
+ { run: undefined, controlHashMismatch: true },
1151
+ );
1152
+ }
1153
+
1154
+ // local:true now means "network access": with the operator's
1155
+ // PI_POC_ALLOW_LOCAL=1 opt-in the run executes on the host; WITHOUT the
1156
+ // opt-in it uses a Docker sandbox with --network host (same FS/cap/user
1157
+ // isolation) and fails closed when Docker is unavailable too. Host
1158
+ // execution is never agent-selectable on its own.
1159
+ const runOptions = (pocMode: string): PocRunOptions => ({
1160
+ network: params.local === true ? "host" : "none",
1161
+ local: params.local === true,
1162
+ env: { PI_POC_MODE: pocMode, PI_POC_TARGET: current.target ?? "" },
1163
+ });
1164
+
1165
+ const run = runPoc(pocPath, runOptions("poc"));
1065
1166
 
1066
1167
  // Fail closed without throwing: non-zero PoC must leave the case investigating.
1067
1168
  if (run.exitCode !== 0) {
@@ -1081,11 +1182,12 @@ export default function casefileExtension(pi: ExtensionAPI) {
1081
1182
  }
1082
1183
 
1083
1184
  // Verification marker check: exit code 0 alone is NOT sufficient.
1084
- // The PoC must print the verification_marker to stdout, proving the
1085
- // exploit actually worked — not just that the script ran. This blocks
1086
- // fluke exit 0 (crash before real logic) and mocked PoCs that don't
1087
- // actually exploit the target.
1088
- if (!run.output.includes(marker)) {
1185
+ // The PoC must print the verification_marker, proving the exploit
1186
+ // actually worked — not just that the script ran. The check runs on
1187
+ // rawOutput (untruncated) so a script printing its marker past the
1188
+ // 4000-char display window cannot hide it.
1189
+ const pocOut = run.rawOutput ?? run.output;
1190
+ if (!pocOut.includes(marker)) {
1089
1191
  return fail(
1090
1192
  `PoC exited 0 but the verification marker "${marker}" was NOT found in the output.\n` +
1091
1193
  `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` +
@@ -1094,10 +1196,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
1094
1196
  );
1095
1197
  }
1096
1198
 
1097
- // Run disconfirmation script if provided — must exit NON-0 (finding survived the attempt to disprove).
1199
+ // Run disconfirmation script — must exit NON-0 (finding survived the attempt to disprove).
1098
1200
  let disconfirmationRun: PocRun | undefined;
1099
- if (params.disconfirmation_path) {
1100
- disconfirmationRun = runPoc(params.disconfirmation_path as string, params.local !== true);
1201
+ if (disconfirmationPath) {
1202
+ disconfirmationRun = runPoc(disconfirmationPath, runOptions("disconfirmation"));
1101
1203
  if (!disconfirmationRun.completed) {
1102
1204
  return fail(
1103
1205
  `Disconfirmation script did NOT complete (spawn error, killed, or timeout — no completion marker). ` +
@@ -1118,30 +1220,47 @@ export default function casefileExtension(pi: ExtensionAPI) {
1118
1220
  // Control-target anti-cheat check: the same PoC pointed at a control that
1119
1221
  // lacks the vuln must NOT print the marker. The control script is
1120
1222
  // agent-written, but the marker-absence check is harness-side and
1121
- // deterministic — the model cannot pass it by asserting success.
1122
- let controlRun: PocRun | undefined;
1123
- if (params.control_path) {
1124
- controlRun = runPoc(params.control_path as string, params.local !== true);
1125
- if (!controlRun.completed) {
1126
- return fail(
1127
- `CONTROL CHECK FAILED: the control-target script did NOT complete (spawn error, killed, or timeout). ` +
1128
- `A control run that never executed proves nothing about the marker — fix the control script and retry.\n` +
1129
- `Control output:\n${controlRun.output}`,
1130
- { run, controlRun, controlCrashed: true },
1131
- );
1132
- }
1133
- if (controlRun.output.includes(marker)) {
1134
- return fail(
1135
- `CONTROL CHECK FAILED: the verification marker "${marker}" appeared in the control-target run. ` +
1136
- `The PoC prints the marker without the vulnerable condition — a cheating PoC (unconditional marker) ` +
1137
- `or a broken check. Case remains investigating.\nControl output:\n${controlRun.output}`,
1138
- { run, controlRun, controlCheated: true },
1139
- );
1140
- }
1223
+ // deterministic — the model cannot pass it by asserting success. The
1224
+ // liveness-marker check closes the "control pointed at an unreachable
1225
+ // host / exited early" hole: the control must prove it reached its target.
1226
+ const controlRun = runPoc(controlPath, runOptions("control"));
1227
+ if (!controlRun.completed) {
1228
+ return fail(
1229
+ `CONTROL CHECK FAILED: the control-target script did NOT complete (spawn error, killed, or timeout). ` +
1230
+ `A control run that never executed proves nothing about the marker — fix the control script and retry.\n` +
1231
+ `Control output:\n${controlRun.output}`,
1232
+ { run, controlRun, controlCrashed: true },
1233
+ );
1234
+ }
1235
+ // Marker-absence + liveness checks run on the UNTRUNCATED control output.
1236
+ const controlOut = controlRun.rawOutput ?? controlRun.output;
1237
+ if (controlOut.includes(marker)) {
1238
+ return fail(
1239
+ `CONTROL CHECK FAILED: the verification marker "${marker}" appeared in the control-target run. ` +
1240
+ `The PoC prints the marker without the vulnerable condition — a cheating PoC (unconditional marker) ` +
1241
+ `or a broken check. Case remains investigating.\nControl output:\n${controlRun.output}`,
1242
+ { run, controlRun, controlCheated: true },
1243
+ );
1244
+ }
1245
+ if (!controlOut.includes(livenessMarker)) {
1246
+ return fail(
1247
+ `CONTROL CHECK FAILED: the control-target run completed but the control_liveness_marker "${livenessMarker}" ` +
1248
+ `was NOT found in its output. The control must print the liveness marker only AFTER reaching/exercising ` +
1249
+ `the control target — an unreachable host, wrong port, or early exit is not a valid control verdict. ` +
1250
+ `Case remains investigating.\nControl output:\n${controlRun.output}`,
1251
+ { run, controlRun, controlLivenessMissing: true },
1252
+ );
1141
1253
  }
1142
1254
 
1143
1255
  // PocRun is structurally a PocVerification — pass the runs straight through.
1144
- const result = promoteFindingResult(caseId, run, disconfirmationRun, controlRun, marker);
1256
+ const result = promoteFindingResult(
1257
+ caseId,
1258
+ run,
1259
+ disconfirmationRun,
1260
+ controlRun,
1261
+ marker,
1262
+ livenessMarker,
1263
+ );
1145
1264
  const record = result.record;
1146
1265
  return {
1147
1266
  content: [
@@ -1487,6 +1606,11 @@ export default function casefileExtension(pi: ExtensionAPI) {
1487
1606
  handler: async (args, ctx) => {
1488
1607
  const next = parseXpModeArg(args ?? "", readXpMode());
1489
1608
  writeXpMode(next);
1609
+ // Re-enabling after a mid-session /xp off must re-inject the workflow
1610
+ // on the next prompt — otherwise workflowInjected (module-level, set on
1611
+ // first enable) stays true and the workflow never comes back until the
1612
+ // process restarts.
1613
+ if (next !== "off") workflowInjected = false;
1490
1614
  ctx.ui.notify(
1491
1615
  `Casefile XP mode: ${next.toUpperCase()} (takes effect on the next prompt)`,
1492
1616
  next === "on" ? "info" : "warning",