@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 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: ["\\.env$", "\\.env\\.local$"],
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
@@ -8,7 +8,8 @@ export interface GuardrailsBlockedEvent {
8
8
  | "preventBrew"
9
9
  | "preventPython"
10
10
  | "protectEnvFiles"
11
- | "permissionGate";
11
+ | "permissionGate"
12
+ | "enforcePackageManager";
12
13
  toolName: string;
13
14
  input: Record<string, unknown>;
14
15
  reason: string;
@@ -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.4.1",
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.0",
32
- "@mariozechner/pi-tui": "0.50.0"
31
+ "@mariozechner/pi-coding-agent": "0.50.3",
32
+ "@mariozechner/pi-tui": "0.50.3"
33
33
  }
34
34
  }
@@ -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 only - array saves handled by submenus
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);