@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.
- package/hooks/permission-gate.ts +98 -62
- package/package.json +2 -2
package/hooks/permission-gate.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
225
|
-
const container = new Container();
|
|
226
|
-
const redBorder = (s: string) => theme.fg("error", s);
|
|
230
|
+
type ConfirmResult = "allow" | "allow-session" | "deny";
|
|
227
231
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
new
|
|
231
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
theme.fg("
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
36
|
+
"@aliou/pi-utils-settings": "^0.3.0"
|
|
37
37
|
},
|
|
38
38
|
"peerDependencies": {
|
|
39
39
|
"@mariozechner/pi-coding-agent": ">=0.51.0"
|