@aliou/pi-guardrails 0.4.1 → 0.5.1
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/README.md +52 -2
- package/config-schema.ts +8 -0
- package/config.ts +11 -1
- package/events.ts +2 -1
- package/hooks/enforce-package-manager.ts +96 -0
- package/hooks/index.ts +2 -0
- package/package.json +3 -3
- package/settings-command.ts +33 -1
package/README.md
CHANGED
|
@@ -36,8 +36,10 @@ pi install npm:@aliou/pi-guardrails
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
38
|
- **prevent-brew**: Blocks Homebrew commands (disabled by default)
|
|
39
|
+
- **prevent-python**: Blocks Python/pip/poetry commands, suggests uv instead (disabled by default)
|
|
39
40
|
- **protect-env-files**: Prevents access to `.env` files (except `.example`/`.sample`/`.test`)
|
|
40
41
|
- **permission-gate**: Prompts for confirmation on dangerous commands
|
|
42
|
+
- **enforce-package-manager**: Enforces a specific Node package manager (npm, pnpm, or bun) (disabled by default)
|
|
41
43
|
|
|
42
44
|
## Configuration
|
|
43
45
|
|
|
@@ -61,8 +63,13 @@ Use `Tab` / `Shift+Tab` to switch tabs. Boolean settings can be toggled directly
|
|
|
61
63
|
"enabled": true,
|
|
62
64
|
"features": {
|
|
63
65
|
"preventBrew": false,
|
|
66
|
+
"preventPython": false,
|
|
64
67
|
"protectEnvFiles": true,
|
|
65
|
-
"permissionGate": true
|
|
68
|
+
"permissionGate": true,
|
|
69
|
+
"enforcePackageManager": false
|
|
70
|
+
},
|
|
71
|
+
"packageManager": {
|
|
72
|
+
"selected": "npm"
|
|
66
73
|
},
|
|
67
74
|
"envFiles": {
|
|
68
75
|
"protectedPatterns": ["\\.env$", "\\.env\\.local$"],
|
|
@@ -96,8 +103,16 @@ All fields are optional. Missing fields use defaults shown above.
|
|
|
96
103
|
| Key | Default | Description |
|
|
97
104
|
|---|---|---|
|
|
98
105
|
| `preventBrew` | `false` | Block Homebrew install/upgrade commands |
|
|
106
|
+
| `preventPython` | `false` | Block python/pip/poetry commands (use uv instead) |
|
|
99
107
|
| `protectEnvFiles` | `true` | Block access to `.env` files containing secrets |
|
|
100
108
|
| `permissionGate` | `true` | Prompt for confirmation on dangerous commands |
|
|
109
|
+
| `enforcePackageManager` | `false` | Enforce a specific Node package manager |
|
|
110
|
+
|
|
111
|
+
#### `packageManager`
|
|
112
|
+
|
|
113
|
+
| Key | Default | Description |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `selected` | `"npm"` | Package manager to enforce: `"npm"`, `"pnpm"`, or `"bun"` |
|
|
101
116
|
|
|
102
117
|
#### `envFiles`
|
|
103
118
|
|
|
@@ -156,6 +171,19 @@ Auto-deny certain commands:
|
|
|
156
171
|
}
|
|
157
172
|
```
|
|
158
173
|
|
|
174
|
+
Enforce pnpm as the package manager:
|
|
175
|
+
|
|
176
|
+
```json
|
|
177
|
+
{
|
|
178
|
+
"features": {
|
|
179
|
+
"enforcePackageManager": true
|
|
180
|
+
},
|
|
181
|
+
"packageManager": {
|
|
182
|
+
"selected": "pnpm"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
159
187
|
## Events
|
|
160
188
|
|
|
161
189
|
The extension emits events on the pi event bus for inter-extension communication.
|
|
@@ -166,7 +194,7 @@ Emitted when a tool call is blocked by any guardrail.
|
|
|
166
194
|
|
|
167
195
|
```typescript
|
|
168
196
|
interface GuardrailsBlockedEvent {
|
|
169
|
-
feature: "preventBrew" | "protectEnvFiles" | "permissionGate";
|
|
197
|
+
feature: "preventBrew" | "preventPython" | "protectEnvFiles" | "permissionGate" | "enforcePackageManager";
|
|
170
198
|
toolName: string;
|
|
171
199
|
input: Record<string, unknown>;
|
|
172
200
|
reason: string;
|
|
@@ -201,6 +229,17 @@ Blocked patterns:
|
|
|
201
229
|
- `brew upgrade`
|
|
202
230
|
- `brew reinstall`
|
|
203
231
|
|
|
232
|
+
### prevent-python
|
|
233
|
+
|
|
234
|
+
Blocks bash commands that use Python tooling directly. Disabled by default. Enable if your project uses uv for Python management.
|
|
235
|
+
|
|
236
|
+
Blocked patterns:
|
|
237
|
+
- `python`, `python3`
|
|
238
|
+
- `pip`, `pip3`
|
|
239
|
+
- `poetry`
|
|
240
|
+
- `pyenv`
|
|
241
|
+
- `virtualenv`, `venv`
|
|
242
|
+
|
|
204
243
|
### protect-env-files
|
|
205
244
|
|
|
206
245
|
Prevents accessing `.env` files that might contain secrets. Only allows access to safe variants:
|
|
@@ -225,3 +264,14 @@ Prompts user confirmation before executing dangerous commands:
|
|
|
225
264
|
- `chown -R` (recursive ownership change)
|
|
226
265
|
|
|
227
266
|
All patterns are configurable. Supports allow-lists and auto-deny lists.
|
|
267
|
+
|
|
268
|
+
### enforce-package-manager
|
|
269
|
+
|
|
270
|
+
Enforces using a specific Node package manager. Disabled by default. When enabled, blocks commands using non-selected package managers.
|
|
271
|
+
|
|
272
|
+
Configure via `packageManager.selected`:
|
|
273
|
+
- `"npm"` (default)
|
|
274
|
+
- `"pnpm"`
|
|
275
|
+
- `"bun"`
|
|
276
|
+
|
|
277
|
+
Example: If `selected` is `"pnpm"`, running `npm install` or `bun add` will be blocked with a message instructing the agent to use `pnpm` instead.
|
package/config-schema.ts
CHANGED
|
@@ -12,6 +12,10 @@ export interface GuardrailsConfig {
|
|
|
12
12
|
preventPython?: boolean;
|
|
13
13
|
protectEnvFiles?: boolean;
|
|
14
14
|
permissionGate?: boolean;
|
|
15
|
+
enforcePackageManager?: boolean;
|
|
16
|
+
};
|
|
17
|
+
packageManager?: {
|
|
18
|
+
selected?: "bun" | "pnpm" | "npm";
|
|
15
19
|
};
|
|
16
20
|
envFiles?: {
|
|
17
21
|
protectedPatterns?: string[];
|
|
@@ -38,6 +42,10 @@ export interface ResolvedConfig {
|
|
|
38
42
|
preventPython: boolean;
|
|
39
43
|
protectEnvFiles: boolean;
|
|
40
44
|
permissionGate: boolean;
|
|
45
|
+
enforcePackageManager: boolean;
|
|
46
|
+
};
|
|
47
|
+
packageManager: {
|
|
48
|
+
selected: "bun" | "pnpm" | "npm";
|
|
41
49
|
};
|
|
42
50
|
envFiles: {
|
|
43
51
|
protectedPatterns: string[];
|
package/config.ts
CHANGED
|
@@ -19,9 +19,19 @@ const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
19
19
|
preventPython: false,
|
|
20
20
|
protectEnvFiles: true,
|
|
21
21
|
permissionGate: true,
|
|
22
|
+
enforcePackageManager: false,
|
|
23
|
+
},
|
|
24
|
+
packageManager: {
|
|
25
|
+
selected: "npm",
|
|
22
26
|
},
|
|
23
27
|
envFiles: {
|
|
24
|
-
protectedPatterns: [
|
|
28
|
+
protectedPatterns: [
|
|
29
|
+
"\\.env$",
|
|
30
|
+
"\\.env\\.local$",
|
|
31
|
+
"\\.env\\.production$",
|
|
32
|
+
"\\.env\\.prod$",
|
|
33
|
+
"\\.dev\\.vars$",
|
|
34
|
+
],
|
|
25
35
|
allowedPatterns: [
|
|
26
36
|
"\\.(example|sample|test)\\.env$",
|
|
27
37
|
"\\.env\\.(example|sample|test)$",
|
package/events.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ResolvedConfig } from "../config-schema";
|
|
3
|
+
import { emitBlocked } from "../events";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enforces using a specific Node package manager (bun, pnpm, or npm).
|
|
7
|
+
* Blocks commands using non-selected package managers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const BUN_PATTERN = /\bbun\b/;
|
|
11
|
+
const PNPM_PATTERN = /\bpnpm\b/;
|
|
12
|
+
const NPM_PATTERN = /\bnpm\b/;
|
|
13
|
+
|
|
14
|
+
type PackageManager = "bun" | "pnpm" | "npm";
|
|
15
|
+
|
|
16
|
+
interface ManagerInfo {
|
|
17
|
+
pattern: RegExp;
|
|
18
|
+
name: string;
|
|
19
|
+
installCmd: string;
|
|
20
|
+
addCmd: string;
|
|
21
|
+
runCmd: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const MANAGER_INFO: Record<PackageManager, ManagerInfo> = {
|
|
25
|
+
bun: {
|
|
26
|
+
pattern: BUN_PATTERN,
|
|
27
|
+
name: "bun",
|
|
28
|
+
installCmd: "bun install",
|
|
29
|
+
addCmd: "bun add <package>",
|
|
30
|
+
runCmd: "bun run <script>",
|
|
31
|
+
},
|
|
32
|
+
pnpm: {
|
|
33
|
+
pattern: PNPM_PATTERN,
|
|
34
|
+
name: "pnpm",
|
|
35
|
+
installCmd: "pnpm install",
|
|
36
|
+
addCmd: "pnpm add <package>",
|
|
37
|
+
runCmd: "pnpm run <script>",
|
|
38
|
+
},
|
|
39
|
+
npm: {
|
|
40
|
+
pattern: NPM_PATTERN,
|
|
41
|
+
name: "npm",
|
|
42
|
+
installCmd: "npm install",
|
|
43
|
+
addCmd: "npm install <package>",
|
|
44
|
+
runCmd: "npm run <script>",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function setupEnforcePackageManagerHook(
|
|
49
|
+
pi: ExtensionAPI,
|
|
50
|
+
config: ResolvedConfig,
|
|
51
|
+
) {
|
|
52
|
+
if (!config.features.enforcePackageManager) return;
|
|
53
|
+
|
|
54
|
+
const selectedManager = config.packageManager.selected;
|
|
55
|
+
const selected = MANAGER_INFO[selectedManager];
|
|
56
|
+
|
|
57
|
+
// Get all managers that should be blocked (all except the selected one)
|
|
58
|
+
const blockedManagers = (
|
|
59
|
+
Object.keys(MANAGER_INFO) as PackageManager[]
|
|
60
|
+
).filter((m) => m !== selectedManager);
|
|
61
|
+
|
|
62
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
63
|
+
if (event.toolName !== "bash") return;
|
|
64
|
+
|
|
65
|
+
const command = String(event.input.command ?? "");
|
|
66
|
+
|
|
67
|
+
for (const blockedManager of blockedManagers) {
|
|
68
|
+
const blocked = MANAGER_INFO[blockedManager];
|
|
69
|
+
|
|
70
|
+
if (blocked.pattern.test(command)) {
|
|
71
|
+
ctx.ui.notify(
|
|
72
|
+
`Blocked ${blocked.name} command. Use ${selected.name} instead.`,
|
|
73
|
+
"warning",
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const reason =
|
|
77
|
+
`This project uses ${selected.name} as its package manager. ` +
|
|
78
|
+
`Use ${selected.name} instead of ${blocked.name}. ` +
|
|
79
|
+
`Run \`${selected.installCmd}\` to install dependencies, ` +
|
|
80
|
+
`\`${selected.addCmd}\` to add packages, ` +
|
|
81
|
+
`and \`${selected.runCmd}\` to run scripts.`;
|
|
82
|
+
|
|
83
|
+
emitBlocked(pi, {
|
|
84
|
+
feature: "enforcePackageManager",
|
|
85
|
+
toolName: "bash",
|
|
86
|
+
input: event.input,
|
|
87
|
+
reason,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return { block: true, reason };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return;
|
|
95
|
+
});
|
|
96
|
+
}
|
package/hooks/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { ResolvedConfig } from "../config-schema";
|
|
3
|
+
import { setupEnforcePackageManagerHook } from "./enforce-package-manager";
|
|
3
4
|
import { setupPermissionGateHook } from "./permission-gate";
|
|
4
5
|
import { setupPreventBrewHook } from "./prevent-brew";
|
|
5
6
|
import { setupPreventPythonHook } from "./prevent-python";
|
|
@@ -10,4 +11,5 @@ export function setupGuardrailsHooks(pi: ExtensionAPI, config: ResolvedConfig) {
|
|
|
10
11
|
setupPreventPythonHook(pi, config);
|
|
11
12
|
setupProtectEnvFilesHook(pi, config);
|
|
12
13
|
setupPermissionGateHook(pi, config);
|
|
14
|
+
setupEnforcePackageManagerHook(pi, config);
|
|
13
15
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-guardrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"keywords": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"README.md"
|
|
29
29
|
],
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@mariozechner/pi-coding-agent": "0.50.
|
|
32
|
-
"@mariozechner/pi-tui": "0.50.
|
|
31
|
+
"@mariozechner/pi-coding-agent": "0.50.3",
|
|
32
|
+
"@mariozechner/pi-tui": "0.50.3"
|
|
33
33
|
}
|
|
34
34
|
}
|
package/settings-command.ts
CHANGED
|
@@ -36,6 +36,11 @@ const FEATURE_UI: Record<FeatureKey, FeatureUiDef> = {
|
|
|
36
36
|
description:
|
|
37
37
|
"Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
|
|
38
38
|
},
|
|
39
|
+
enforcePackageManager: {
|
|
40
|
+
label: "Enforce package manager",
|
|
41
|
+
description:
|
|
42
|
+
"Enforce using a specific Node package manager (bun, pnpm, or npm)",
|
|
43
|
+
},
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
@@ -328,6 +333,21 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
328
333
|
},
|
|
329
334
|
],
|
|
330
335
|
},
|
|
336
|
+
{
|
|
337
|
+
label: "Package Manager",
|
|
338
|
+
items: [
|
|
339
|
+
{
|
|
340
|
+
id: "packageManager.selected",
|
|
341
|
+
label: "Selected manager",
|
|
342
|
+
description:
|
|
343
|
+
"Which package manager to enforce (when feature is enabled)",
|
|
344
|
+
currentValue:
|
|
345
|
+
config.packageManager?.selected ??
|
|
346
|
+
resolved.packageManager.selected,
|
|
347
|
+
values: ["npm", "pnpm", "bun"],
|
|
348
|
+
},
|
|
349
|
+
],
|
|
350
|
+
},
|
|
331
351
|
];
|
|
332
352
|
|
|
333
353
|
return new SectionedSettings(
|
|
@@ -355,7 +375,7 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
355
375
|
: configLoader.getGlobalConfig();
|
|
356
376
|
const updated: GuardrailsConfig = structuredClone(config);
|
|
357
377
|
|
|
358
|
-
// Boolean toggles
|
|
378
|
+
// Boolean toggles
|
|
359
379
|
if (
|
|
360
380
|
newValue === "enabled" ||
|
|
361
381
|
newValue === "disabled" ||
|
|
@@ -365,6 +385,18 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
365
385
|
const boolVal = newValue === "enabled" || newValue === "on";
|
|
366
386
|
setNestedValue(updated, id, boolVal);
|
|
367
387
|
|
|
388
|
+
const ok = await saveTabConfig(tab, updated);
|
|
389
|
+
if (ok) {
|
|
390
|
+
settings = buildSettings(activeTab);
|
|
391
|
+
tui.requestRender();
|
|
392
|
+
}
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Package manager selection
|
|
397
|
+
if (id === "packageManager.selected") {
|
|
398
|
+
setNestedValue(updated, id, newValue);
|
|
399
|
+
|
|
368
400
|
const ok = await saveTabConfig(tab, updated);
|
|
369
401
|
if (ok) {
|
|
370
402
|
settings = buildSettings(activeTab);
|