@erichll/pi-auto-review 0.15.2 → 0.15.3
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/CHANGELOG.md +15 -0
- package/README.md +8 -1
- package/package.json +3 -3
- package/src/policy-audit/classifier.ts +60 -3
- package/src/policy.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.15.3 - 2026-09-03
|
|
4
|
+
|
|
5
|
+
- Tolerate a single enclosing ```` ```json ```` or bare Markdown code fence
|
|
6
|
+
around reviewer decisions while preserving strict decision-schema
|
|
7
|
+
validation (fixes reviewer models that fence JSON despite the prompt,
|
|
8
|
+
notably when routed through Claude Code).
|
|
9
|
+
- Verify compatibility with `@gotgenes/pi-permission-system` 30.2.0 and
|
|
10
|
+
31.0.0, widen the peer range through 31.x, and move the development baseline
|
|
11
|
+
to 31.0.0.
|
|
12
|
+
- Keep permission-system 31 statement-operand audit classification aligned for
|
|
13
|
+
`for`/`select` word lists and `case` subjects without treating case patterns
|
|
14
|
+
as accessed paths.
|
|
15
|
+
- Confirm that model auto-confirm stays one-shot and cannot select
|
|
16
|
+
permission-system 30.2's wider both-directions session grant.
|
|
17
|
+
|
|
3
18
|
## 0.15.2 - 2026-09-02
|
|
4
19
|
|
|
5
20
|
- No behavior changes. Verified against `@gotgenes/pi-permission-system`
|
package/README.md
CHANGED
|
@@ -31,7 +31,8 @@ that dependency is a hard prerequisite (see [Install and enable](#install-and-en
|
|
|
31
31
|
> **Prerequisite:** pi-auto-review is an authorizer inside
|
|
32
32
|
> `@gotgenes/pi-permission-system`. Pi does not auto-install peer packages, so
|
|
33
33
|
> install the permission system separately (once per machine) before this
|
|
34
|
-
> extension. This release line supports permission-system
|
|
34
|
+
> extension. This release line supports permission-system 29.3.0 through the
|
|
35
|
+
> latest 31.x release:
|
|
35
36
|
|
|
36
37
|
Node.js 22.13.0 or newer is required. Permission auditing uses Node's built-in
|
|
37
38
|
`node:sqlite`; it does not require a SQLite CLI, system SQLite library, or npm
|
|
@@ -76,6 +77,12 @@ following recognized permission dialog. The bridge is request-ID-bound,
|
|
|
76
77
|
expires after ten seconds, and is consumed once. Mode, component, request, or
|
|
77
78
|
event-order mismatches leave the original human dialog in place.
|
|
78
79
|
|
|
80
|
+
Permission-system 31.x also projects paths named directly by `for`/`select`
|
|
81
|
+
word lists and `case` subjects onto the existing `path` and
|
|
82
|
+
`external_directory` surfaces. Those additional requests follow the same
|
|
83
|
+
bounded review and one-shot auto-confirm rules; no new surface is implicitly
|
|
84
|
+
trusted.
|
|
85
|
+
|
|
79
86
|
Automatic review stops for the current turn after three consecutive denials or
|
|
80
87
|
ten denials in the last fifty reviews. An explicit denial tells the agent that
|
|
81
88
|
automatic policy denied the request (not a human click), not to rephrase or
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@erichll/pi-auto-review",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.3",
|
|
4
4
|
"description": "Fail-closed, model-backed approval broker for Pi: deterministically hard-denies dangerous operations, reviews dangerous boundaries with a reviewer model, and issues one-shot expiring grants that OS sandbox adapters must consume before retrying.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,12 +49,12 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
51
51
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
52
|
-
"@gotgenes/pi-permission-system": ">=
|
|
52
|
+
"@gotgenes/pi-permission-system": ">=29.3.0 <32.0.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@earendil-works/pi-ai": "^0.84.4",
|
|
56
56
|
"@earendil-works/pi-coding-agent": "^0.84.4",
|
|
57
|
-
"@gotgenes/pi-permission-system": "
|
|
57
|
+
"@gotgenes/pi-permission-system": "31.0.0",
|
|
58
58
|
"@types/node": "^26.2.0",
|
|
59
59
|
"typescript": "^6.0.3"
|
|
60
60
|
}
|
|
@@ -75,6 +75,62 @@ function shellTokens(command: string): string[] {
|
|
|
75
75
|
return command.trim().split(/\s+/u);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
function normalizedPathToken(token: string): string {
|
|
79
|
+
let value = token.replace(/^@/, "").replace(/[;)]+$/u, "");
|
|
80
|
+
if (
|
|
81
|
+
value.length >= 2 &&
|
|
82
|
+
((value.startsWith("\"") && value.endsWith("\"")) ||
|
|
83
|
+
(value.startsWith("'") && value.endsWith("'")))
|
|
84
|
+
) {
|
|
85
|
+
value = value.slice(1, -1);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function firstExplicitPathToken(tokens: readonly string[]): string | undefined {
|
|
91
|
+
return tokens
|
|
92
|
+
.map(normalizedPathToken)
|
|
93
|
+
.find((token) =>
|
|
94
|
+
token.startsWith("/") ||
|
|
95
|
+
token.startsWith("./") ||
|
|
96
|
+
token.startsWith("../") ||
|
|
97
|
+
token.startsWith("~/")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Recover the path-bearing statement operands added to permission-system 31.
|
|
103
|
+
* This remains deliberately conservative rather than pretending to be a shell
|
|
104
|
+
* parser: case-arm patterns are excluded, while the actual path gate's
|
|
105
|
+
* separate decision event still records paths found in arm/body commands.
|
|
106
|
+
*/
|
|
107
|
+
function statementOperandPathToken(
|
|
108
|
+
tokens: readonly string[],
|
|
109
|
+
index: number,
|
|
110
|
+
): { handled: boolean; token?: string } {
|
|
111
|
+
const statement = tokens[index]?.replace(/^["']|["']$/gu, "");
|
|
112
|
+
if (statement !== "for" && statement !== "select" && statement !== "case") {
|
|
113
|
+
return { handled: false };
|
|
114
|
+
}
|
|
115
|
+
const inIndex = tokens.findIndex((token, tokenIndex) =>
|
|
116
|
+
tokenIndex > index && token === "in"
|
|
117
|
+
);
|
|
118
|
+
if (inIndex < 0) return { handled: true };
|
|
119
|
+
if (statement === "case") {
|
|
120
|
+
return {
|
|
121
|
+
handled: true,
|
|
122
|
+
token: firstExplicitPathToken(tokens.slice(index + 1, inIndex)),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const operands: string[] = [];
|
|
126
|
+
for (const token of tokens.slice(inIndex + 1)) {
|
|
127
|
+
if (token === "do") break;
|
|
128
|
+
operands.push(token);
|
|
129
|
+
if (/[;]$/u.test(token)) break;
|
|
130
|
+
}
|
|
131
|
+
return { handled: true, token: firstExplicitPathToken(operands) };
|
|
132
|
+
}
|
|
133
|
+
|
|
78
134
|
function executableIndex(tokens: readonly string[]): number {
|
|
79
135
|
let index = 0;
|
|
80
136
|
if (tokens[index] === "env") index++;
|
|
@@ -161,9 +217,10 @@ export function classifyBash(command: unknown, cwd: string): ClassifiedPermissio
|
|
|
161
217
|
eligible: blocker === undefined,
|
|
162
218
|
...(blocker ? { blocker } : {}),
|
|
163
219
|
} : undefined;
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
220
|
+
const statementOperand = statementOperandPathToken(tokens, index);
|
|
221
|
+
const pathToken = statementOperand.handled
|
|
222
|
+
? statementOperand.token
|
|
223
|
+
: firstExplicitPathToken(tokens.slice(index + 1));
|
|
167
224
|
return {
|
|
168
225
|
surface: "bash",
|
|
169
226
|
signature: template ?? "<command>",
|
package/src/policy.ts
CHANGED
|
@@ -202,10 +202,20 @@ function exactKeys(
|
|
|
202
202
|
);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// Some reviewer models (notably any routed through Claude Code, which always
|
|
206
|
+
// markdown-fences JSON) wrap the decision object in a ```json ... ``` block
|
|
207
|
+
// despite the prompt forbidding markdown. Strip a single enclosing fence before
|
|
208
|
+
// parsing; the strict shape validation below is unchanged.
|
|
209
|
+
function stripReviewerFence(text: string): string {
|
|
210
|
+
const t = text.trim();
|
|
211
|
+
const fenced = t.match(/^```(?:json)?\s*\n?([\s\S]*?)\n?```$/i);
|
|
212
|
+
return (fenced ? fenced[1] : t).trim();
|
|
213
|
+
}
|
|
214
|
+
|
|
205
215
|
export function parseDecision(text: string): ModelDecision {
|
|
206
216
|
let value: unknown;
|
|
207
217
|
try {
|
|
208
|
-
value = JSON.parse(text
|
|
218
|
+
value = JSON.parse(stripReviewerFence(text));
|
|
209
219
|
} catch {
|
|
210
220
|
throw new Error("reviewer returned non-JSON output");
|
|
211
221
|
}
|