@xaccefy/pi-casefile 0.9.0 → 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/src/scratchpad.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * verify/ — PoC logs, run outputs (validate phase)
17
17
  * chain/ — exploit-chain analysis
18
18
  * patch/ — remediation work
19
- * report/ — report-writer context
19
+ * report/ — final report context
20
20
  * state.json — checkpoint file with phase completion + key IDs
21
21
  *
22
22
  * Resume re-reads scratchpad artifacts; it does not re-run completed phases
@@ -25,16 +25,15 @@
25
25
  */
26
26
 
27
27
  import { createHash } from "node:crypto";
28
+ import { existsSync, readdirSync, rmSync } from "node:fs";
29
+ import { basename, dirname, join, resolve } from "node:path";
28
30
  import {
29
- existsSync,
30
- mkdirSync,
31
- readdirSync,
32
- readFileSync,
33
- renameSync,
34
- rmSync,
35
- writeFileSync,
36
- } from "node:fs";
37
- import { dirname, join, resolve } from "node:path";
31
+ assertSafeRegularFile,
32
+ assertSafeStateDirectory,
33
+ ensureSafeStateDirectory,
34
+ readSafeFile,
35
+ writeSafeFileAtomic,
36
+ } from "./safe-state.ts";
38
37
 
39
38
  // ── Types ────────────────────────────────────────────────────────────
40
39
 
