@xaccefy/pi-casefile 0.2.3 → 0.2.5
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 +54 -35
- package/package.json +1 -1
- package/src/index.ts +81 -2
package/README.md
CHANGED
|
@@ -1,51 +1,70 @@
|
|
|
1
1
|
# pi-casefile
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Local-first security case ledger for Pi Agent. Tracks hypotheses → confirmed findings with a hard PoC gate, SQLite storage, and an isolated PoC runner.
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
- **State Management**: Enforces strict lifecycle transitions across states (`hypothesis`, `investigating`, `confirmed`, `blocked`, `killed`, `reported`).
|
|
8
|
-
- **PoC Runner**: Validates vulnerability reachability in an isolated Docker sandbox with `--network none` or locally on the host.
|
|
9
|
-
- **SQL Backend**: Syncs case information to SQLite via Node's `node:sqlite` or Bun's `bun:sqlite` compat layers.
|
|
10
|
-
- **Reporting**: Automatically compiles markdown vulnerability reports for confirmed or reported findings.
|
|
11
|
-
|
|
12
|
-
## Installation
|
|
5
|
+
## Install
|
|
13
6
|
|
|
14
7
|
```bash
|
|
15
|
-
pi install npm
|
|
8
|
+
pi install npm:@xaccefy/pi-casefile
|
|
16
9
|
```
|
|
17
10
|
|
|
18
|
-
|
|
11
|
+
Or via the XPI umbrella package: `pi install npm:@xaccefy/pi-xpi`
|
|
12
|
+
|
|
13
|
+
## XP mode (default OFF)
|
|
14
|
+
|
|
15
|
+
The cyber-workflow context injection is **quiet by default** so normal dev work is not flooded with security process text.
|
|
16
|
+
|
|
17
|
+
| Control | Effect |
|
|
18
|
+
|---------|--------|
|
|
19
|
+
| `/xp` | Toggle ON/OFF |
|
|
20
|
+
| `/xp on` / `/xp off` | Set explicitly |
|
|
21
|
+
| `PI_XP_MODE=on` | Force ON for this process (overrides file) |
|
|
22
|
+
| `PI_XP_MODE=off` | Force OFF |
|
|
23
|
+
|
|
24
|
+
When **ON**, every prompt injects the attacker-oriented cyber workflow plus any active (non-killed/non-reported) cases. When **OFF**, nothing is injected; tools remain available.
|
|
25
|
+
|
|
26
|
+
State is persisted next to the ledger as `xp-mode` (e.g. `.pi/xp-mode`).
|
|
27
|
+
|
|
28
|
+
## Environment
|
|
29
|
+
|
|
30
|
+
| Variable | Purpose |
|
|
31
|
+
|----------|---------|
|
|
32
|
+
| `PI_CASEFILE_PATH` | Absolute path to the SQLite ledger file |
|
|
33
|
+
| `CASEFILE_WORKSPACE_ROOT` / `PI_WORKSPACE_ROOT` | Override workspace root used to place `.pi/casefile.db` |
|
|
34
|
+
|
|
35
|
+
Default DB path: `<workspace>/.pi/casefile.db`
|
|
36
|
+
|
|
37
|
+
## State machine
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
hypothesis → investigating → confirmed → reported
|
|
41
|
+
↓ ↓
|
|
42
|
+
blocked killed (terminal)
|
|
43
|
+
```
|
|
19
44
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
45
|
+
- **investigating** requires `evidence` + `confidence`
|
|
46
|
+
- **confirmed** only via `PromoteFinding` (PoC exit 0) — `CaseUpdate(status:"confirmed")` is rejected
|
|
47
|
+
- **reported** requires `CaseReport` first
|
|
48
|
+
- **killed** / **reported** are terminal (no further field edits)
|
|
22
49
|
|
|
23
|
-
|
|
50
|
+
There is **no** `impact_proof` tool field. Put proof text in `impact` or `evidence`.
|
|
24
51
|
|
|
25
|
-
|
|
26
|
-
1. **Hypothesis**: Initial lead tracking (`CaseAdd`).
|
|
27
|
-
2. **Investigation**: Move to `investigating` when code paths or reachability trace are identified (`CaseUpdate`).
|
|
28
|
-
3. **Confirmation**: Validate with a working PoC script (`PromoteFinding`). Requires the PoC script to exit with code `0`.
|
|
29
|
-
4. **Chaining**: Link low-impact primitives to build high-impact chains (`CaseLink`).
|
|
30
|
-
5. **Reporting**: Compile markdown report (`CaseReport`) and transition finding to `reported` (`CaseUpdate`).
|
|
52
|
+
## Tools
|
|
31
53
|
|
|
32
|
-
|
|
54
|
+
| Tool | Use |
|
|
55
|
+
|------|-----|
|
|
56
|
+
| `CaseAdd` | Open a case (`title` required; start as `hypothesis` or `investigating`) |
|
|
57
|
+
| `CaseUpdate` | Evidence, impact, severity, status (not direct confirm) |
|
|
58
|
+
| `PromoteFinding` | Run on-disk PoC (Docker sandbox by default; `local:true` for host) → confirm on exit 0 |
|
|
59
|
+
| `CaseGet` / `CaseList` / `CaseSearch` | Read / filter / search |
|
|
60
|
+
| `CaseLink` / `CaseUnlink` | Bidirectional exploit chains |
|
|
61
|
+
| `CaseReport` | Markdown report for confirmed/reported cases |
|
|
33
62
|
|
|
34
|
-
|
|
35
|
-
|---|---|---|
|
|
36
|
-
| `CaseAdd` | Create a new hypothesis or investigation case | Title, target (scope) |
|
|
37
|
-
| `CaseUpdate` | Modify case details (remediation, impact, status) | ID, field updates |
|
|
38
|
-
| `PromoteFinding` | Run on-disk PoC script to transition status to `confirmed` | ID, `poc_path`, `local` flag |
|
|
39
|
-
| `CaseGet` | Retrieve full JSON details of a case | ID |
|
|
40
|
-
| `CaseList` | Query and filter cases by status/confidence/tags | Filter flags, limit, offset |
|
|
41
|
-
| `CaseSearch` | Perform full-text search across specific case fields | Query string, optional field |
|
|
42
|
-
| `CaseLink` | Create a bidirectional link between two cases | Source ID, Target ID |
|
|
43
|
-
| `CaseUnlink` | Remove link between two cases | Source ID, Target ID |
|
|
44
|
-
| `CaseReport` | Write markdown report to disk | ID (must be `confirmed` or `reported`) |
|
|
63
|
+
Commands: `/casefile` (dashboard), `/xp` (XP mode).
|
|
45
64
|
|
|
46
|
-
## Development
|
|
65
|
+
## Development
|
|
47
66
|
|
|
48
|
-
Run tests via Bun:
|
|
49
67
|
```bash
|
|
50
|
-
bun test
|
|
68
|
+
bun test packages/pi-casefile
|
|
69
|
+
bun run typecheck
|
|
51
70
|
```
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { matchesKey, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
11
11
|
import { Type } from "@sinclair/typebox";
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { homedir } from "node:os";
|
|
12
15
|
|
|
13
16
|
import {
|
|
14
17
|
addCaseResult,
|
|
@@ -542,6 +545,58 @@ function buildAgentInjection(active: CaseRecord[]): string {
|
|
|
542
545
|
return caseList ? `${caseList}\n\n${STATIC_CYBER_WORKFLOW}` : STATIC_CYBER_WORKFLOW;
|
|
543
546
|
}
|
|
544
547
|
|
|
548
|
+
// ── XP (offensive / exploit) mode toggle ─────────────────────────────
|
|
549
|
+
// Casefile historically injected the cyber workflow into every prompt.
|
|
550
|
+
// For normal dev work that is just noise, so XP mode defaults OFF. Enable
|
|
551
|
+
// it for offensive/audit sessions to get the full attacker discipline back.
|
|
552
|
+
// Toggle with /xp (or /xp on|off); override per-session with PI_XP_MODE.
|
|
553
|
+
// Pure helpers exported for unit tests.
|
|
554
|
+
|
|
555
|
+
export const XP_MODE_ENV = "PI_XP_MODE";
|
|
556
|
+
export type XpMode = "on" | "off";
|
|
557
|
+
|
|
558
|
+
export function getXpModeStatePath(): string {
|
|
559
|
+
try {
|
|
560
|
+
return join(dirname(getCasefilePath()), "xp-mode");
|
|
561
|
+
} catch {
|
|
562
|
+
return join(homedir(), ".pi", "xp-mode");
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function readXpMode(
|
|
567
|
+
envValue: string | undefined = process.env[XP_MODE_ENV],
|
|
568
|
+
statePath: string = getXpModeStatePath(),
|
|
569
|
+
): XpMode {
|
|
570
|
+
const env = (envValue ?? "").trim().toLowerCase();
|
|
571
|
+
if (env === "on" || env === "1" || env === "true") return "on";
|
|
572
|
+
if (env === "off" || env === "0" || env === "false") return "off";
|
|
573
|
+
try {
|
|
574
|
+
if (existsSync(statePath)) {
|
|
575
|
+
const v = readFileSync(statePath, "utf8").trim().toLowerCase();
|
|
576
|
+
if (v === "on") return "on";
|
|
577
|
+
if (v === "off") return "off";
|
|
578
|
+
}
|
|
579
|
+
} catch {
|
|
580
|
+
// ignore and fall through to default
|
|
581
|
+
}
|
|
582
|
+
return "off";
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function writeXpMode(state: XpMode, statePath: string = getXpModeStatePath()): void {
|
|
586
|
+
try {
|
|
587
|
+
writeFileSync(statePath, state, "utf8");
|
|
588
|
+
} catch {
|
|
589
|
+
// best-effort; env var can still override at runtime
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export function parseXpModeArg(args: string, current: XpMode): XpMode {
|
|
594
|
+
const arg = (args ?? "").trim().toLowerCase();
|
|
595
|
+
if (arg === "on") return "on";
|
|
596
|
+
if (arg === "off") return "off";
|
|
597
|
+
return current === "on" ? "off" : "on";
|
|
598
|
+
}
|
|
599
|
+
|
|
545
600
|
// ── Main extension ────────────────────────────────────────────────────
|
|
546
601
|
|
|
547
602
|
export default function casefileExtension(pi: ExtensionAPI) {
|
|
@@ -976,6 +1031,10 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
976
1031
|
name: "CaseUnlink",
|
|
977
1032
|
label: "Unlink Cases",
|
|
978
1033
|
description: "Remove a bidirectional link between two cases.",
|
|
1034
|
+
promptSnippet: "Remove a link between two cases",
|
|
1035
|
+
promptGuidelines: [
|
|
1036
|
+
"Use CaseUnlink to detach two cases that were previously linked with CaseLink (e.g. when a chain step is disproven or no longer relevant).",
|
|
1037
|
+
],
|
|
979
1038
|
parameters: UnlinkSchema,
|
|
980
1039
|
|
|
981
1040
|
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
@@ -1068,6 +1127,21 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1068
1127
|
},
|
|
1069
1128
|
});
|
|
1070
1129
|
|
|
1130
|
+
// ── Command: /xp (toggle offensive XP mode) ──
|
|
1131
|
+
|
|
1132
|
+
pi.registerCommand("xp", {
|
|
1133
|
+
description:
|
|
1134
|
+
"Toggle casefile XP (offensive) mode. ON injects the full cyber workflow each prompt; OFF (default) keeps context quiet for normal dev work. Usage: /xp [on|off]",
|
|
1135
|
+
handler: async (args, ctx) => {
|
|
1136
|
+
const next = parseXpModeArg(args ?? "", readXpMode());
|
|
1137
|
+
writeXpMode(next);
|
|
1138
|
+
ctx.ui.notify(
|
|
1139
|
+
`Casefile XP mode: ${next.toUpperCase()} (takes effect on the next prompt)`,
|
|
1140
|
+
next === "on" ? "info" : "warning",
|
|
1141
|
+
);
|
|
1142
|
+
},
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1071
1145
|
// ── Command: /casefile ──
|
|
1072
1146
|
|
|
1073
1147
|
pi.registerCommand("casefile", {
|
|
@@ -1103,10 +1177,15 @@ export default function casefileExtension(pi: ExtensionAPI) {
|
|
|
1103
1177
|
});
|
|
1104
1178
|
|
|
1105
1179
|
// ── Event: Inject context into system prompt ──
|
|
1180
|
+
// XP (offensive) mode is OFF by default so normal dev work stays quiet.
|
|
1181
|
+
// Only when enabled do we inject the cyber workflow (and case list) each
|
|
1182
|
+
// prompt. This keeps the agent focused during everyday development while
|
|
1183
|
+
// still allowing the full attacker discipline to be switched on for
|
|
1184
|
+
// offensive/audit/bounty sessions.
|
|
1106
1185
|
|
|
1107
1186
|
pi.on("before_agent_start", async () => {
|
|
1108
|
-
|
|
1109
|
-
|
|
1187
|
+
if (readXpMode() === "off") return;
|
|
1188
|
+
|
|
1110
1189
|
let active: CaseRecord[] = [];
|
|
1111
1190
|
try {
|
|
1112
1191
|
const records = readCasefile();
|