@xaccefy/pi-casefile 0.8.2 → 0.9.0

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/workflow.ts CHANGED
@@ -25,28 +25,82 @@ RECON -> HYPOTHESIS --+
25
25
  |
26
26
  +--> KILLED (insufficient impact, duplicate, etc.)
27
27
  \`\`\``;
28
- export const STATIC_CYBER_WORKFLOW = `
28
+
29
+ /**
30
+ * Subagent-dispatch conventions per host. Pi (pi-subagents extension) dispatches
31
+ * through \`subagent({ workflowScript: runs.run(...) })\`; OMP (fork, @oh-my-pi)
32
+ * dispatches through its native \`task\` tool with a tasks array. The workflow
33
+ * body is identical — only the launch mechanics differ.
34
+ */
35
+ type DispatchSpec = {
36
+ /** Tool-reference paragraph. */
37
+ reference: string;
38
+ /** HARD GATE launch sentence (after "record the entry-point inventory, then STOP..."). */
39
+ hardGate: string;
40
+ /** Crash-handling paragraph. */
41
+ crash: string;
42
+ /** Skeptic dispatch snippet (follows "dispatch it BEFORE the exploit agent with "). */
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
+ };
49
+
50
+ const PI_DISPATCH: DispatchSpec = {
51
+ 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.",
53
+ hardGate:
54
+ "Your next tool call MUST launch one async workflowScript whose `runs.all([...])` dispatches HUNT auditors.",
55
+ 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.",
57
+ skeptic:
58
+ "`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
+ };
64
+
65
+ const OMP_DISPATCH: DispatchSpec = {
66
+ 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.",
68
+ 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.",
70
+ 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.",
72
+ skeptic:
73
+ "`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
+ };
79
+
80
+ /** Build the full cyber workflow for a host's dispatch convention. */
81
+ function buildCyberWorkflow(d: DispatchSpec): string {
82
+ return `
29
83
  # Cyber Workflow (Attacker-Oriented)
30
84
 
31
85
  Think like a real external attacker, not a code reviewer. Technical bugs are cheap; **reachable attacker impact** is what matters. Every lead starts HYPOTHESIS; nothing reaches CONFIRMED without a proven attacker path and demonstrated impact against a real production target or faithful replica.
32
86
 
33
87
  ## Tool Reference
34
88
 
35
- **Casefile (state tracking):** CaseAdd, CaseUpdate, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, PromoteFinding, PipelineSubmit
89
+ **Casefile (state tracking):** CaseAdd, CaseUpdate, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, PromoteFinding, ConfirmFinding, PipelineSubmit
36
90
 
37
91
  **Scratchpad (pipeline artifacts):** ScratchpadInit, ScratchpadResume, ScratchpadCheckpoint, ScratchpadWrite, ScratchpadRead, ScratchpadPhaseDone, ScratchpadClear
38
92
 
39
93
  **Web lookup (research):** web_search, web_fetch, exploit_search, context7, deepwiki, http_request
40
94
 
41
- **Subagent dispatch:** \`subagent({agent: "auditor"|"tracer"|"skeptic"|"exploit"|"chain"|"reporter", task: "..."})\` — dispatch specialists; do NOT do the specialist work yourself.
95
+ ${d.reference}
42
96
 
43
97
  ## Stage Machine (run in order — you are the coordinator)
44
98
 
45
99
  RECON (you, inline) → **HUNT** (auditor subagents, one per attack class, parallel) → TRACE (tracer) → SKEPTIC (high-confidence only) → VALIDATE (exploit) → CHAIN (chain) → REPORT (reporter)
46
100
 
47
- **HARD GATE — after RECON:** record the entry-point inventory, then STOP all inline reading/probing. Your very next tool call MUST be \`subagent({ tasks: [...] })\` dispatching HUNT auditors. If you catch yourself mapping a sink, reading a handler, or probing an endpoint beyond the recon inventory that is HUNT work; stop, note it as a hunt task, and dispatch. Recon that bleeds into hunting is a pipeline violation, not progress.
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.
48
102
 
49
- **Subagent crash handling:** a subagent that dies (SIGABRT, OOM, timeout) is a RETRY, not a verdict — re-dispatch the same task once with a stronger model (\`subagent({agent, model, task})\`); repetition-loop runs are a known failure mode on cheap models. Crash again → record \`blocked: <agent> crashed\` in the pipeline-run case and continue; never silently drop the stage.
103
+ ${d.crash}
50
104
 
51
105
  ## Case Lifecycle (State Machine)
52
106
  ${LIFECYCLE_DIAGRAM}
@@ -66,7 +120,7 @@ ${LIFECYCLE_DIAGRAM}
66
120
  | Advance To | Required Case Fields | On Disk |
67
121
  |-----------|---------------------|---------|
68
122
  | HYPOTHESIS → INVESTIGATING | evidence (observations), confidence | Notes on what was observed |
69
- | INVESTIGATING → **CONFIRMED** | evidence, poc, **impact** (content below), severity, **target**, **disconfirmation** (your documented disprove attempt) | PoC script, exit 0, **verification_marker in output** (proves the exploit ran, not just the script). Optional disconfirmation script exit non-0. |
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. |
70
124
  | Any → KILLED | assumptions (why it died) | — |
71
125
  | CONFIRMED → REPORTED | CaseContext(id) succeeded (records report path) AND the reporter agent wrote the report file | Context bundle + report file |
72
126
 
@@ -120,22 +174,24 @@ If you cannot name a concrete attacker who gains something they should not have
120
174
 
121
175
  The finding must survive an attempt to disprove it. Two tiers, gated on \`confidence\` (severity comes later, from the PoC):
122
176
 
123
- **\`confidence: high\` → skeptic subagent (MANDATORY):** dispatch \`subagent({agent: "skeptic", task: "..."})\` BEFORE the exploit agent. It independently re-reads the source (or re-probes live), verifies scope, and tries to disprove. Its \`disconfirmation_attempt\` becomes the case's \`disconfirmation\` stronger than self-disconfirmation. DISPROVEN → killed directly, no tie-breaker. Do NOT skip; do NOT self-disconfirm high-confidence findings.
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.
124
178
 
125
- **Below high → self-disconfirmation:** actively try to disprove your own finding; document it. Not a formality.
179
+ **Below high → self-disconfirmation:** actively try to disprove your own finding; document it (see the strong/weak example below). Not a formality.
126
180
 
127
181
  An attempt: reproduce under different conditions (auth/config/network position); test the behavior against docs/baseline endpoints; trigger protections (WAF/CSP/CSRF/rate limits); try to trigger the same behavior without your attacker-controlled input. Document in \`disconfirmation\`: what you tried, how (conditions/inputs/target), result (failing to disprove is the expected outcome), why the attempt was valid.
128
182
 
129
183
  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."
130
184
  Weak: "Tried to disprove. Could not." — insufficient.
131
185
 
132
- If the disconfirmation script (\`disconfirmation_path\`) exits 0, promotion is blocked. The disconfirmation script is REQUIRED for **every** promotion — the prose \`disconfirmation\` field alone cannot carry the disprove-attempt at any severity (a case filed low/medium must not skip the run and be re-raised afterwards). If you cannot write a meaningful disconfirmation script, you don't understand the finding well enough to promote it. A disconfirmation (or control) script that CRASHES killed, timed out, interpreter missing — is blocked too: the harness detects the missing completion marker, and a crash is neither a survived disproof nor a clean control verdict.
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.
133
187
 
134
188
  **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.
135
189
 
136
- **Control-target check (anti-cheat, REQUIRED for EVERY promotion sandboxed and live alike):** write \`control_path\` the SAME script as the PoC (the harness enforces sha256 equality; a separately written control file is rejected — the same actor writes both files, so only one parameterized script counts). The script reads the target from the \`PI_POC_TARGET\` env var and branches on \`PI_POC_MODE\` (\`poc\` | \`control\` | \`disconfirmation\`) — the control run is literally the same script in control mode against a control lacking the vulnerability (patched replica, second account, baseline endpoint, WAF-blocked path). The harness blocks promotion if the verification_marker appears in the control output (checked on the UNTRUNCATED output a script printing its marker past the 4000-char display window is caught). That is what proves the marker is target-dependent, not an unconditional print. Also pass \`control_liveness_marker\`: a unique string the control 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. If the PoC cannot be pointed at a control (no replica exists), say so in \`disconfirmation\` and downgrade confidence accordingly — do not skip the check. **Local/live findings:** \`local:true\` runs in the Docker sandbox with \`--network host\` (still read-only FS, dropped capabilities, unprivileged user). True host execution is NOT agent-selectable: it requires the operator to set \`PI_POC_ALLOW_LOCAL=1\`, and is only a fallback when Docker is unavailable.
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.
137
191
 
138
- **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 deterministic backstops are code, not prompts: the control run (mandatory, marker-absence + liveness-presence checks) and, for high/critical, the executed disconfirmation run.
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.**
193
+
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.
139
195
 
140
196
  ### 2. Design & Runtime Check — non-intentionality gate (mandatory)
141
197
 
@@ -210,7 +266,7 @@ Reproduce at least twice or via two methods.
210
266
  ## At REPORT
211
267
 
212
268
  1. **Run CaseContext(case_id)** — writes the context bundle (complete record, PoC + disconfirmation logs, links, pipeline artifacts) and records the report path.
213
- 2. **Dispatch the reporter subagent**: \`subagent({agent: "reporter", task: "Write the final report for case <id>. case_id=<id>, context_path=<path from CaseContext>, report_path=<path from CaseContext>, program_name=<program if known>. Apply the fixed report format rules in your prompt (title convention, body template, tone rules). Output: the report file written to report_path + CaseUpdate(status: 'reported')."})\`. It writes the polished report and flips the case to REPORTED.
269
+ 2. **Dispatch the reporter subagent** with ${d.reporter}. It writes the polished report and flips the case to REPORTED.
214
270
  3. **Report-readiness gate** (YOU check this on the reporter's output before accepting; on failure, re-dispatch with the gap list):
215
271
  - Deterministic reproduction by another researcher
216
272
  - Steps realistic in production
@@ -227,6 +283,13 @@ The ledger enforces a machine floor on the report file before accepting \`report
227
283
 
228
284
  When a case is definitively dead (not "I don't know yet"), record the reason: ${KILL_REASONS_TEXT} (true bug, no realistic attacker value). Documenting kills prevents re-opening dead ends. Cases with unresolved unknowns stay INVESTIGATING, not killed.
229
285
  `.trim();
286
+ }
287
+
288
+ /** Cyber workflow for Pi Agent (pi-subagents dispatch). */
289
+ export const STATIC_CYBER_WORKFLOW = buildCyberWorkflow(PI_DISPATCH);
290
+
291
+ /** Cyber workflow for OMP (fork of Pi; native `task` dispatch). */
292
+ export const STATIC_CYBER_WORKFLOW_OMP = buildCyberWorkflow(OMP_DISPATCH);
230
293
 
