protect-context 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +613 -2
- package/extension/index.ts +1024 -0
- package/extension/lib/brief.ts +128 -0
- package/extension/lib/config.ts +67 -0
- package/extension/lib/contract.ts +160 -0
- package/extension/lib/contract_check.ts +62 -0
- package/extension/lib/defaults.ts +124 -0
- package/extension/lib/events.ts +69 -0
- package/extension/lib/fingerprint.ts +56 -0
- package/extension/lib/gitparse.ts +229 -0
- package/extension/lib/journal.ts +100 -0
- package/extension/lib/loops.ts +151 -0
- package/extension/lib/orientation.ts +209 -0
- package/extension/lib/status.ts +88 -0
- package/extension/lib/store.ts +500 -0
- package/extension/lib/text.ts +24 -0
- package/extension/lib/verify.ts +96 -0
- package/extension/lib/widget.ts +28 -0
- package/package.json +54 -4
- package/preview.png +0 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Evidence ledger (.agent-guard/verify_runs.jsonl) formatting (SPEC §1 #2, §2).
|
|
3
|
+
* Pure logic — no pi imports; command execution lives in index.ts.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { appendJsonl, readJsonl, verifyRunsPath } from "./store.ts";
|
|
7
|
+
import { CMD_DISPLAY_MAX, truncLine } from "./brief.ts";
|
|
8
|
+
|
|
9
|
+
export interface VerifyRun {
|
|
10
|
+
id: string;
|
|
11
|
+
ts: string;
|
|
12
|
+
session: string;
|
|
13
|
+
cmd: string;
|
|
14
|
+
exit: number;
|
|
15
|
+
ok: boolean;
|
|
16
|
+
digest: string;
|
|
17
|
+
ms: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** FROZEN digest rule: last ≤2000 chars of combined stdout+stderr. */
|
|
21
|
+
export function digestOf(stdout: string, stderr: string): string {
|
|
22
|
+
return `${stdout}${stderr}`.slice(-2000);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Timeout clamps (mirrors verify._clamp_timeout on the Python side): a
|
|
26
|
+
// model-supplied timeout_s can never wedge the session.
|
|
27
|
+
export const TIMEOUT_MIN_S = 1;
|
|
28
|
+
export const TIMEOUT_MAX_S = 900;
|
|
29
|
+
export const TIMEOUT_DEFAULT_S = 180;
|
|
30
|
+
|
|
31
|
+
/** Clamp a timeout to [TIMEOUT_MIN_S, TIMEOUT_MAX_S]; non-finite input falls
|
|
32
|
+
* back to the default. Fractions are truncated to whole seconds. */
|
|
33
|
+
export function clampTimeoutS(v: number | null | undefined): number {
|
|
34
|
+
const t = typeof v === "number" && Number.isFinite(v) ? Math.trunc(v) : TIMEOUT_DEFAULT_S;
|
|
35
|
+
return Math.max(TIMEOUT_MIN_S, Math.min(TIMEOUT_MAX_S, t));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Next evidence id "ev-NNNN" based on the max existing suffix. */
|
|
39
|
+
export function nextEvidenceId(runs: readonly unknown[]): string {
|
|
40
|
+
let max = 0;
|
|
41
|
+
for (const r of runs) {
|
|
42
|
+
const id = r && typeof r === "object" ? (r as { id?: unknown }).id : null;
|
|
43
|
+
const m = typeof id === "string" ? id.match(/^ev-(\d+)$/) : null;
|
|
44
|
+
if (m) max = Math.max(max, Number(m[1]));
|
|
45
|
+
}
|
|
46
|
+
return `ev-${String(max + 1).padStart(4, "0")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function readVerifyRuns(cwd: string): VerifyRun[] {
|
|
50
|
+
return readJsonl<unknown>(verifyRunsPath(cwd)).filter(isVerifyRun);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Raw object rows are used only for collision-free id allocation. A damaged
|
|
54
|
+
* row with a valid ev-NNNN id still reserves that id, even though typed views
|
|
55
|
+
* correctly ignore the rest of the malformed record. */
|
|
56
|
+
export function readVerifyRowsForIds(cwd: string): unknown[] {
|
|
57
|
+
return readJsonl<unknown>(verifyRunsPath(cwd));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isVerifyRun(value: unknown): value is VerifyRun {
|
|
61
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
62
|
+
const row = value as Record<string, unknown>;
|
|
63
|
+
return (
|
|
64
|
+
typeof row.id === "string" &&
|
|
65
|
+
typeof row.ts === "string" &&
|
|
66
|
+
typeof row.session === "string" &&
|
|
67
|
+
typeof row.cmd === "string" &&
|
|
68
|
+
typeof row.exit === "number" &&
|
|
69
|
+
Number.isFinite(row.exit) &&
|
|
70
|
+
typeof row.ok === "boolean" &&
|
|
71
|
+
typeof row.digest === "string" &&
|
|
72
|
+
typeof row.ms === "number" &&
|
|
73
|
+
Number.isFinite(row.ms)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function appendVerifyRun(cwd: string, run: VerifyRun): void {
|
|
78
|
+
appendJsonl(verifyRunsPath(cwd), run);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function formatMs(ms: number | null): string {
|
|
82
|
+
if (ms === null || !Number.isFinite(ms)) return "?s";
|
|
83
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One compact evidence line: `ev-0002 exit=0 'npm test' 3.2s`. The command
|
|
87
|
+
* is truncated (goal #12) — never inject an unbounded one-liner. */
|
|
88
|
+
export function formatVerifyLine(run: VerifyRun): string {
|
|
89
|
+
const id = truncLine(run.id, 40) || "unknown-id";
|
|
90
|
+
const exit = Number.isFinite(run.exit) ? run.exit : "?";
|
|
91
|
+
return `${id} exit=${exit} '${truncLine(run.cmd, CMD_DISPLAY_MAX)}' ${formatMs(run.ms)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function runsForSession(runs: VerifyRun[], session: string): VerifyRun[] {
|
|
95
|
+
return runs.filter((r) => r.session === session);
|
|
96
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent single-line status widget (SPEC §5).
|
|
3
|
+
* Pure logic — no pi imports.
|
|
4
|
+
*
|
|
5
|
+
* Format (FROZEN example): `PC Δ+123L/4f 1loop ev2 ctx41%`
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface WidgetData {
|
|
9
|
+
diffLines: number;
|
|
10
|
+
diffFiles: number;
|
|
11
|
+
/** False until/unless the Git scope audit succeeds. */
|
|
12
|
+
diffAvailable?: boolean;
|
|
13
|
+
/** True when the line count stopped at a bounded untracked scan. */
|
|
14
|
+
diffLowerBound?: boolean;
|
|
15
|
+
/** Number of commands currently in a failing loop. */
|
|
16
|
+
loops: number;
|
|
17
|
+
/** Evidence (verify runs) recorded this session. */
|
|
18
|
+
evidence: number;
|
|
19
|
+
ctxPct: number | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function renderWidget(d: WidgetData): string {
|
|
23
|
+
const pct = d.ctxPct === null ? 0 : Math.round(d.ctxPct);
|
|
24
|
+
const diff = d.diffAvailable === false
|
|
25
|
+
? "Δ?"
|
|
26
|
+
: `${d.diffLowerBound ? "Δ≥" : "Δ+"}${d.diffLines}L/${d.diffFiles}f`;
|
|
27
|
+
return `PC ${diff} ${d.loops}loop ev${d.evidence} ctx${pct}%`;
|
|
28
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protect-context",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Pi extension that adds agent guardrails: context brief, verification evidence, loop blocking, diff-scope warnings, and a durable journal.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"pi",
|
|
11
|
+
"guardrails",
|
|
12
|
+
"agent-safety",
|
|
13
|
+
"context"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/XJTLUmedia/Protect-Context-Pi-Coding-Agent",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/XJTLUmedia/Protect-Context-Pi-Coding-Agent.git"
|
|
19
|
+
},
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/XJTLUmedia/Protect-Context-Pi-Coding-Agent/issues"
|
|
22
|
+
},
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./extension/index.ts"
|
|
26
|
+
],
|
|
27
|
+
"image": "./preview.png"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"extension/index.ts",
|
|
31
|
+
"extension/lib",
|
|
32
|
+
"preview.png",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=22.19.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"test": "node --experimental-strip-types --test extension/test/*.test.ts",
|
|
40
|
+
"typecheck": "tsc --noEmit -p extension/tsconfig.json",
|
|
41
|
+
"check": "npm run typecheck && npm test",
|
|
42
|
+
"prepublishOnly": "npm run check"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@earendil-works/pi-ai": "*",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
47
|
+
"typebox": "*"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
52
|
+
"@types/node": "22.20.1",
|
|
53
|
+
"typebox": "1.3.23",
|
|
54
|
+
"typescript": "7.0.2"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/preview.png
ADDED
|
Binary file
|