@aliou/pi-guardrails 0.5.3 → 0.6.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 +63 -94
- package/config-schema.ts +37 -25
- package/config.ts +108 -141
- package/events.ts +1 -6
- package/glob-expander.ts +128 -0
- package/hooks/index.ts +0 -6
- package/hooks/permission-gate.ts +243 -142
- package/hooks/protect-env-files.ts +120 -43
- package/index.ts +6 -3
- package/matching.ts +119 -0
- package/migration.ts +135 -0
- package/package.json +7 -4
- package/pattern-editor.ts +61 -10
- package/settings-command.ts +247 -426
- package/shell-utils.ts +139 -0
- package/array-editor.ts +0 -213
- package/hooks/enforce-package-manager.ts +0 -96
- package/hooks/prevent-brew.ts +0 -41
- package/hooks/prevent-python.ts +0 -45
- package/sectioned-settings.ts +0 -345
package/README.md
CHANGED
|
@@ -35,11 +35,12 @@ pi install npm:@aliou/pi-guardrails
|
|
|
35
35
|
|
|
36
36
|
## Features
|
|
37
37
|
|
|
38
|
-
- **prevent-brew**: Blocks Homebrew commands (disabled by default)
|
|
39
|
-
- **prevent-python**: Blocks Python/pip/poetry commands, suggests uv instead (disabled by default)
|
|
40
38
|
- **protect-env-files**: Prevents access to `.env` files (except `.example`/`.sample`/`.test`)
|
|
41
39
|
- **permission-gate**: Prompts for confirmation on dangerous commands
|
|
42
|
-
|
|
40
|
+
|
|
41
|
+
All hooks use structural shell parsing via `@aliou/sh` to avoid false positives from keywords inside commit messages, grep patterns, heredocs, or file paths. On parse failure, each hook falls back to regex matching (previous behavior).
|
|
42
|
+
|
|
43
|
+
> **Migration note**: The `preventBrew`, `preventPython`, `enforcePackageManager`, and `packageManager` fields have been removed from guardrails and moved to the [`@aliou/pi-toolchain`](../toolchain) extension. Old configs containing these fields are auto-cleaned on first load with a one-time warning. Install `@aliou/pi-toolchain` and configure `.pi/extensions/toolchain.json` instead.
|
|
43
44
|
|
|
44
45
|
## Configuration
|
|
45
46
|
|
|
@@ -54,28 +55,35 @@ Run `/guardrails:settings` to open an interactive settings UI with two tabs:
|
|
|
54
55
|
- **Local**: edit project-scoped config (`.pi/extensions/guardrails.json`)
|
|
55
56
|
- **Global**: edit global config (`~/.pi/agent/extensions/guardrails.json`)
|
|
56
57
|
|
|
57
|
-
Use `Tab` / `Shift+Tab` to switch tabs. Boolean settings can be toggled directly.
|
|
58
|
+
Use `Tab` / `Shift+Tab` to switch tabs. Boolean settings can be toggled directly.
|
|
59
|
+
|
|
60
|
+
### Migration from v0
|
|
61
|
+
|
|
62
|
+
Configs without a `version` field are automatically migrated on first load. The migration:
|
|
63
|
+
- Backs up the original as `guardrails.v0.json`
|
|
64
|
+
- Converts all string patterns to `{ pattern, regex: true }` to preserve behavior
|
|
65
|
+
- Adds a `version` field
|
|
58
66
|
|
|
59
67
|
### Configuration Schema
|
|
60
68
|
|
|
61
69
|
```json
|
|
62
70
|
{
|
|
71
|
+
"version": "0.7.0-20260204",
|
|
63
72
|
"enabled": true,
|
|
64
73
|
"features": {
|
|
65
|
-
"preventBrew": false,
|
|
66
|
-
"preventPython": false,
|
|
67
74
|
"protectEnvFiles": true,
|
|
68
|
-
"permissionGate": true
|
|
69
|
-
"enforcePackageManager": false
|
|
70
|
-
},
|
|
71
|
-
"packageManager": {
|
|
72
|
-
"selected": "npm"
|
|
75
|
+
"permissionGate": true
|
|
73
76
|
},
|
|
74
77
|
"envFiles": {
|
|
75
|
-
"protectedPatterns": [
|
|
78
|
+
"protectedPatterns": [
|
|
79
|
+
{ "pattern": ".env" },
|
|
80
|
+
{ "pattern": ".env.local" },
|
|
81
|
+
{ "pattern": ".env.production" }
|
|
82
|
+
],
|
|
76
83
|
"allowedPatterns": [
|
|
77
|
-
"
|
|
78
|
-
"
|
|
84
|
+
{ "pattern": ".env.example" },
|
|
85
|
+
{ "pattern": ".env.sample" },
|
|
86
|
+
{ "pattern": "*.example.env" }
|
|
79
87
|
],
|
|
80
88
|
"protectedDirectories": [],
|
|
81
89
|
"protectedTools": ["read", "write", "edit", "bash", "grep", "find", "ls"],
|
|
@@ -84,7 +92,8 @@ Use `Tab` / `Shift+Tab` to switch tabs. Boolean settings can be toggled directly
|
|
|
84
92
|
},
|
|
85
93
|
"permissionGate": {
|
|
86
94
|
"patterns": [
|
|
87
|
-
{ "pattern": "rm
|
|
95
|
+
{ "pattern": "rm -rf", "description": "recursive force delete" },
|
|
96
|
+
{ "pattern": "sudo", "description": "superuser command" }
|
|
88
97
|
],
|
|
89
98
|
"customPatterns": [],
|
|
90
99
|
"requireConfirmation": true,
|
|
@@ -96,31 +105,36 @@ Use `Tab` / `Shift+Tab` to switch tabs. Boolean settings can be toggled directly
|
|
|
96
105
|
|
|
97
106
|
All fields are optional. Missing fields use defaults shown above.
|
|
98
107
|
|
|
108
|
+
### Pattern Format
|
|
109
|
+
|
|
110
|
+
Patterns support two modes controlled by the `regex` flag:
|
|
111
|
+
|
|
112
|
+
**File patterns** (envFiles section):
|
|
113
|
+
- Default (`regex` omitted or `false`): glob matching against the filename. `*` matches any non-`/` chars, `?` matches a single char. Example: `.env.*` matches `.env.local`, `.env.production`.
|
|
114
|
+
- `regex: true`: full regex (case-insensitive) against the full path. Example: `{ "pattern": "\\.env$", "regex": true }`.
|
|
115
|
+
|
|
116
|
+
**Command patterns** (permissionGate section):
|
|
117
|
+
- Default (`regex` omitted or `false`): substring matching against the raw command string. Example: `"rm -rf"` matches any command containing `rm -rf`.
|
|
118
|
+
- `regex: true`: full regex against the raw command string. Example: `{ "pattern": "rm\\s+-rf", "regex": true }`.
|
|
119
|
+
|
|
120
|
+
Built-in dangerous command patterns (`rm -rf`, `sudo`, `dd if=`, `mkfs.*`, `chmod -R 777`, `chown -R`) are matched structurally via AST parsing, independent of the pattern format.
|
|
121
|
+
|
|
99
122
|
### Configuration Details
|
|
100
123
|
|
|
101
124
|
#### `features`
|
|
102
125
|
|
|
103
126
|
| Key | Default | Description |
|
|
104
127
|
|---|---|---|
|
|
105
|
-
| `preventBrew` | `false` | Block Homebrew install/upgrade commands |
|
|
106
|
-
| `preventPython` | `false` | Block python/pip/poetry commands (use uv instead) |
|
|
107
128
|
| `protectEnvFiles` | `true` | Block access to `.env` files containing secrets |
|
|
108
129
|
| `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"` |
|
|
116
130
|
|
|
117
131
|
#### `envFiles`
|
|
118
132
|
|
|
119
133
|
| Key | Default | Description |
|
|
120
134
|
|---|---|---|
|
|
121
|
-
| `protectedPatterns` | `["
|
|
122
|
-
| `allowedPatterns` | `["
|
|
123
|
-
| `protectedDirectories` | `[]` |
|
|
135
|
+
| `protectedPatterns` | `[".env", ".env.local", ...]` | Patterns for files to protect (glob by default) |
|
|
136
|
+
| `allowedPatterns` | `[".env.example", "*.example.env", ...]` | Patterns for allowed exceptions |
|
|
137
|
+
| `protectedDirectories` | `[]` | Patterns for directories to protect |
|
|
124
138
|
| `protectedTools` | `["read", "write", "edit", "bash", "grep", "find", "ls"]` | Tools to intercept |
|
|
125
139
|
| `onlyBlockIfExists` | `true` | Only block if the file exists on disk |
|
|
126
140
|
| `blockMessage` | See defaults | Message shown when blocked. Supports `{file}` placeholder |
|
|
@@ -132,54 +146,47 @@ All fields are optional. Missing fields use defaults shown above.
|
|
|
132
146
|
| `patterns` | See defaults | Array of `{ pattern, description }` for dangerous commands |
|
|
133
147
|
| `customPatterns` | Not set | If set, replaces `patterns` entirely |
|
|
134
148
|
| `requireConfirmation` | `true` | Show confirmation dialog (if `false`, just warns) |
|
|
135
|
-
| `allowedPatterns` | `[]` |
|
|
136
|
-
| `autoDenyPatterns` | `[]` |
|
|
149
|
+
| `allowedPatterns` | `[]` | Patterns that bypass the gate |
|
|
150
|
+
| `autoDenyPatterns` | `[]` | Patterns that are blocked immediately without dialog |
|
|
137
151
|
|
|
138
152
|
### Examples
|
|
139
153
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
```json
|
|
143
|
-
{
|
|
144
|
-
"features": {
|
|
145
|
-
"preventBrew": true
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
Add a custom dangerous command pattern:
|
|
154
|
+
Add a custom dangerous command pattern (substring match):
|
|
151
155
|
|
|
152
156
|
```json
|
|
153
157
|
{
|
|
154
158
|
"permissionGate": {
|
|
155
159
|
"patterns": [
|
|
156
|
-
{ "pattern": "rm
|
|
157
|
-
{ "pattern": "
|
|
158
|
-
{ "pattern": "docker
|
|
160
|
+
{ "pattern": "rm -rf", "description": "recursive force delete" },
|
|
161
|
+
{ "pattern": "sudo", "description": "superuser command" },
|
|
162
|
+
{ "pattern": "docker system prune", "description": "docker system prune" }
|
|
159
163
|
]
|
|
160
164
|
}
|
|
161
165
|
}
|
|
162
166
|
```
|
|
163
167
|
|
|
164
|
-
|
|
168
|
+
Add a regex-based pattern:
|
|
165
169
|
|
|
166
170
|
```json
|
|
167
171
|
{
|
|
168
172
|
"permissionGate": {
|
|
169
|
-
"
|
|
173
|
+
"patterns": [
|
|
174
|
+
{ "pattern": "rm\\s+-rf\\s+/(?!tmp)", "description": "rm -rf outside /tmp", "regex": true }
|
|
175
|
+
]
|
|
170
176
|
}
|
|
171
177
|
}
|
|
172
178
|
```
|
|
173
179
|
|
|
174
|
-
|
|
180
|
+
Protect env files with glob patterns:
|
|
175
181
|
|
|
176
182
|
```json
|
|
177
183
|
{
|
|
178
|
-
"
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
"envFiles": {
|
|
185
|
+
"protectedPatterns": [
|
|
186
|
+
{ "pattern": ".env" },
|
|
187
|
+
{ "pattern": ".env.*" },
|
|
188
|
+
{ "pattern": ".dev.vars" }
|
|
189
|
+
]
|
|
183
190
|
}
|
|
184
191
|
}
|
|
185
192
|
```
|
|
@@ -194,7 +201,7 @@ Emitted when a tool call is blocked by any guardrail.
|
|
|
194
201
|
|
|
195
202
|
```typescript
|
|
196
203
|
interface GuardrailsBlockedEvent {
|
|
197
|
-
feature: "
|
|
204
|
+
feature: "protectEnvFiles" | "permissionGate";
|
|
198
205
|
toolName: string;
|
|
199
206
|
input: Record<string, unknown>;
|
|
200
207
|
reason: string;
|
|
@@ -218,37 +225,11 @@ The [presenter extension](../presenter) listens for `guardrails:dangerous` event
|
|
|
218
225
|
|
|
219
226
|
## Hooks
|
|
220
227
|
|
|
221
|
-
### prevent-brew
|
|
222
|
-
|
|
223
|
-
Blocks bash commands that attempt to install packages using Homebrew. Disabled by default. Enable via config if your project uses Nix.
|
|
224
|
-
|
|
225
|
-
Blocked patterns:
|
|
226
|
-
- `brew install`
|
|
227
|
-
- `brew cask install`
|
|
228
|
-
- `brew bundle`
|
|
229
|
-
- `brew upgrade`
|
|
230
|
-
- `brew reinstall`
|
|
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
|
-
|
|
243
228
|
### protect-env-files
|
|
244
229
|
|
|
245
|
-
Prevents accessing `.env` files that might contain secrets. Only allows access to safe variants
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
- `.env.test`
|
|
249
|
-
- `*.example.env`
|
|
250
|
-
- `*.sample.env`
|
|
251
|
-
- `*.test.env`
|
|
230
|
+
Prevents accessing `.env` files that might contain secrets. Only allows access to safe variants like `.env.example`, `.env.sample`, `.env.test`.
|
|
231
|
+
|
|
232
|
+
Shell globs (e.g. `.env*`) are expanded via `fd` to check if any expanded path matches a protected pattern.
|
|
252
233
|
|
|
253
234
|
Covers tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls` (configurable).
|
|
254
235
|
|
|
@@ -257,21 +238,9 @@ Covers tools: `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls` (configurabl
|
|
|
257
238
|
Prompts user confirmation before executing dangerous commands:
|
|
258
239
|
- `rm -rf` (recursive force delete)
|
|
259
240
|
- `sudo` (superuser command)
|
|
260
|
-
- `: | sh` (piped shell execution)
|
|
261
241
|
- `dd if=` (disk write operation)
|
|
262
242
|
- `mkfs.` (filesystem format)
|
|
263
243
|
- `chmod -R 777` (insecure recursive permissions)
|
|
264
244
|
- `chown -R` (recursive ownership change)
|
|
265
245
|
|
|
266
|
-
|
|
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.
|
|
246
|
+
Built-in patterns are matched structurally (AST-based). Custom patterns use substring or regex matching. Supports allow-lists and auto-deny lists.
|
package/config-schema.ts
CHANGED
|
@@ -5,60 +5,72 @@
|
|
|
5
5
|
* ResolvedConfig is the internal schema (all fields required, defaults applied).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* A pattern with explicit matching mode.
|
|
10
|
+
* Default: glob for files, substring for commands.
|
|
11
|
+
* regex: true means full regex matching.
|
|
12
|
+
*/
|
|
13
|
+
export interface PatternConfig {
|
|
14
|
+
pattern: string;
|
|
15
|
+
regex?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Permission gate pattern. When regex is false (default), the pattern
|
|
20
|
+
* is matched as substring against the raw command string.
|
|
21
|
+
* When regex is true, uses full regex against the raw string.
|
|
22
|
+
*/
|
|
23
|
+
export interface DangerousPattern extends PatternConfig {
|
|
24
|
+
description: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
8
27
|
export interface GuardrailsConfig {
|
|
28
|
+
version?: string;
|
|
9
29
|
enabled?: boolean;
|
|
10
30
|
features?: {
|
|
11
|
-
preventBrew?: boolean;
|
|
12
|
-
preventPython?: boolean;
|
|
13
31
|
protectEnvFiles?: boolean;
|
|
14
32
|
permissionGate?: boolean;
|
|
15
|
-
enforcePackageManager?: boolean;
|
|
16
|
-
};
|
|
17
|
-
packageManager?: {
|
|
18
|
-
selected?: "bun" | "pnpm" | "npm";
|
|
19
33
|
};
|
|
20
34
|
envFiles?: {
|
|
21
|
-
protectedPatterns?:
|
|
22
|
-
allowedPatterns?:
|
|
23
|
-
protectedDirectories?:
|
|
35
|
+
protectedPatterns?: PatternConfig[];
|
|
36
|
+
allowedPatterns?: PatternConfig[];
|
|
37
|
+
protectedDirectories?: PatternConfig[];
|
|
24
38
|
protectedTools?: string[];
|
|
25
39
|
onlyBlockIfExists?: boolean;
|
|
26
40
|
blockMessage?: string;
|
|
27
41
|
};
|
|
28
42
|
permissionGate?: {
|
|
29
|
-
patterns?:
|
|
43
|
+
patterns?: DangerousPattern[];
|
|
30
44
|
/** If set, replaces the default patterns entirely. */
|
|
31
|
-
customPatterns?:
|
|
45
|
+
customPatterns?: DangerousPattern[];
|
|
32
46
|
requireConfirmation?: boolean;
|
|
33
|
-
allowedPatterns?:
|
|
34
|
-
autoDenyPatterns?:
|
|
47
|
+
allowedPatterns?: PatternConfig[];
|
|
48
|
+
autoDenyPatterns?: PatternConfig[];
|
|
35
49
|
};
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
export interface ResolvedConfig {
|
|
53
|
+
version: string;
|
|
39
54
|
enabled: boolean;
|
|
40
55
|
features: {
|
|
41
|
-
preventBrew: boolean;
|
|
42
|
-
preventPython: boolean;
|
|
43
56
|
protectEnvFiles: boolean;
|
|
44
57
|
permissionGate: boolean;
|
|
45
|
-
enforcePackageManager: boolean;
|
|
46
|
-
};
|
|
47
|
-
packageManager: {
|
|
48
|
-
selected: "bun" | "pnpm" | "npm";
|
|
49
58
|
};
|
|
50
59
|
envFiles: {
|
|
51
|
-
protectedPatterns:
|
|
52
|
-
allowedPatterns:
|
|
53
|
-
protectedDirectories:
|
|
60
|
+
protectedPatterns: PatternConfig[];
|
|
61
|
+
allowedPatterns: PatternConfig[];
|
|
62
|
+
protectedDirectories: PatternConfig[];
|
|
54
63
|
protectedTools: string[];
|
|
55
64
|
onlyBlockIfExists: boolean;
|
|
56
65
|
blockMessage: string;
|
|
57
66
|
};
|
|
58
67
|
permissionGate: {
|
|
59
|
-
patterns:
|
|
68
|
+
patterns: DangerousPattern[];
|
|
69
|
+
/** When true, use hardcoded structural matchers for built-in patterns.
|
|
70
|
+
* Set to false when customPatterns replaces the defaults. */
|
|
71
|
+
useBuiltinMatchers: boolean;
|
|
60
72
|
requireConfirmation: boolean;
|
|
61
|
-
allowedPatterns:
|
|
62
|
-
autoDenyPatterns:
|
|
73
|
+
allowedPatterns: PatternConfig[];
|
|
74
|
+
autoDenyPatterns: PatternConfig[];
|
|
63
75
|
};
|
|
64
76
|
}
|
package/config.ts
CHANGED
|
@@ -1,40 +1,98 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
1
|
+
import { ConfigLoader, type Migration } from "@aliou/pi-utils-settings";
|
|
4
2
|
import type { GuardrailsConfig, ResolvedConfig } from "./config-schema";
|
|
3
|
+
import {
|
|
4
|
+
backupConfig,
|
|
5
|
+
CURRENT_VERSION,
|
|
6
|
+
migrateV0,
|
|
7
|
+
needsMigration,
|
|
8
|
+
} from "./migration";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Config fields removed in the toolchain extraction.
|
|
12
|
+
* Old configs containing these are auto-cleaned on first load.
|
|
13
|
+
*/
|
|
14
|
+
const REMOVED_FEATURE_KEYS = [
|
|
15
|
+
"preventBrew",
|
|
16
|
+
"preventPython",
|
|
17
|
+
"enforcePackageManager",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
const TOOLCHAIN_MIGRATION_VERSION = "0.7.0-20260204";
|
|
21
|
+
|
|
22
|
+
function hasRemovedFields(config: GuardrailsConfig): boolean {
|
|
23
|
+
const raw = config as Record<string, unknown>;
|
|
24
|
+
const features = raw.features as Record<string, unknown> | undefined;
|
|
25
|
+
if (features) {
|
|
26
|
+
for (const key of REMOVED_FEATURE_KEYS) {
|
|
27
|
+
if (key in features) return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return "packageManager" in raw;
|
|
31
|
+
}
|
|
5
32
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
)
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
function stripRemovedFields(config: GuardrailsConfig): GuardrailsConfig {
|
|
34
|
+
const cleaned = structuredClone(config) as Record<string, unknown>;
|
|
35
|
+
const features = cleaned.features as Record<string, unknown> | undefined;
|
|
36
|
+
if (features) {
|
|
37
|
+
for (const key of REMOVED_FEATURE_KEYS) {
|
|
38
|
+
delete features[key];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
delete cleaned.packageManager;
|
|
42
|
+
cleaned.version = TOOLCHAIN_MIGRATION_VERSION;
|
|
43
|
+
return cleaned as GuardrailsConfig;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const migrations: Migration<GuardrailsConfig>[] = [
|
|
47
|
+
{
|
|
48
|
+
name: "v0-format-upgrade",
|
|
49
|
+
shouldRun: (config) => needsMigration(config),
|
|
50
|
+
run: async (config, filePath) => {
|
|
51
|
+
await backupConfig(filePath);
|
|
52
|
+
return migrateV0(config);
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: "strip-toolchain-fields",
|
|
57
|
+
shouldRun: (config) => hasRemovedFields(config),
|
|
58
|
+
run: (config) => {
|
|
59
|
+
const version = (config as Record<string, unknown>).version as
|
|
60
|
+
| string
|
|
61
|
+
| undefined;
|
|
62
|
+
if (!version || version < TOOLCHAIN_MIGRATION_VERSION) {
|
|
63
|
+
console.error(
|
|
64
|
+
"[guardrails] preventBrew, preventPython, enforcePackageManager, and packageManager " +
|
|
65
|
+
"have been removed from guardrails and moved to @aliou/pi-toolchain. " +
|
|
66
|
+
"These fields will be stripped from your config.",
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return stripRemovedFields(config);
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
];
|
|
14
73
|
|
|
15
74
|
const DEFAULT_CONFIG: ResolvedConfig = {
|
|
75
|
+
version: CURRENT_VERSION,
|
|
16
76
|
enabled: true,
|
|
17
77
|
features: {
|
|
18
|
-
preventBrew: false,
|
|
19
|
-
preventPython: false,
|
|
20
78
|
protectEnvFiles: true,
|
|
21
79
|
permissionGate: true,
|
|
22
|
-
enforcePackageManager: false,
|
|
23
|
-
},
|
|
24
|
-
packageManager: {
|
|
25
|
-
selected: "npm",
|
|
26
80
|
},
|
|
27
81
|
envFiles: {
|
|
28
82
|
protectedPatterns: [
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
83
|
+
{ pattern: ".env" },
|
|
84
|
+
{ pattern: ".env.local" },
|
|
85
|
+
{ pattern: ".env.production" },
|
|
86
|
+
{ pattern: ".env.prod" },
|
|
87
|
+
{ pattern: ".dev.vars" },
|
|
34
88
|
],
|
|
35
89
|
allowedPatterns: [
|
|
36
|
-
"
|
|
37
|
-
"
|
|
90
|
+
{ pattern: "*.example.env" },
|
|
91
|
+
{ pattern: "*.sample.env" },
|
|
92
|
+
{ pattern: "*.test.env" },
|
|
93
|
+
{ pattern: ".env.example" },
|
|
94
|
+
{ pattern: ".env.sample" },
|
|
95
|
+
{ pattern: ".env.test" },
|
|
38
96
|
],
|
|
39
97
|
protectedDirectories: [],
|
|
40
98
|
protectedTools: ["read", "write", "edit", "bash", "grep", "find", "ls"],
|
|
@@ -46,131 +104,40 @@ const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
46
104
|
},
|
|
47
105
|
permissionGate: {
|
|
48
106
|
patterns: [
|
|
49
|
-
{ pattern: "rm
|
|
50
|
-
{ pattern: "
|
|
51
|
-
{ pattern: "
|
|
52
|
-
{ pattern: "
|
|
53
|
-
{ pattern: "mkfs\\.", description: "filesystem format" },
|
|
107
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
108
|
+
{ pattern: "sudo", description: "superuser command" },
|
|
109
|
+
{ pattern: "dd if=", description: "disk write operation" },
|
|
110
|
+
{ pattern: "mkfs.", description: "filesystem format" },
|
|
54
111
|
{
|
|
55
|
-
pattern: "
|
|
112
|
+
pattern: "chmod -R 777",
|
|
56
113
|
description: "insecure recursive permissions",
|
|
57
114
|
},
|
|
58
|
-
{
|
|
59
|
-
pattern: "\\bchown\\s+-R",
|
|
60
|
-
description: "recursive ownership change",
|
|
61
|
-
},
|
|
115
|
+
{ pattern: "chown -R", description: "recursive ownership change" },
|
|
62
116
|
],
|
|
117
|
+
useBuiltinMatchers: true,
|
|
63
118
|
requireConfirmation: true,
|
|
64
119
|
allowedPatterns: [],
|
|
65
120
|
autoDenyPatterns: [],
|
|
66
121
|
},
|
|
67
122
|
};
|
|
68
123
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} catch {
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
private mergeConfigs(): ResolvedConfig {
|
|
90
|
-
const merged = structuredClone(DEFAULT_CONFIG);
|
|
91
|
-
|
|
92
|
-
if (this.globalConfig) {
|
|
93
|
-
this.mergeInto(merged, this.globalConfig);
|
|
94
|
-
}
|
|
95
|
-
if (this.projectConfig) {
|
|
96
|
-
this.mergeInto(merged, this.projectConfig);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// customPatterns replaces entire patterns array
|
|
100
|
-
if (this.projectConfig?.permissionGate?.customPatterns) {
|
|
101
|
-
merged.permissionGate.patterns =
|
|
102
|
-
this.projectConfig.permissionGate.customPatterns;
|
|
103
|
-
} else if (this.globalConfig?.permissionGate?.customPatterns) {
|
|
104
|
-
merged.permissionGate.patterns =
|
|
105
|
-
this.globalConfig.permissionGate.customPatterns;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return merged;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
private mergeInto<TTarget extends object, TSource extends object>(
|
|
112
|
-
target: TTarget,
|
|
113
|
-
source: TSource,
|
|
114
|
-
): void {
|
|
115
|
-
const t = target as Record<string, unknown>;
|
|
116
|
-
const s = source as Record<string, unknown>;
|
|
117
|
-
|
|
118
|
-
for (const key in s) {
|
|
119
|
-
if (s[key] === undefined) continue;
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
typeof s[key] === "object" &&
|
|
123
|
-
!Array.isArray(s[key]) &&
|
|
124
|
-
s[key] !== null
|
|
125
|
-
) {
|
|
126
|
-
if (!t[key]) t[key] = {};
|
|
127
|
-
this.mergeInto(t[key] as object, s[key] as object);
|
|
128
|
-
} else {
|
|
129
|
-
t[key] = s[key];
|
|
124
|
+
export const configLoader = new ConfigLoader<GuardrailsConfig, ResolvedConfig>(
|
|
125
|
+
"guardrails",
|
|
126
|
+
DEFAULT_CONFIG,
|
|
127
|
+
{
|
|
128
|
+
migrations,
|
|
129
|
+
afterMerge: (resolved, global, project) => {
|
|
130
|
+
// customPatterns replaces the entire patterns array and disables
|
|
131
|
+
// built-in structural matchers (user owns all matching).
|
|
132
|
+
if (project?.permissionGate?.customPatterns) {
|
|
133
|
+
resolved.permissionGate.patterns =
|
|
134
|
+
project.permissionGate.customPatterns;
|
|
135
|
+
resolved.permissionGate.useBuiltinMatchers = false;
|
|
136
|
+
} else if (global?.permissionGate?.customPatterns) {
|
|
137
|
+
resolved.permissionGate.patterns = global.permissionGate.customPatterns;
|
|
138
|
+
resolved.permissionGate.useBuiltinMatchers = false;
|
|
130
139
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (!this.resolved) {
|
|
136
|
-
throw new Error("Config not loaded. Call load() first.");
|
|
137
|
-
}
|
|
138
|
-
return this.resolved;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
async saveGlobal(config: GuardrailsConfig): Promise<void> {
|
|
142
|
-
await this.saveConfigFile(GLOBAL_CONFIG_PATH, config);
|
|
143
|
-
await this.load();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
async saveProject(config: GuardrailsConfig): Promise<void> {
|
|
147
|
-
await this.saveConfigFile(PROJECT_CONFIG_PATH, config);
|
|
148
|
-
await this.load();
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
private async saveConfigFile(
|
|
152
|
-
path: string,
|
|
153
|
-
config: GuardrailsConfig,
|
|
154
|
-
): Promise<void> {
|
|
155
|
-
await mkdir(dirname(path), { recursive: true });
|
|
156
|
-
await writeFile(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
hasGlobalConfig(): boolean {
|
|
160
|
-
return this.globalConfig !== null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
hasProjectConfig(): boolean {
|
|
164
|
-
return this.projectConfig !== null;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
getGlobalConfig(): GuardrailsConfig {
|
|
168
|
-
return this.globalConfig ?? {};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
getProjectConfig(): GuardrailsConfig {
|
|
172
|
-
return this.projectConfig ?? {};
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
export const configLoader = new ConfigLoader();
|
|
140
|
+
return resolved;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
);
|
package/events.ts
CHANGED
|
@@ -4,12 +4,7 @@ export const GUARDRAILS_BLOCKED_EVENT = "guardrails:blocked";
|
|
|
4
4
|
export const GUARDRAILS_DANGEROUS_EVENT = "guardrails:dangerous";
|
|
5
5
|
|
|
6
6
|
export interface GuardrailsBlockedEvent {
|
|
7
|
-
feature:
|
|
8
|
-
| "preventBrew"
|
|
9
|
-
| "preventPython"
|
|
10
|
-
| "protectEnvFiles"
|
|
11
|
-
| "permissionGate"
|
|
12
|
-
| "enforcePackageManager";
|
|
7
|
+
feature: "protectEnvFiles" | "permissionGate";
|
|
13
8
|
toolName: string;
|
|
14
9
|
input: Record<string, unknown>;
|
|
15
10
|
reason: string;
|