231
294
  /**
232
295
  * Cyber workflow for XP LITE mode — single-agent, no subagent dispatch.
@@ -244,13 +307,13 @@ Think like a real external attacker, not a code reviewer. Technical bugs are che
244
307
 
245
308
  ## Tool Reference
246
309
 
247
- **Casefile (state tracking):** CaseAdd, CaseUpdate, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, PromoteFinding, PipelineSubmit
310
+ **Casefile (state tracking):** CaseAdd, CaseUpdate, CaseGet, CaseList, CaseSearch, CaseLink, CaseUnlink, CaseContext, PromoteFinding, ConfirmFinding, PipelineSubmit
248
311
 
249
312
  **Scratchpad (pipeline artifacts):** ScratchpadInit, ScratchpadResume, ScratchpadCheckpoint, ScratchpadWrite, ScratchpadRead, ScratchpadPhaseDone, ScratchpadClear
250
313
 
251
314
  **Web lookup (research):** web_search, web_fetch, exploit_search, context7, deepwiki, http_request
252
315
 
253
- **No subagent tool.** In lite mode you do not call \`subagent\`. All specialist work is yours.
316
+ **No subagent/task tool.** In lite mode you do not dispatch subagents (pi's \`subagent\` or OMP's \`task\`). All specialist work is yours.
254
317
 
255
318
  ## Case Lifecycle (State Machine)
256
319
  ${LIFECYCLE_DIAGRAM}
@@ -260,7 +323,7 @@ ${LIFECYCLE_DIAGRAM}
260
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", ...)\`.
261
324
  2. **HUNT** — for each attack class, examine every entry point. \`CaseAdd\` each lead as a hypothesis. Track coverage per class.
262
325
  3. **TRACE** — prove reachability yourself: read the source (grep/find) or probe the live endpoint (\`http_request\`). Only reachable findings advance.
263
- 4. **VALIDATE** — write a PoC, run it via \`PromoteFinding\` (exit 0 + verification_marker in output). Derive severity from the proven impact.
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.
264
327
  5. **CHAIN** — link confirmed findings via \`CaseLink\` to find exploit chains.
265
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")\`.
266
329
 
@@ -277,7 +340,7 @@ Write the final report as a self-contained markdown file at the report path Case
277
340
  - **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.
278
341
  - **No finding is validated without a reachability trace** showing REACHABLE.
279
342
  - **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.
280
- - **Confirmed requires** evidence + poc + impact + severity + target + disconfirmation, and a PoC that exited 0 **with the verification_marker in the output**. **Every promotion also requires control_path + control_liveness_marker**: the same PoC run against a control lacking the vuln must NOT print the marker AND must print the liveness marker (harness-side checks) this is what stops unconditional-marker, mock-target, and dead-control cheats. \`local:true\` uses a host-network sandbox; host execution needs the operator's \`PI_POC_ALLOW_LOCAL=1\`. No mocks for the exploitation step.
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.
281
344
  - **Severity is derived from proven PoC impact, not theory.** Under-claiming is safe; over-claiming gets the finding rejected at triage.
282
345
  - **Evidence-first:** every claim must be traceable to observed/reproduced behavior, source code, or documented platform behavior.
283
346
  - **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.