@@ -74,7 +73,9 @@ export interface ScratchpadResume {
74
73
 
75
74
  // ── Constants ────────────────────────────────────────────────────────
76
75
 
77
- export const PHASE_ORDER: ScratchpadPhase[] = [
76
+ // All accepted artifact buckets. Some are legacy/manual-only and should not be
77
+ // scheduled by ScratchpadResume for new swarm runs.
78
+ export const SCRATCHPAD_PHASES: ScratchpadPhase[] = [
78
79
  "recon",
79
80
  "hunt",
80
81
  "gapfil",
@@ -86,6 +87,17 @@ export const PHASE_ORDER: ScratchpadPhase[] = [
86
87
  "report",
87
88
  ];
88
89
 
90
+ // Active pipeline order for new/resumed runs.
91
+ export const PHASE_ORDER: ScratchpadPhase[] = [
92
+ "recon",
93
+ "hunt",
94
+ "trace",
95
+ "skeptic",
96
+ "validate",
97
+ "chain",
98
+ "report",
99
+ ];
100
+
89
101
  const PHASE_DIRS: Record<ScratchpadPhase, string> = {
90
102
  recon: "recon",
91
103
  hunt: "hunt",
@@ -215,17 +227,23 @@ function emptyCheckpoint(runId: string, projectRoot: string): ScratchpadCheckpoi
215
227
  }
216
228
 
217
229
  function ensureRunDirs(runDir: string): void {
218
- if (!existsSync(runDir)) mkdirSync(runDir, { recursive: true });
219
- for (const phase of PHASE_ORDER) {
220
- const dir = join(runDir, PHASE_DIRS[phase]);
221
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
230
+ const scratchpadRoot = dirname(runDir);
231
+ const projectRoot = dirname(scratchpadRoot);
232
+ const runName = basename(runDir);
233
+ ensureSafeStateDirectory(projectRoot, [SCRATCHPAD_DIR, runName]);
234
+ for (const phase of SCRATCHPAD_PHASES) {
235
+ ensureSafeStateDirectory(projectRoot, [SCRATCHPAD_DIR, runName, PHASE_DIRS[phase]]);
222
236
  }
223
237
  }
224
238
 
225
239
  function readCheckpointRaw(runId: string, projectRoot?: string): ScratchpadCheckpoint | null {
240
+ const root = projectRoot ?? detectWorkspaceRoot();
226
241
  const statePath = getStatePath(runId, projectRoot);
227
- if (!existsSync(statePath)) return null;
228
- const raw = readFileSync(statePath, "utf8");
242
+ if (existsSync(statePath)) {
243
+ assertSafeStateDirectory(root, [SCRATCHPAD_DIR, runDirName(runId)]);
244
+ }
245
+ if (!assertSafeRegularFile(statePath, "Scratchpad checkpoint")) return null;
246
+ const raw = readSafeFile(statePath, "Scratchpad checkpoint").toString("utf8");
229
247
  const cp = JSON.parse(raw) as ScratchpadCheckpoint;
230
248
  if (typeof cp !== "object" || cp === null || Array.isArray(cp)) {
231
249
  throw new Error(`Corrupt scratchpad state for ${runId}: root must be an object`);
@@ -237,7 +255,7 @@ function readCheckpointRaw(runId: string, projectRoot?: string): ScratchpadCheck
237
255
  throw new Error(`Corrupt scratchpad state for ${runId}: completed_phases must be an array`);
238
256
  }
239
257
  for (const phase of cp.completed_phases) {
240
- if (!PHASE_ORDER.includes(phase)) {
258
+ if (!SCRATCHPAD_PHASES.includes(phase)) {
241
259
  throw new Error(`Corrupt scratchpad state for ${runId}: invalid phase ${phase}`);
242
260
  }
243
261
  }
@@ -258,9 +276,7 @@ function writeCheckpointRaw(cp: ScratchpadCheckpoint, projectRoot?: string): voi
258
276
  cp.last_updated = new Date().toISOString();
259
277
  const statePath = getStatePath(cp.run_id, projectRoot);
260
278
  ensureRunDirs(getRunDir(cp.run_id, projectRoot));
261
- const tmp = `${statePath}.${process.pid}.${Date.now()}.tmp`;
262
- writeFileSync(tmp, JSON.stringify(cp, null, 2), "utf8");
263
- renameSync(tmp, statePath);
279
+ writeSafeFileAtomic(statePath, JSON.stringify(cp, null, 2));
264
280
  }
265
281
 
266
282
  // ── Public API ───────────────────────────────────────────────────────
@@ -309,7 +325,7 @@ export function scratchpad_write(
309
325
  const safeName = artifactFileName(artifactName);
310
326
  const dir = join(runDir, PHASE_DIRS[phase]);
311
327
  const filePath = join(dir, safeName);
312
- writeFileSync(filePath, content, "utf8");
328
+ writeSafeFileAtomic(filePath, content);
313
329
  return filePath;
314
330
  }
315
331
 
@@ -325,8 +341,11 @@ export function scratchpad_read(
325
341
  const root = projectRoot ?? detectWorkspaceRoot();
326
342
  const safeName = artifactFileName(artifactName);
327
343
  const filePath = join(getRunDir(runId, root), PHASE_DIRS[phase], safeName);
328
- if (!existsSync(filePath)) return null;
329
- return readFileSync(filePath, "utf8");
344
+ if (existsSync(filePath)) {
345
+ assertSafeStateDirectory(root, [SCRATCHPAD_DIR, runDirName(runId), PHASE_DIRS[phase]]);
346
+ }
347
+ if (!assertSafeRegularFile(filePath, "Scratchpad artifact")) return null;
348
+ return readSafeFile(filePath, "Scratchpad artifact").toString("utf8");
330
349
  }
331
350
 
332
351
  /**
@@ -335,13 +354,16 @@ export function scratchpad_read(
335
354
  export function scratchpad_runs(projectRoot?: string): string[] {
336
355
  const root = getScratchpadRoot(projectRoot);
337
356
  if (!existsSync(root)) return [];
357
+ assertSafeStateDirectory(dirname(root), [SCRATCHPAD_DIR]);
338
358
  const out: string[] = [];
339
359
  for (const entry of readdirSync(root, { withFileTypes: true })) {
340
360
  if (!entry.isDirectory()) continue;
341
361
  const state = join(root, entry.name, "state.json");
342
- if (!existsSync(state)) continue;
362
+ if (!assertSafeRegularFile(state, "Scratchpad checkpoint")) continue;
343
363
  try {
344
- const cp = JSON.parse(readFileSync(state, "utf8")) as { run_id?: unknown };
364
+ const cp = JSON.parse(readSafeFile(state, "Scratchpad checkpoint").toString("utf8")) as {
365
+ run_id?: unknown;
366
+ };
345
367
  if (typeof cp.run_id !== "string") continue;
346
368
  if (getRunDir(cp.run_id, projectRoot) === join(root, entry.name)) {
347
369
  out.push(cp.run_id);
@@ -366,7 +388,10 @@ export function scratchpad_list(
366
388
  const root = projectRoot ?? detectWorkspaceRoot();
367
389
  const dir = join(getRunDir(runId, root), PHASE_DIRS[phase]);
368
390
  if (!existsSync(dir)) return [];
369
- return readdirSync(dir).filter((f) => f !== "state.json");
391
+ assertSafeStateDirectory(root, [SCRATCHPAD_DIR, runDirName(runId), PHASE_DIRS[phase]]);
392
+ return readdirSync(dir).filter(
393
+ (f) => f !== "state.json" && assertSafeRegularFile(join(dir, f), "Scratchpad artifact"),
394
+ );
370
395
  }
371
396
 
372
397
  /**
@@ -387,7 +412,7 @@ export function scratchpad_checkpoint(
387
412
  if (!cp.completed_phases.includes(phase)) {
388
413
  cp.completed_phases.push(phase);
389
414
  // Keep completed_phases in pipeline order for predictable resume.
390
- cp.completed_phases.sort((a, b) => PHASE_ORDER.indexOf(a) - PHASE_ORDER.indexOf(b));
415
+ cp.completed_phases.sort((a, b) => SCRATCHPAD_PHASES.indexOf(a) - SCRATCHPAD_PHASES.indexOf(b));
391
416
  }
392
417
  cp.last_phase_at = new Date().toISOString();
393
418
  if (data.ids) cp.phase_ids[phase] = data.ids;
@@ -437,5 +462,8 @@ export function scratchpad_phase_done(
437
462
  export function scratchpad_clear(runId: string, projectRoot?: string): void {
438
463
  const root = projectRoot ?? detectWorkspaceRoot();
439
464
  const runDir = getRunDir(runId, root);
440
- if (existsSync(runDir)) rmSync(runDir, { recursive: true, force: true });
465
+ if (existsSync(runDir)) {
466
+ assertSafeStateDirectory(root, [SCRATCHPAD_DIR, runDirName(runId)]);
467
+ rmSync(runDir, { recursive: true, force: true });
468
+ }
441
469
  }
package/src/workflow.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Cyber workflow injected into agent context when XP mode is ON.
2
+ * Cyber workflow injected into agent context when XP mode is SWARM.
3
3
  *
4
4
  * Skills (cyberwf, web-pentest) cover tool usage and methodology. This file
5
5
  * adds the unique attacker discipline: state machine with preconditions,
@@ -39,42 +39,30 @@ type DispatchSpec = {
39
39
  hardGate: string;
40
40
  /** Crash-handling paragraph. */
41
41
  crash: string;
42
- /** Skeptic dispatch snippet (follows "dispatch it BEFORE the exploit agent with "). */
42
+ /** Skeptic dispatch snippet (follows "dispatch it before main-agent validation with "). */
43
43
  skeptic: string;
44
- /** Confirmer dispatch snippet (follows "dispatch the confirmer: "). */
45
- confirmer: string;
46
- /** Reporter dispatch snippet (follows "Dispatch the reporter subagent with "). */
47
- reporter: string;
48
44
  };
49
45
 
50
46
  const PI_DISPATCH: DispatchSpec = {
51
47
  reference:
52
- "**Subagent dispatch:** every launch uses `subagent({ workflowScript: \"return runs.run('stable-key', { agent: 'tracer', task: '...' })\", context: 'fresh', async: true })`. Parallel HUNT uses one workflowScript with `return runs.all([{ key: 'run-class-attempt', agent: 'auditor', task: '...' }, ...])`. Stable keys include run, stage, class/case, and attempt. Dispatch specialists; do NOT do their work yourself.",
48
+ "**Subagent dispatch:** every launch uses `subagent({ workflowScript: \"return runs.run('stable-key', { agent: 'tracer', task: '...' })\", context: 'fresh', async: true })`. Parallel HUNT uses one workflowScript with `return runs.all([...])`, capped at 3 auditor tasks per round. Batch related classes by surface/family instead of spawning one agent per bug class. Stable keys include run, stage, batch/case, and attempt.",
53
49
  hardGate:
54
- "Your next tool call MUST launch one async workflowScript whose `runs.all([...])` dispatches HUNT auditors.",
50
+ "Your next tool call MUST launch one async workflowScript whose `runs.all([...])` dispatches at most 3 batched HUNT auditors.",
55
51
  crash:
56
- "**Subagent crash handling:** a crash (SIGABRT, OOM, timeout) is a RETRY, not a verdict. Launch one new workflowScript with the same specialist task, a new stable attempt key, and a stronger model. Crash again → record `blocked: <agent> crashed` in the pipeline-run case and continue; never silently drop the stage.",
52
+ "**Subagent failure handling:** a crash, timeout, hung run, unparseable output, or schema-invalid output is a RETRY, not a verdict. Launch one new workflowScript with the same specialist task, a new stable attempt key, and a stronger model. Failure again → record `blocked: <agent> failed` in the pipeline-run case and continue; never silently drop the stage.",
57
53
  skeptic:
58
54
  "`subagent({ workflowScript: \"return runs.run('skeptic-<case>-1', { agent: 'skeptic', task: '...' })\", context: 'fresh', async: true })`",
59
- confirmer:
60
- "`subagent({ workflowScript: \"return runs.run('confirm-<case>-1', { agent: 'confirmer', task: 'Verify the PoC evidence for case <id> (poc_path=..., control_target=..., evidence_sha256=..., poc_sha256=...). Assume fabricated, prove real. Re-send the verify request yourself. Return the verdict.' })\", context: 'fresh', async: true })`",
61
- reporter:
62
- "`subagent({ workflowScript: \"return runs.run('report-<case>-1', { agent: 'reporter', task: 'Write the final report. case_id=<id>, context_path=<context path>, report_path=<report path>, program_name=<if known>.' })\", context: 'fresh', async: true })`",
63
55
  };
64
56
 
65
57
  const OMP_DISPATCH: DispatchSpec = {
66
58
  reference:
67
- "**Subagent dispatch (OMP):** every launch uses `task({ context: 'fresh', tasks: [{ name: 'stable-key', agent: 'tracer', task: '...' }] })`. Parallel HUNT dispatches ONE task call whose `tasks` array carries one entry per attack class: `task({ context: 'fresh', tasks: [{ name: 'hunt-sqli-1', agent: 'auditor', task: '...' }, { name: 'hunt-xss-1', agent: 'auditor', task: '...' }] })`. Stable names include run, stage, class/case, and attempt. Results deliver automatically; steer with `hub`. Dispatch specialists; do NOT do their work yourself.",
59
+ "**Subagent dispatch (OMP):** every launch uses `task({ context: 'fresh', tasks: [{ name: 'stable-key', agent: 'tracer', task: '...' }] })`. Parallel HUNT dispatches ONE task call whose `tasks` array carries at most 3 auditor tasks per round, batching related classes by surface/family instead of spawning one agent per bug class. Stable names include run, stage, batch/case, and attempt. Results deliver automatically; steer with `hub`.",
68
60
  hardGate:
69
- "Your next tool call MUST launch one async `task` call whose `tasks` array dispatches HUNT auditors (one entry per attack class). When their results are delivered, submit each output through PipelineSubmit.",
61
+ "Your next tool call MUST launch one async `task` call whose `tasks` array dispatches at most 3 batched HUNT auditors. When their results are delivered, submit each output through PipelineSubmit.",
70
62
  crash:
71
- "**Subagent crash handling:** a failed or hung task (SIGABRT, OOM, timeout) is a RETRY, not a verdict. Re-dispatch the same specialist task with a new attempt name and a stronger model. Crash again → record `blocked: <agent> crashed` in the pipeline-run case and continue; never silently drop the stage.",
63
+ "**Subagent failure handling:** a failed or hung task, timeout, unparseable output, or schema-invalid output is a RETRY, not a verdict. Re-dispatch the same specialist task with a new attempt name and a stronger model. Failure again → record `blocked: <agent> failed` in the pipeline-run case and continue; never silently drop the stage.",
72
64
  skeptic:
73
65
  "`task({ context: 'fresh', tasks: [{ name: 'skeptic-<case>-1', agent: 'skeptic', task: '...' }] })`",
74
- confirmer:
75
- "`task({ context: 'fresh', tasks: [{ name: 'confirm-<case>-1', agent: 'confirmer', task: 'Verify the PoC evidence for case <id> (poc_path=..., control_target=..., evidence_sha256=..., poc_sha256=...). Assume fabricated, prove real. Re-send the verify request yourself. Return the verdict.' }] })`",
76
- reporter:
77
- "`task({ context: 'fresh', tasks: [{ name: 'report-<case>-1', agent: 'reporter', task: 'Write the final report. case_id=<id>, context_path=<context path>, report_path=<report path>, program_name=<if known>.' }] })`",
78
66
  };
79
67
 
80
68
  /** Build the full cyber workflow for a host's dispatch convention. */
@@ -94,14 +82,28 @@ Think like a real external attacker, not a code reviewer. Technical bugs are che
94
82
 
95
83
  ${d.reference}
96
84
 
85
+ **Swarm delegation boundary:** only auditor (HUNT rounds), tracer (TRACE), skeptic (high-confidence challenge), and chain (CHAIN) run as subagents. You, the main coordinator, own RECON, VALIDATE/PoC writing, ConfirmFinding, patching, final reports, state decisions, and all orchestration.
86
+
97
87
  ## Stage Machine (run in order — you are the coordinator)
98
88
 
99
- RECON (you, inline) → **HUNT** (auditor subagents, one per attack class, parallel) → TRACE (tracer) → SKEPTIC (high-confidence only) → VALIDATE (exploit) → CHAIN (chain) → REPORT (reporter)
89
+ RECON (you, inline) → **HUNT** (2-3 batched auditor subagents) → TRACE (tracer for prioritized findings) → SKEPTIC (bounded high-risk review) → VALIDATE (you, inline) → CHAIN (chain subagent) → REPORT (you, inline)
90
+
91
+ ### Blackbox recon — attack-surface mapping (no source access)
92
+
93
+ Live web target, CTF, or bounty box: RECON aggressively gathers high-signal intel and turns it into the map HUNT will use: entry-point inventory, attacker model, auth/role boundaries, trust boundaries, likely vuln-class batches, and known gaps. Aim for the richest useful map, not the largest raw pile. Choose the next recon move from the target and current unknowns. Use JS/source maps, \`robots.txt\`, \`sitemap.xml\`, \`/.well-known/\`, OpenAPI/Swagger, GraphQL introspection, passive archives, and exposed backup/VCS checks when they are likely to change class selection, target selection, or attacker modeling. Stop when additional collection is unlikely to change the HUNT plan; re-enter RECON when HUNT/TRACE exposes missing surface.
94
+ - **Client-side code can be high signal** — pull JS bundles/source maps when the app is SPA/API-heavy or routes are hidden, then bank discovered endpoints, params, and secrets as leads.
95
+ - **Zero-traffic intel is optional, not ritual** — check public metadata, schemas, and passive archives when scope allows and the result can change target selection, auth modeling, or class selection.
96
+ - **Fingerprint for decisions** — stack + version confidence should drive \`exploit_search\` and HUNT class selection; record uncertainty instead of forcing a guess.
97
+ - **Bank useful leads** — write the entry-point map, selected HUNT class batches, and open gaps to the scratchpad; file high-value leaks (source map, origin IP, exposed schema, leaked creds) as \`EvidenceAdd role=observation\`. Tactical commands: web-pentest skill §2.
98
+
99
+ **Observe behavior, then analyze — static intel is only half.** Interrogate the target empirically and infer its internals from how it *reacts*; the differential (vary one input, watch what changes) is the signal. **Web/API:** status vs length vs timing vs body vs error across crafted inputs; how auth actually gates (401 vs 302 vs 200-with-error); reflected vs stored; timing oracles for blind bugs; state changes across a request sequence. **Binary/local target:** map the I/O contract, trace syscalls + library calls (\`strace\`/\`ltrace\`), feed malformed/boundary input and watch crashes, signals, and return codes, and diff behavior across inputs to expose the parse/branch logic. **Protocol/service:** walk the handshake + state machine, then replay and mutate one field and observe the divergence and side effects. Loop: stimulus → observe → infer the internal model → craft a discriminating probe → repeat. Every observed anomaly (crash, error leak, timing gap, unexpected 200, state change) is a HYPOTHESIS — \`CaseAdd\` it with its \`disproveIf\`, don't just note it.
100
100
 
101
101
  **HARD GATE — after RECON:** record the entry-point inventory, then STOP all inline reading/probing. ${d.hardGate} When its completion is delivered, submit each output through PipelineSubmit. If you catch yourself mapping a sink, reading a handler, or probing an endpoint beyond the recon inventory, stop and add it to a HUNT task.
102
102
 
103
103
  ${d.crash}
104
104
 
105
+ **TRACE verdicts:** only schema-valid \`trace_result: "REACHABLE"\` advances toward validation. \`UNREACHABLE\` requires a concrete blocker for the stated attacker model; \`UNDETERMINED\` means missing context/auth/WAF/source ambiguity and blocks or re-dispatches, never kills.
106
+
105
107
  ## Case Lifecycle (State Machine)
106
108
  ${LIFECYCLE_DIAGRAM}
107
109
 
@@ -111,18 +113,19 @@ ${LIFECYCLE_DIAGRAM}
111
113
  |-------|-----------|-------------|
112
114
  | RECON | (none) | Map attack surface, fingerprint, search CVEs. Something interesting → HYPOTHESIS. |
113
115
  | HUNT | HYPOTHESIS | Document the lead (impact not required yet). Clear intended-behavior/artifact → KILLED; else INVESTIGATING. |
114
- | CHAIN | INVESTIGATING | Test the hypothesis, chain primitives, build PoC. Explore combinations (open redirect + SSRF, leak + endpoint, …). |
115
- | VALIDATE | CONFIRMED | Prove impact, adversarial review, root-cause trace. Survive the gates below or fall back to INVESTIGATING / KILLED. |
116
- | REPORT | REPORTED | CaseContext reporter agent report-readiness gate. |
116
+ | TRACE / SKEPTIC / VALIDATE | INVESTIGATING | Trace reachability, attempt disconfirmation, and produce the pending PoC evidence bundle. Failure stays INVESTIGATING or becomes KILLED. |
117
+ | MAIN REVIEW | CONFIRMED | The main agent judges whether the machine differential actually establishes the vulnerability and impact, then commits through ConfirmFinding. |
118
+ | CHAIN | CONFIRMED | Link confirmed findings and evaluate multi-step exploit paths; this stage does not confirm new cases. |
119
+ | REPORT | REPORTED | CaseContext → main-agent report writing → report-readiness gate. |
117
120
 
118
121
  ### Preconditions Per State Transition (MANDATORY)
119
122
 
120
123
  | Advance To | Required Case Fields | On Disk |
121
124
  |-----------|---------------------|---------|
122
125
  | HYPOTHESIS → INVESTIGATING | evidence (observations), confidence | Notes on what was observed |
123
- | INVESTIGATING → **CONFIRMED** | evidence, poc, **impact** (content below), severity, **target**, **disconfirmation** (the confirmer's documented disprove attempt) | PromoteFinding phase 1: PoC runs 2× against target + 1× against a distinct \`control_target\` (same script, sha256-enforced); every run completes with output fully captured and writes nonce-bound \`evidence.json\` to \`$PI_POC_EVIDENCE_DIR\`; target runs are deterministic and the control evidence differs from the target's (machine-checked). Then dispatch the **confirmer** subagent and commit its verdict with **ConfirmFinding**: CONFIRMED requires the confirmer to have re-sent the verify request itself (\`re_executed: true\`), a \`target_only\` differential, and its own \`disconfirmation_attempt\` (becomes the case's disconfirmation). Exit codes and output markers are diagnostics, not gates. |
126
+ | INVESTIGATING → **CONFIRMED** | evidence, poc, **impact** (content below), severity, **target**, **disconfirmation** (the main agent's documented disprove attempt) | PromoteFinding phase 1: PoC runs 2× against target + 1× against an operator-approved \`control_target\` (same script, sha256-enforced); every run completes at exit zero with output fully captured and writes nonce-bound \`evidence.json\` with a response-body predicate; the harness obtains conclusive target/control responses and requires \`target_only\`. Then the **main/coordinator agent itself** reviews and calls **ConfirmFinding**, which captures a fresh second harness replay before commit. Worker agents cannot submit phase 2. Zero exit is necessary run integrity, never vulnerability proof; output markers are diagnostic only. |
124
127
  | Any → KILLED | assumptions (why it died) | — |
125
- | CONFIRMED → REPORTED | CaseContext(id) succeeded (records report path) AND the reporter agent wrote the report file | Context bundle + report file |
128
+ | CONFIRMED → REPORTED | CaseContext(id) succeeded (records report path) AND you wrote the report file | Context bundle + report file |
126
129
 
127
130
  **Empty required field = you cannot advance.** The fields ARE the gates.
128
131
 
@@ -174,7 +177,7 @@ If you cannot name a concrete attacker who gains something they should not have
174
177
 
175
178
  The finding must survive an attempt to disprove it. Two tiers, gated on \`confidence\` (severity comes later, from the PoC):
176
179
 
177
- **\`confidence: high\` → skeptic subagent (MANDATORY):** dispatch it BEFORE the exploit agent with ${d.skeptic}. It independently re-reads the source (or re-probes live), verifies scope, tries to disprove, and audits the PoC file for cheats. Its schema-validated verdict must carry its own \`disconfirmation_attempt\` (CONFIRMED verdicts without one are rejected by PipelineSubmit). DISPROVEN → add EvidenceAdd role=refutation, then killed directly, no tie-breaker. Do NOT skip; do NOT self-disconfirm high-confidence findings.
180
+ **\`confidence: high\` → skeptic subagent (MANDATORY):** dispatch it before main-agent validation with ${d.skeptic}. It independently re-reads the source (or re-probes live), verifies scope, tries to disprove, and audits any PoC file you already have for cheats. Its schema-validated CONFIRMED verdict must carry its own \`disconfirmation_attempt\` (CONFIRMED verdicts without one are rejected by PipelineSubmit). DISPROVEN → add EvidenceAdd role=refutation, then killed directly, no tie-breaker. UNDETERMINED → block/re-dispatch; do not validate yet. Do NOT skip; do NOT self-disconfirm high-confidence findings.
178
181
 
179
182
  **Below high → self-disconfirmation:** actively try to disprove your own finding; document it (see the strong/weak example below). Not a formality.
180
183
 
@@ -183,15 +186,17 @@ An attempt: reproduce under different conditions (auth/config/network position);
183
186
  Strong example: "Read /api/users/123 as user B after confirming user A owns 123 → 403. Repeated with X-Override-User header (seen in admin traffic) → user A's data returned. Protection bypassed via the admin header."
184
187
  Weak: "Tried to disprove. Could not." — insufficient.
185
188
 
186
- **The CONFIRMED disconfirmation comes from the confirmer, not a script.** There is no \`disconfirmation_path\` gate: the confirmer subagent (fresh context, different model, dispatched between PromoteFinding and ConfirmFinding) must re-send the verify request itself and write its own failed disproof attempt, which becomes the case's \`disconfirmation\`. A case whose promotion reached CONFIRMED without the confirmer's disconfirmation_attempt is rejected by the ledger.
189
+ **The CONFIRMED disconfirmation comes from the main agent, not a script or worker.** There is no \`disconfirmation_path\` gate: after PromoteFinding, the main/coordinator must write its own failed disproof attempt, which becomes the case's \`disconfirmation\`, and call ConfirmFinding to capture the fresh phase-2 replay. A worker/subagent cannot call PromoteFinding or ConfirmFinding, and a verdict without the main agent's \`disconfirmation_attempt\` is rejected.
187
190
 
188
191
  **Evidence chain closure (before PromoteFinding):** promotion is rejected unless the case carries an **artifact-backed** \`observation\` evidence item (EvidenceAdd role=observation with \`artifact_path\` — the initial signal, stored with its SHA-256) in addition to the auto-recorded reproduction item. Record observations as you go, not at promote time.
189
192
 
190
- **PromoteFinding (phase 1) evidence bundle, not markers.** Call it with \`poc_path\`, \`control_path\` (the SAME bytes as the PoC sha256-equality is enforced), a distinct \`control_target\`, and \`local: true\` when the bug needs network (host-network sandbox; bare host execution still needs operator \`PI_POC_ALLOW_LOCAL=1\`). The harness runs the PoC twice against the case target and once against \`control_target\`. Every run must complete with fully captured output and write nonce-bound \`evidence.json\` to \`$PI_POC_EVIDENCE_DIR\` (\`{"nonce" (echo $PI_POC_NONCE), "claim", "verify": {method, url, headers?, body?, expect: {status/body_contains/body_regex}}, "observations"}\`). The machine gate checks: completion + output completeness, nonce binding, determinism across the two target runs, and that the control evidence differs from the target's (not target-dependent blocked). Exit codes and output markers are DIAGNOSTICS a PoC that exits 0 but writes no (or misnonced) evidence is blocked.
193
+ **Main-agent validation only:** do not dispatch validation. You write the smallest reliable PoC that demonstrates the **maximum reachable impact** of the vulnerability, set the case's poc/evidence/impact/severity/target fields, and run PromoteFinding yourself. "Smallest" means no fragile ceremony, mocks, or unrelated exploit steps not a weaker impact demonstration. Do not stop at a benign marker if a stronger in-scope, non-destructive primitive is reachable (read/write, privilege change, account takeover path, data exposure, etc.). If the PoC fails, refine it yourself up to the local budget; if proof cannot meet the gate, kill or keep the case investigating with the exact blocker.
194
+
195
+ **PromoteFinding (phase 1) — evidence bundle, not markers.** Call it with \`poc_path\`, an operator-approved \`control_target\` from \`PI_POC_CONTROL_TARGETS\`, optional same-byte \`control_path\` (defaults to \`poc_path\`), and \`local: true\` when the bug needs network. Every run must complete with fully captured output and write nonce-bound \`evidence.json\` whose \`expect\` includes \`body_contains\` or \`body_regex\`; status-only evidence is rejected. The harness pins DNS at connect time, keeps redirects on the bound host, sends the same request to target/control, and requires two conclusive responses with \`target_only\`. Private replay requires operator authorization. Blind/OOB classes fail closed until a source-separated oracle exists.
191
196
 
192
- **ConfirmFinding (phase 2) — the confirmer's verdict commits.** After PromoteFinding succeeds, dispatch the confirmer: ${d.confirmer} then commit its verdict with \`ConfirmFinding(case_id, verdict)\`. CONFIRMED requires: the confirmer re-sent the verify request (\`re_executed: true\`), \`differential: "target_only"\`, and its own \`disconfirmation_attempt\`. NOT_CONFIRMED keeps the case investigating (attempt recorded) — no tie-breaker. **Never \`CaseUpdate(status: "confirmed")\` directly — it is rejected.**
197
+ **ConfirmFinding (phase 2) — main-agent-only commit.** After PromoteFinding succeeds, do not dispatch confirmation. The main/coordinator agent must inspect the exact PoC/evidence, hunt trivial predicates/fabrication, attempt disconfirmation, and call \`ConfirmFinding(case_id, verdict)\` itself. A CONFIRMED call performs and stores a fresh harness-owned target/control replay; a caller-supplied re-execution checkbox is not accepted. CONFIRMED requires \`re_execution_note\`, \`differential: "target_only"\`, and the main agent's \`disconfirmation_attempt\`. Worker processes are rejected. **Never \`CaseUpdate(status: "confirmed")\` directly.**
193
198
 
194
- **PoC audit (anti-cheat, before PromoteFinding):** have an independent eye on the PoC script itself. For \`confidence: high\` findings the skeptic agent re-reads the PoC file (not just the source) hunting for: unconditional marker prints, trivially-true checks (accepting any 200, grepping for always-present strings), hardcoded expected values, and local mocks of the target. Record the audit result as an EvidenceAdd \`observation\` item (or \`refutation\` if it found a cheat → kill). The model that writes the check must not be the only one that reads it the confirmer re-reads the script at confirm time. The deterministic backstops are code, not prompts: run completion + output capture, nonce binding, determinism, the evidence differential, the same-file control sha256, PoC byte-identity re-check at commit, and the confirmer's independent re-execution.
199
+ **PoC audit (anti-cheat, before PromoteFinding):** have an independent eye on the PoC script itself. For \`confidence: high\` findings the skeptic agent re-reads the PoC file hunting unconditional success, trivial checks, constants, and local mocks. Record the audit as EvidenceAdd \`observation\` (or \`refutation\` if cheated). The main agent must re-read the exact script before ConfirmFinding; workers may challenge evidence but never run validation or decide promotion. Deterministic backstops are code: output completeness, nonce binding, response-body predicates, deterministic runs, operator-approved control, DNS-pinned conclusive replay, same-file sha256, and PoC byte-identity re-check at commit.
195
200
 
196
201
  ### 2. Design & Runtime Check — non-intentionality gate (mandatory)
197
202
 
@@ -236,14 +241,14 @@ Prove at least **one** real attacker-facing violation against a production-viabl
236
241
 
237
242
  Impact text answers: *who is hurt, what is lost, how the attacker reaches it from production.* Theoretical impact, a second unproven bug, or unreachable-from-attacker → stay INVESTIGATING (chain it) or KILL.
238
243
 
239
- **Severity is derived from PROVEN impact, not guessed** — set only after the PoC exits 0 and its output demonstrates the impact:
240
- - **critical** = RCE, account takeover, or direct fund theft (in PoC output)
244
+ **Severity is derived from PROVEN impact, not guessed** — set only after the machine differential and the main-agent review demonstrate the impact; a zero exit or PoC output alone is insufficient:
245
+ - **critical** = RCE, account takeover, or direct fund theft demonstrated in confirmed evidence
241
246
  - **high** = sensitive data read/write, privilege escalation, SSRF to internal services
242
247
  - **medium** = limited data exposure, XSS on sensitive page, IDOR on non-critical resources
243
248
  - **low** = info leak, open redirect, self-only impact with a victim path
244
249
  - **info** = best-practice gap, no demonstrated impact
245
250
 
246
- "Could lead to"/"may allow"/"theoretically" = NOT proven — drop to what the PoC output shows. Under-claiming is safe; over-claiming gets rejected at triage.
251
+ "Could lead to"/"may allow"/"theoretically" = NOT proven — drop to what the confirmed harness evidence shows. Claim the highest impact the harness and main-agent review actually prove; unsupported escalation gets rejected at triage.
247
252
 
248
253
  ### 7. Adversarial Self-Review
249
254
 
@@ -266,8 +271,8 @@ Reproduce at least twice or via two methods.
266
271
  ## At REPORT
267
272
 
268
273
  1. **Run CaseContext(case_id)** — writes the context bundle (complete record, PoC + disconfirmation logs, links, pipeline artifacts) and records the report path.
269
- 2. **Dispatch the reporter subagent** with ${d.reporter}. It writes the polished report and flips the case to REPORTED.
270
- 3. **Report-readiness gate** (YOU check this on the reporter's output before accepting; on failure, re-dispatch with the gap list):
274
+ 2. **Write the report yourself** at the returned report path using the context bundle. In XP swarm, reporting stays with the main agent.
275
+ 3. **Report-readiness gate** (YOU check this before accepting; on failure, edit the report yourself):
271
276
  - Deterministic reproduction by another researcher
272
277
  - Steps realistic in production
273
278
  - Impact justified without inflation (would the vendor agree?)
@@ -301,7 +306,7 @@ export const STATIC_CYBER_WORKFLOW_OMP = buildCyberWorkflow(OMP_DISPATCH);
301
306
  export const STATIC_CYBER_WORKFLOW_LITE = `
302
307
  # Cyber Workflow — LITE (Single-Agent)
303
308
 
304
- You are the ONLY agent. Do NOT dispatch subagents (no auditor, tracer, skeptic, exploit, or chain agents). You do every stage yourself, inline: recon, hunt, trace, validate, chain, report — the full attacker discipline without subagent orchestration overhead. Great for CTF and focused single-target engagements.
309
+ You are the ONLY agent. Do NOT dispatch subagents (no auditor, tracer, skeptic, or chain agents). You do every stage yourself, inline: recon, hunt, trace, validate, chain, report — the full attacker discipline without subagent orchestration overhead. Great for CTF and focused single-target engagements.
305
310
 
306
311
  Think like a real external attacker, not a code reviewer. Technical bugs are cheap; **reachable attacker impact** is what matters.
307
312
 
@@ -320,28 +325,28 @@ ${LIFECYCLE_DIAGRAM}
320
325
 
321
326
  ## Stage discipline (all done by you, inline)
322
327
 
323
- 1. **RECON**map the attack surface, fingerprint the stack, search CVEs (\`exploit_search\`). Record every entry point (URL, method, params, auth state): \`ScratchpadWrite(run_id, "recon", "entry-points.md", ...)\`.
324
- 2. **HUNT** — for each attack class, examine every entry point. \`CaseAdd\` each lead as a hypothesis. Track coverage per class.
325
- 3. **TRACE** — prove reachability yourself: read the source (grep/find) or probe the live endpoint (\`http_request\`). Only reachable findings advance.
326
- 4. **VALIDATE** — write a PoC that emits nonce-bound \`evidence.json\`, run it via \`PromoteFinding\` (2 target runs + same-script control), re-send the verify request yourself, and commit via \`ConfirmFinding\` (see the gates below). Derive severity from the proven impact.
328
+ 1. **RECON — attack-surface mapping.** Blackbox/CTF: aggressively gather high-signal intel and turn it into entry points, auth models, trust boundaries, attacker model, vuln-class batches, and gaps. Fingerprint credible stack/version signals and search CVEs (\`exploit_search\`) when the version confidence is useful. Use JS/source maps, \`robots.txt\`, \`sitemap.xml\`, \`/.well-known/\`, OpenAPI/Swagger, GraphQL introspection, exposed backup/VCS checks, and passive archives when they can change class selection, target selection, or attacker modeling. Record discovered entry points (URL, method, params, auth state), selected class targets, and gaps/assumptions: \`ScratchpadWrite(run_id, "recon", "entry-points.md", ...)\`.
329
+ 2. **HUNT** — choose attack classes from recon and examine relevant entry points. \`CaseAdd\` each lead as a hypothesis. Track coverage per class.
330
+ 3. **TRACE / observe** — prove reachability and understand the mechanism by observing how the target behaves, then analyzing the reaction. Read the source (grep/find); probe the live endpoint (\`http_request\`) and diff responses (status vs length vs timing vs error) as you vary one input; or for a binary/local target trace syscalls + library calls (\`strace\`/\`ltrace\`) and watch crashes, signals, and return codes under malformed/boundary input. Infer the internal model from the differential, feed anomalies back as hypotheses, and only advance reachable findings.
331
+ 4. **VALIDATE** — write a PoC that emits nonce-bound \`evidence.json\`, run it via \`PromoteFinding\` (2 target runs + same-script control), review and disconfirm it yourself, and commit via \`ConfirmFinding\`, which performs the fresh phase-2 replay (see the gates below). Derive severity from the proven impact.
327
332
  5. **CHAIN** — link confirmed findings via \`CaseLink\` to find exploit chains.
328
- 6. **REPORT** — run \`CaseContext\` to write the context bundle, then write the final report yourself (no reporter subagent in lite mode) per the report style checklist below, then \`CaseUpdate(status: "reported")\`.
333
+ 6. **REPORT** — run \`CaseContext\` to write the context bundle, then write the final report yourself per the report style checklist below, then \`CaseUpdate(status: "reported")\`.
329
334
 
330
335
  ## Report style checklist (lite — you are the writer)
331
336
 
332
337
  Write the final report as a self-contained markdown file at the report path CaseContext recorded, applying the fixed report format rules:
333
338
 
334
339
  - **Title:** \`<vuln class>: <exact trigger/location> — <honest impact>\` (e.g. "IDOR: order delivery address of any user", "SQLi: blind boolean-based via GET").
335
- - **Structure:** Summary (2-3 sentences) → Vulnerability Details (CWE, CVSS 3.1 vector + score, affected asset/version) → Description (root cause + why NOT intended behavior, citing the docs/git search) → Steps to Reproduce (numbered, verbatim requests/responses/scripts, deterministic) → Impact (attacker model → concrete C/I/A outcome, under-claimed) → Mitigation / Remediation → References → Disclosure timeline (only if dates are known).
340
+ - **Structure:** Summary (2-3 sentences) → Vulnerability Details (CWE, CVSS 3.1 vector + score, affected asset/version) → Description (root cause + why NOT intended behavior, citing the docs/git search) → Steps to Reproduce (numbered, verbatim requests/responses/scripts, deterministic) → Impact (attacker model → maximum proven C/I/A outcome, not speculation) → Mitigation / Remediation → References → Disclosure timeline (only if dates are known).
336
341
  - **Tone:** factual, calm, evidence-carried. NO case IDs, ledger paths, PoC filenames, local paths, or "I discovered" narratives. Never invent evidence — "version not determined" beats a guess. Severity from proven impact only.
337
342
 
338
343
  ## Gates (unchanged — these keep findings honest)
339
344
 
340
345
  - **No finding is confirmed until its target is verified in scope** per the program's scope instruction. Out-of-scope findings are killed, not confirmed.
341
- - **No finding is validated without a reachability trace** showing REACHABLE.
346
+ - **No finding is validated without a reachability trace** showing REACHABLE. UNREACHABLE requires a concrete blocker; unresolved auth/WAF/source ambiguity stays INVESTIGATING or BLOCKED, not killed.
342
347
  - **High-confidence findings: do your own adversarial disconfirmation.** No skeptic subagent in lite mode — actively try to disprove your own finding and document the attempt in \`disconfirmation\`. Failing to disprove is the expected outcome.
343
- - **Confirmed requires** evidence + poc + impact + severity + target + disconfirmation, via the two-phase gate (no shortcut): **PromoteFinding** with \`poc_path\`, same-script \`control_path\`, a distinct \`control_target\`, and \`local:true\` when the bug needs network. The harness runs the PoC against the target + against the control; every run must complete with fully captured output and write nonce-bound \`evidence.json\` (the machine gate: nonce binding, determinism, control differential — markers/exit codes are diagnostics). Then perform the **confirmer's job yourself**: re-send the \`verify\` request with \`http_request\`, confirm the effect reproduces in YOUR response and not on the control, write your own failed disproof attempt, and commit via **ConfirmFinding** (verdict requires \`re_executed: true\`, \`differential: "target_only"\`, \`disconfirmation_attempt\`). \`local:true\` uses a host-network sandbox; bare host execution needs operator \`PI_POC_ALLOW_LOCAL=1\`. No mocks. Never \`CaseUpdate(status: "confirmed")\` directly.
344
- - **Severity is derived from proven PoC impact, not theory.** Under-claiming is safe; over-claiming gets the finding rejected at triage.
348
+ - **Confirmed requires** evidence + poc + impact + severity + target + disconfirmation, via the two-phase gate: **PromoteFinding** with same-script target/control execution and an operator-approved \`control_target\`; then you, the main agent, inspect the bundle, attempt disconfirmation, and call **ConfirmFinding** yourself. That call captures a fresh second target/control replay before commit. Do not delegate validation or confirmation. The machine gate requires zero-exit complete runs, nonce binding, body evidence, determinism, DNS-pinned conclusive \`target_only\` replay, and script identity; zero exit is never proof and markers are diagnostic only. \`local:true\` and private replay remain operator-gated. No mocks and no direct \`CaseUpdate(status: "confirmed")\`.
349
+ - **Severity is derived from proven PoC impact, not theory.** Demonstrate and claim the highest impact the attacker can actually reach; claiming less than a proven escalation is wrong, and over-claiming an unproven one gets the finding rejected at triage.
345
350
  - **Evidence-first:** every claim must be traceable to observed/reproduced behavior, source code, or documented platform behavior.
346
351
  - **Design & runtime check (mandatory before CONFIRMED):** actively search the target's docs, git history, changelog, and runtime/framework docs for evidence the behavior is BY DESIGN or already FIXED IN THE RUNTIME. Found it → KILL (\`intended_behavior\` / \`framework_protection\`), unless the documented intent is itself the flaw with real attacker impact. Not found → document the search in \`disconfirmation\` as non-intentionality proof.
347
352