@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 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 28.x and 29.x:
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.2",
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": ">=28.0.0 <30.0.0"
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": "29.3.0",
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 pathToken = tokens.slice(index + 1)
165
- .map((token) => token.replace(/^@/, ""))
166
- .find((token) => token.startsWith("/") || token.startsWith("./") || token.startsWith("../") || token.startsWith("~/"));
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.trim());
218
+ value = JSON.parse(stripReviewerFence(text));
209
219
  } catch {
210
220
  throw new Error("reviewer returned non-JSON output");
211
221
  }