@aliou/pi-guardrails 0.4.0 → 0.5.0
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 +20 -11
- package/events.ts +2 -1
- package/hooks/enforce-package-manager.ts +96 -0
- package/hooks/index.ts +2 -0
- package/package.json +1 -1
- package/settings-command.ts +80 -38
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,6 +19,10 @@ 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
28
|
protectedPatterns: ["\\.env$", "\\.env\\.local$"],
|
|
@@ -98,20 +102,25 @@ class ConfigLoader {
|
|
|
98
102
|
return merged;
|
|
99
103
|
}
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
private mergeInto<TTarget extends object, TSource extends object>(
|
|
106
|
+
target: TTarget,
|
|
107
|
+
source: TSource,
|
|
108
|
+
): void {
|
|
109
|
+
const t = target as Record<string, unknown>;
|
|
110
|
+
const s = source as Record<string, unknown>;
|
|
111
|
+
|
|
112
|
+
for (const key in s) {
|
|
113
|
+
if (s[key] === undefined) continue;
|
|
105
114
|
|
|
106
115
|
if (
|
|
107
|
-
typeof
|
|
108
|
-
!Array.isArray(
|
|
109
|
-
|
|
116
|
+
typeof s[key] === "object" &&
|
|
117
|
+
!Array.isArray(s[key]) &&
|
|
118
|
+
s[key] !== null
|
|
110
119
|
) {
|
|
111
|
-
if (!
|
|
112
|
-
this.mergeInto(
|
|
120
|
+
if (!t[key]) t[key] = {};
|
|
121
|
+
this.mergeInto(t[key] as object, s[key] as object);
|
|
113
122
|
} else {
|
|
114
|
-
|
|
123
|
+
t[key] = s[key];
|
|
115
124
|
}
|
|
116
125
|
}
|
|
117
126
|
}
|
|
@@ -138,7 +147,7 @@ class ConfigLoader {
|
|
|
138
147
|
config: GuardrailsConfig,
|
|
139
148
|
): Promise<void> {
|
|
140
149
|
await mkdir(dirname(path), { recursive: true });
|
|
141
|
-
await writeFile(path, JSON.stringify(config, null, 2)
|
|
150
|
+
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
hasGlobalConfig(): boolean {
|
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
package/settings-command.ts
CHANGED
|
@@ -3,12 +3,46 @@ import { getSettingsListTheme } from "@mariozechner/pi-coding-agent";
|
|
|
3
3
|
import { Key, matchesKey } from "@mariozechner/pi-tui";
|
|
4
4
|
import { ArrayEditor } from "./array-editor";
|
|
5
5
|
import { configLoader } from "./config";
|
|
6
|
-
import type { GuardrailsConfig } from "./config-schema";
|
|
6
|
+
import type { GuardrailsConfig, ResolvedConfig } from "./config-schema";
|
|
7
7
|
import { PatternEditor } from "./pattern-editor";
|
|
8
8
|
import { SectionedSettings, type SettingsSection } from "./sectioned-settings";
|
|
9
9
|
|
|
10
10
|
type Tab = "local" | "global";
|
|
11
11
|
|
|
12
|
+
// Typed feature UI definitions. Adding a key to ResolvedConfig.features
|
|
13
|
+
// without adding it here will cause a type error.
|
|
14
|
+
type FeatureKey = keyof ResolvedConfig["features"];
|
|
15
|
+
|
|
16
|
+
interface FeatureUiDef {
|
|
17
|
+
label: string;
|
|
18
|
+
description: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const FEATURE_UI: Record<FeatureKey, FeatureUiDef> = {
|
|
22
|
+
preventBrew: {
|
|
23
|
+
label: "Prevent Homebrew",
|
|
24
|
+
description: "Block brew commands",
|
|
25
|
+
},
|
|
26
|
+
preventPython: {
|
|
27
|
+
label: "Prevent Python",
|
|
28
|
+
description: "Block python/pip/poetry commands. Use uv instead.",
|
|
29
|
+
},
|
|
30
|
+
protectEnvFiles: {
|
|
31
|
+
label: "Protect .env files",
|
|
32
|
+
description: "Block access to .env files containing secrets",
|
|
33
|
+
},
|
|
34
|
+
permissionGate: {
|
|
35
|
+
label: "Permission gate",
|
|
36
|
+
description:
|
|
37
|
+
"Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
|
|
38
|
+
},
|
|
39
|
+
enforcePackageManager: {
|
|
40
|
+
label: "Enforce package manager",
|
|
41
|
+
description:
|
|
42
|
+
"Enforce using a specific Node package manager (bun, pnpm, or npm)",
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
12
46
|
export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
13
47
|
pi.registerCommand("guardrails:settings", {
|
|
14
48
|
description: "Configure guardrails (local/global)",
|
|
@@ -168,45 +202,26 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
168
202
|
: configLoader.getGlobalConfig();
|
|
169
203
|
const resolved = configLoader.getConfig();
|
|
170
204
|
|
|
205
|
+
const featureItems = (Object.keys(FEATURE_UI) as FeatureKey[]).map(
|
|
206
|
+
(key) => {
|
|
207
|
+
const def = FEATURE_UI[key];
|
|
208
|
+
return {
|
|
209
|
+
id: `features.${key}`,
|
|
210
|
+
label: def.label,
|
|
211
|
+
description: def.description,
|
|
212
|
+
currentValue:
|
|
213
|
+
(config.features?.[key] ?? resolved.features[key])
|
|
214
|
+
? "enabled"
|
|
215
|
+
: "disabled",
|
|
216
|
+
values: ["enabled", "disabled"],
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
);
|
|
220
|
+
|
|
171
221
|
const sections: SettingsSection[] = [
|
|
172
222
|
{
|
|
173
223
|
label: "Features",
|
|
174
|
-
items:
|
|
175
|
-
{
|
|
176
|
-
id: "features.preventBrew",
|
|
177
|
-
label: "Prevent Homebrew",
|
|
178
|
-
description: "Block brew commands",
|
|
179
|
-
currentValue:
|
|
180
|
-
(config.features?.preventBrew ??
|
|
181
|
-
resolved.features.preventBrew)
|
|
182
|
-
? "enabled"
|
|
183
|
-
: "disabled",
|
|
184
|
-
values: ["enabled", "disabled"],
|
|
185
|
-
},
|
|
186
|
-
{
|
|
187
|
-
id: "features.protectEnvFiles",
|
|
188
|
-
label: "Protect .env files",
|
|
189
|
-
description: "Block access to .env files containing secrets",
|
|
190
|
-
currentValue:
|
|
191
|
-
(config.features?.protectEnvFiles ??
|
|
192
|
-
resolved.features.protectEnvFiles)
|
|
193
|
-
? "enabled"
|
|
194
|
-
: "disabled",
|
|
195
|
-
values: ["enabled", "disabled"],
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
id: "features.permissionGate",
|
|
199
|
-
label: "Permission gate",
|
|
200
|
-
description:
|
|
201
|
-
"Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
|
|
202
|
-
currentValue:
|
|
203
|
-
(config.features?.permissionGate ??
|
|
204
|
-
resolved.features.permissionGate)
|
|
205
|
-
? "enabled"
|
|
206
|
-
: "disabled",
|
|
207
|
-
values: ["enabled", "disabled"],
|
|
208
|
-
},
|
|
209
|
-
],
|
|
224
|
+
items: featureItems,
|
|
210
225
|
},
|
|
211
226
|
{
|
|
212
227
|
label: "Env Files",
|
|
@@ -318,6 +333,21 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
318
333
|
},
|
|
319
334
|
],
|
|
320
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
|
+
},
|
|
321
351
|
];
|
|
322
352
|
|
|
323
353
|
return new SectionedSettings(
|
|
@@ -345,7 +375,7 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
345
375
|
: configLoader.getGlobalConfig();
|
|
346
376
|
const updated: GuardrailsConfig = structuredClone(config);
|
|
347
377
|
|
|
348
|
-
// Boolean toggles
|
|
378
|
+
// Boolean toggles
|
|
349
379
|
if (
|
|
350
380
|
newValue === "enabled" ||
|
|
351
381
|
newValue === "disabled" ||
|
|
@@ -355,6 +385,18 @@ export function registerSettingsCommand(pi: ExtensionAPI): void {
|
|
|
355
385
|
const boolVal = newValue === "enabled" || newValue === "on";
|
|
356
386
|
setNestedValue(updated, id, boolVal);
|
|
357
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
|
+
|
|
358
400
|
const ok = await saveTabConfig(tab, updated);
|
|
359
401
|
if (ok) {
|
|
360
402
|
settings = buildSettings(activeTab);
|