@aliou/pi-guardrails 0.7.1 → 0.7.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.
@@ -10,6 +10,7 @@ import {
10
10
  wrapTextWithAnsi,
11
11
  } from "@mariozechner/pi-tui";
12
12
  import type { DangerousPattern, ResolvedConfig } from "../config";
13
+ import { configLoader } from "../config";
13
14
  import { emitBlocked, emitDangerous } from "../utils/events";
14
15
  import {
15
16
  type CompiledPattern,
@@ -119,7 +120,11 @@ function findDangerousMatch(
119
120
  }
120
121
  return false;
121
122
  });
122
- if (match) return match;
123
+ // Structural matching succeeded -- return result (even if no match).
124
+ // Do NOT fall through to compiled patterns which do raw substring
125
+ // matching and would false-positive on e.g. "sudo" inside a quoted
126
+ // commit message argument.
127
+ return match;
123
128
  } catch {
124
129
  // Parse failed -- fall back to substring matching on raw string
125
130
  for (const p of fallbackPatterns) {
@@ -131,7 +136,8 @@ function findDangerousMatch(
131
136
  }
132
137
 
133
138
  // Check compiled patterns (substring/regex on raw string).
134
- // When customPatterns replaces defaults, this is the only matching path.
139
+ // Only reached when customPatterns replaces defaults (useBuiltinMatchers
140
+ // is false) or when the structural parse failed and no fallback matched.
135
141
  for (const cp of compiledPatterns) {
136
142
  if (cp.test(command)) {
137
143
  const src = cp.source as DangerousPattern;
@@ -221,70 +227,100 @@ export function setupPermissionGateHook(
221
227
  return { block: true, reason };
222
228
  }
223
229
 
224
- const proceed = await ctx.ui.custom<boolean>((_tui, theme, _kb, done) => {
225
- const container = new Container();
226
- const redBorder = (s: string) => theme.fg("error", s);
230
+ type ConfirmResult = "allow" | "allow-session" | "deny";
227
231
 
228
- container.addChild(new DynamicBorder(redBorder));
229
- container.addChild(
230
- new Text(
231
- theme.fg("error", theme.bold("Dangerous Command Detected")),
232
- 1,
233
- 0,
234
- ),
235
- );
236
- container.addChild(new Spacer(1));
237
- container.addChild(
238
- new Text(
239
- theme.fg("warning", `This command contains ${description}:`),
240
- 1,
241
- 0,
242
- ),
243
- );
244
- container.addChild(new Spacer(1));
245
- container.addChild(
246
- new DynamicBorder((s: string) => theme.fg("muted", s)),
247
- );
248
- const commandText = new Text("", 1, 0);
249
- container.addChild(commandText);
250
- container.addChild(
251
- new DynamicBorder((s: string) => theme.fg("muted", s)),
252
- );
253
- container.addChild(new Spacer(1));
254
- container.addChild(
255
- new Text(theme.fg("text", "Allow execution?"), 1, 0),
256
- );
257
- container.addChild(new Spacer(1));
258
- container.addChild(
259
- new Text(theme.fg("dim", "y/enter: allow • n/esc: deny"), 1, 0),
260
- );
261
- container.addChild(new DynamicBorder(redBorder));
232
+ const result = await ctx.ui.custom<ConfirmResult>(
233
+ (_tui, theme, _kb, done) => {
234
+ const container = new Container();
235
+ const redBorder = (s: string) => theme.fg("error", s);
262
236
 
263
- return {
264
- render: (width: number) => {
265
- const wrappedCommand = wrapTextWithAnsi(
266
- theme.fg("text", command),
267
- width - 4,
268
- ).join("\n");
269
- commandText.setText(wrappedCommand);
270
- return container.render(width);
271
- },
272
- invalidate: () => container.invalidate(),
273
- handleInput: (data: string) => {
274
- if (matchesKey(data, Key.enter) || data === "y" || data === "Y") {
275
- done(true);
276
- } else if (
277
- matchesKey(data, Key.escape) ||
278
- data === "n" ||
279
- data === "N"
280
- ) {
281
- done(false);
282
- }
237
+ container.addChild(new DynamicBorder(redBorder));
238
+ container.addChild(
239
+ new Text(
240
+ theme.fg("error", theme.bold("Dangerous Command Detected")),
241
+ 1,
242
+ 0,
243
+ ),
244
+ );
245
+ container.addChild(new Spacer(1));
246
+ container.addChild(
247
+ new Text(
248
+ theme.fg("warning", `This command contains ${description}:`),
249
+ 1,
250
+ 0,
251
+ ),
252
+ );
253
+ container.addChild(new Spacer(1));
254
+ container.addChild(
255
+ new DynamicBorder((s: string) => theme.fg("muted", s)),
256
+ );
257
+ const commandText = new Text("", 1, 0);
258
+ container.addChild(commandText);
259
+ container.addChild(
260
+ new DynamicBorder((s: string) => theme.fg("muted", s)),
261
+ );
262
+ container.addChild(new Spacer(1));
263
+ container.addChild(
264
+ new Text(theme.fg("text", "Allow execution?"), 1, 0),
265
+ );
266
+ container.addChild(new Spacer(1));
267
+ container.addChild(
268
+ new Text(
269
+ theme.fg(
270
+ "dim",
271
+ "y/enter: allow • a: allow for session • n/esc: deny",
272
+ ),
273
+ 1,
274
+ 0,
275
+ ),
276
+ );
277
+ container.addChild(new DynamicBorder(redBorder));
278
+
279
+ return {
280
+ render: (width: number) => {
281
+ const wrappedCommand = wrapTextWithAnsi(
282
+ theme.fg("text", command),
283
+ width - 4,
284
+ ).join("\n");
285
+ commandText.setText(wrappedCommand);
286
+ return container.render(width);
287
+ },
288
+ invalidate: () => container.invalidate(),
289
+ handleInput: (data: string) => {
290
+ if (matchesKey(data, Key.enter) || data === "y" || data === "Y") {
291
+ done("allow");
292
+ } else if (data === "a" || data === "A") {
293
+ done("allow-session");
294
+ } else if (
295
+ matchesKey(data, Key.escape) ||
296
+ data === "n" ||
297
+ data === "N"
298
+ ) {
299
+ done("deny");
300
+ }
301
+ },
302
+ };
303
+ },
304
+ );
305
+
306
+ if (result === "allow-session") {
307
+ // Save command as allowed in memory scope (session-only).
308
+ // Spread the resolved allowed patterns and append the new one.
309
+ const resolved = configLoader.getConfig();
310
+ await configLoader.save("memory", {
311
+ permissionGate: {
312
+ allowedPatterns: [
313
+ ...resolved.permissionGate.allowedPatterns,
314
+ { pattern: command },
315
+ ],
283
316
  },
284
- };
285
- });
317
+ });
318
+
319
+ // Update the local cache so it takes effect immediately
320
+ allowedPatterns.push(...compileCommandPatterns([{ pattern: command }]));
321
+ }
286
322
 
287
- if (!proceed) {
323
+ if (result === "deny") {
288
324
  emitBlocked(pi, {
289
325
  feature: "permissionGate",
290
326
  toolName: "bash",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-guardrails",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  ],
34
34
  "dependencies": {
35
35
  "@aliou/sh": "^0.1.0",
36
- "@aliou/pi-utils-settings": "^0.2.1"
36
+ "@aliou/pi-utils-settings": "^0.3.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@mariozechner/pi-coding-agent": ">=0.51.0"