@aliou/pi-guardrails 0.10.0 → 0.11.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 +46 -3
- package/docs/defaults.md +26 -1
- package/package.json +5 -2
- package/src/commands/onboarding-command.ts +19 -2
- package/src/commands/onboarding.ts +122 -6
- package/src/commands/settings-command.ts +29 -0
- package/src/config.ts +44 -1
- package/src/hooks/index.ts +4 -2
- package/src/hooks/path-access.ts +396 -0
- package/src/hooks/permission-gate/dangerous-commands.test.ts +336 -0
- package/src/hooks/permission-gate/dangerous-commands.ts +345 -0
- package/src/hooks/permission-gate/index.test.ts +332 -0
- package/src/hooks/{permission-gate.ts → permission-gate/index.ts} +35 -62
- package/src/utils/bash-paths.test.ts +91 -0
- package/src/utils/bash-paths.ts +96 -0
- package/src/utils/events.ts +1 -1
- package/src/utils/path-access.test.ts +154 -0
- package/src/utils/path-access.ts +62 -0
- package/src/utils/path.test.ts +177 -0
- package/src/utils/path.ts +63 -7
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
1
3
|
# Guardrails
|
|
2
4
|
|
|
3
5
|
Security hooks for Pi to reduce accidental destructive actions and secret-file access.
|
|
@@ -27,6 +29,7 @@ pi install git:github.com/aliou/pi-guardrails
|
|
|
27
29
|
|
|
28
30
|
- **policies**: named file-protection rules with per-rule protection levels.
|
|
29
31
|
- **permission-gate**: detects dangerous bash commands and asks for confirmation.
|
|
32
|
+
- **path-access**: restricts tool access to the current working directory with allow/ask/block modes.
|
|
30
33
|
- **optional command explainer**: can call a small LLM to explain a dangerous command inline in the confirmation dialog.
|
|
31
34
|
|
|
32
35
|
## Config locations
|
|
@@ -48,7 +51,12 @@ Use `/guardrails:settings` to edit config interactively.
|
|
|
48
51
|
"enabled": true,
|
|
49
52
|
"features": {
|
|
50
53
|
"policies": true,
|
|
51
|
-
"permissionGate": true
|
|
54
|
+
"permissionGate": true,
|
|
55
|
+
"pathAccess": false
|
|
56
|
+
},
|
|
57
|
+
"pathAccess": {
|
|
58
|
+
"mode": "ask",
|
|
59
|
+
"allowedPaths": []
|
|
52
60
|
},
|
|
53
61
|
"policies": {
|
|
54
62
|
"rules": [
|
|
@@ -121,6 +129,31 @@ Use:
|
|
|
121
129
|
|
|
122
130
|
This starts a subagent that helps build and save one policy rule.
|
|
123
131
|
|
|
132
|
+
## Path access
|
|
133
|
+
|
|
134
|
+
Restrict tool access to the current working directory. When enabled, any tool call targeting a path outside `cwd` is checked against the configured mode:
|
|
135
|
+
|
|
136
|
+
- **allow**: no restrictions
|
|
137
|
+
- **ask**: prompt with options to grant access (file or directory, for session or always)
|
|
138
|
+
- **block**: deny all outside access
|
|
139
|
+
|
|
140
|
+
```jsonc
|
|
141
|
+
{
|
|
142
|
+
"features": { "pathAccess": true },
|
|
143
|
+
"pathAccess": {
|
|
144
|
+
"mode": "ask",
|
|
145
|
+
"allowedPaths": ["~/code/shared-libs/", "~/.config/myapp"]
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Grants are stored in project config (always) or session memory (session). The `allowedPaths` array is merged across all config scopes.
|
|
151
|
+
|
|
152
|
+
Limitations:
|
|
153
|
+
- Symlinks are not resolved (lexical path comparison only).
|
|
154
|
+
- Bash path extraction is best-effort (AST-based heuristics).
|
|
155
|
+
- In non-interactive mode, `ask` mode degrades to `block`.
|
|
156
|
+
|
|
124
157
|
## Permission gate
|
|
125
158
|
|
|
126
159
|
Detects dangerous bash commands and prompts user confirmation.
|
|
@@ -161,6 +194,16 @@ Also note:
|
|
|
161
194
|
|
|
162
195
|
- `preventBrew`, `preventPython`, `enforcePackageManager`, `packageManager` were removed from guardrails and moved to `@aliou/pi-toolchain`.
|
|
163
196
|
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
pnpm test # Run tests
|
|
201
|
+
pnpm test:watch # Run tests in watch mode
|
|
202
|
+
pnpm typecheck # Type check
|
|
203
|
+
pnpm lint # Lint
|
|
204
|
+
pnpm format # Format
|
|
205
|
+
```
|
|
206
|
+
|
|
164
207
|
## Events
|
|
165
208
|
|
|
166
209
|
Guardrails emits events for other extensions:
|
|
@@ -169,7 +212,7 @@ Guardrails emits events for other extensions:
|
|
|
169
212
|
|
|
170
213
|
```ts
|
|
171
214
|
interface GuardrailsBlockedEvent {
|
|
172
|
-
feature: "policies" | "permissionGate";
|
|
215
|
+
feature: "policies" | "permissionGate" | "pathAccess";
|
|
173
216
|
toolName: string;
|
|
174
217
|
input: Record<string, unknown>;
|
|
175
218
|
reason: string;
|
|
@@ -185,4 +228,4 @@ interface GuardrailsDangerousEvent {
|
|
|
185
228
|
description: string;
|
|
186
229
|
pattern: string;
|
|
187
230
|
}
|
|
188
|
-
```
|
|
231
|
+
```
|
package/docs/defaults.md
CHANGED
|
@@ -101,6 +101,31 @@ Blocks access to GPG/GnuPG private keys, keyrings, and configuration. Disabled b
|
|
|
101
101
|
|
|
102
102
|
---
|
|
103
103
|
|
|
104
|
+
## Path Access
|
|
105
|
+
|
|
106
|
+
| Setting | Default |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `features.pathAccess` | `false` |
|
|
109
|
+
| `pathAccess.mode` | `"ask"` |
|
|
110
|
+
| `pathAccess.allowedPaths` | `[]` |
|
|
111
|
+
|
|
112
|
+
Modes:
|
|
113
|
+
- `allow` — no path restrictions
|
|
114
|
+
- `ask` — prompt when accessing paths outside working directory
|
|
115
|
+
- `block` — deny all access outside working directory
|
|
116
|
+
|
|
117
|
+
Allowed paths use trailing-slash convention:
|
|
118
|
+
- `/path/to/file` — exact file match
|
|
119
|
+
- `/path/to/dir/` — directory and all descendants
|
|
120
|
+
- Supports `~/` for home directory
|
|
121
|
+
|
|
122
|
+
Limitations:
|
|
123
|
+
- Bash path extraction is best-effort (AST-based heuristics). Tokens like `application/json` may trigger false-positive prompts.
|
|
124
|
+
- Symlinks are not resolved. Lexical path comparison only.
|
|
125
|
+
- In non-interactive mode (--print), `ask` mode degrades to `block`.
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
104
129
|
## Default Permission Gate Patterns
|
|
105
130
|
|
|
106
131
|
These commands are detected using AST-based structural matching for accuracy.
|
|
@@ -109,7 +134,7 @@ These commands are detected using AST-based structural matching for accuracy.
|
|
|
109
134
|
|-----------------|--------------------------------|
|
|
110
135
|
| `rm -rf` | Recursive force delete |
|
|
111
136
|
| `sudo` | Superuser command |
|
|
112
|
-
| `dd
|
|
137
|
+
| `dd of=` | Disk write operation |
|
|
113
138
|
| `mkfs.` | Filesystem format |
|
|
114
139
|
| `chmod -R 777` | Insecure recursive permissions |
|
|
115
140
|
| `chown -R` | Recursive ownership change |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-guardrails",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -49,7 +49,8 @@
|
|
|
49
49
|
"@sinclair/typebox": "^0.34.48",
|
|
50
50
|
"@types/node": "^25.0.10",
|
|
51
51
|
"husky": "^9.1.7",
|
|
52
|
-
"typescript": "^5.9.3"
|
|
52
|
+
"typescript": "^5.9.3",
|
|
53
|
+
"vitest": "^4.1.4"
|
|
53
54
|
},
|
|
54
55
|
"peerDependenciesMeta": {
|
|
55
56
|
"@mariozechner/pi-agent-core": {
|
|
@@ -67,6 +68,8 @@
|
|
|
67
68
|
},
|
|
68
69
|
"scripts": {
|
|
69
70
|
"typecheck": "tsc --noEmit",
|
|
71
|
+
"test": "vitest run",
|
|
72
|
+
"test:watch": "vitest",
|
|
70
73
|
"lint": "biome check",
|
|
71
74
|
"format": "biome check --write",
|
|
72
75
|
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
@@ -10,12 +10,25 @@ import {
|
|
|
10
10
|
function mergeOnboarding(
|
|
11
11
|
base: GuardrailsConfig | null,
|
|
12
12
|
applyBuiltinDefaults: boolean,
|
|
13
|
+
pathAccessEnabled?: boolean | null,
|
|
13
14
|
): GuardrailsConfig {
|
|
14
15
|
const next = structuredClone(base ?? {});
|
|
15
|
-
const onboarded = buildOnboardedConfig(
|
|
16
|
+
const onboarded = buildOnboardedConfig(
|
|
17
|
+
applyBuiltinDefaults,
|
|
18
|
+
pathAccessEnabled,
|
|
19
|
+
);
|
|
16
20
|
next.applyBuiltinDefaults = onboarded.applyBuiltinDefaults;
|
|
17
21
|
next.version = onboarded.version;
|
|
18
22
|
next.onboarding = onboarded.onboarding;
|
|
23
|
+
if (onboarded.features?.pathAccess !== undefined) {
|
|
24
|
+
next.features = {
|
|
25
|
+
...next.features,
|
|
26
|
+
pathAccess: onboarded.features.pathAccess,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (onboarded.pathAccess) {
|
|
30
|
+
next.pathAccess = onboarded.pathAccess;
|
|
31
|
+
}
|
|
19
32
|
return next;
|
|
20
33
|
}
|
|
21
34
|
|
|
@@ -48,7 +61,11 @@ export function registerGuardrailsOnboardingCommand(
|
|
|
48
61
|
return;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
const merged = mergeOnboarding(
|
|
64
|
+
const merged = mergeOnboarding(
|
|
65
|
+
globalConfig,
|
|
66
|
+
result.applyBuiltinDefaults,
|
|
67
|
+
result.pathAccessEnabled,
|
|
68
|
+
);
|
|
52
69
|
await configLoader.save("global", merged);
|
|
53
70
|
await configLoader.load();
|
|
54
71
|
|
|
@@ -11,11 +11,13 @@ import { CURRENT_VERSION } from "../utils/migration";
|
|
|
11
11
|
|
|
12
12
|
interface OnboardingState {
|
|
13
13
|
applyBuiltinDefaults: boolean | null;
|
|
14
|
+
pathAccessEnabled: boolean | null;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export interface OnboardingResult {
|
|
17
18
|
completed: boolean;
|
|
18
19
|
applyBuiltinDefaults: boolean | null;
|
|
20
|
+
pathAccessEnabled: boolean | null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
class IntroStep implements Component {
|
|
@@ -68,7 +70,7 @@ class DefaultsChoiceStep implements Component {
|
|
|
68
70
|
"",
|
|
69
71
|
"- Protect secret files like `.env`, `.env.local`, `.env.production`, and `.dev.vars`",
|
|
70
72
|
"- Keep safe exceptions like `.env.example` and `*.sample.env`",
|
|
71
|
-
"- Require confirmation before running dangerous commands like `rm -rf`, `sudo`, and `dd
|
|
73
|
+
"- Require confirmation before running dangerous commands like `rm -rf`, `sudo`, and `dd of=`",
|
|
72
74
|
].join("\n"),
|
|
73
75
|
[
|
|
74
76
|
"Start with no built-in file policy defaults.",
|
|
@@ -129,6 +131,88 @@ class DefaultsChoiceStep implements Component {
|
|
|
129
131
|
}
|
|
130
132
|
}
|
|
131
133
|
|
|
134
|
+
class PathAccessStep implements Component {
|
|
135
|
+
private selectedIndex = 0;
|
|
136
|
+
private readonly settingsTheme: SettingsTheme;
|
|
137
|
+
|
|
138
|
+
constructor(
|
|
139
|
+
private readonly theme: Theme,
|
|
140
|
+
private readonly state: OnboardingState,
|
|
141
|
+
private readonly onSelect: () => void,
|
|
142
|
+
) {
|
|
143
|
+
this.settingsTheme = getSettingsTheme(theme);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
invalidate() {}
|
|
147
|
+
|
|
148
|
+
render(width: number): string[] {
|
|
149
|
+
const options = ["Ask before accessing outside files", "No restrictions"];
|
|
150
|
+
const explanations = [
|
|
151
|
+
[
|
|
152
|
+
"When enabled, guardrails will prompt you before the agent accesses files outside the current working directory.",
|
|
153
|
+
"",
|
|
154
|
+
"- You can grant access per-file or per-directory",
|
|
155
|
+
"- Grants can be session-only or permanent",
|
|
156
|
+
"- In non-interactive mode, outside access is blocked",
|
|
157
|
+
].join("\n"),
|
|
158
|
+
[
|
|
159
|
+
"The agent can access any path on your system without prompting.",
|
|
160
|
+
"",
|
|
161
|
+
"- You can enable path access later in `/guardrails:settings`",
|
|
162
|
+
].join("\n"),
|
|
163
|
+
];
|
|
164
|
+
|
|
165
|
+
const lines: string[] = [
|
|
166
|
+
" Restrict access to your project directory?",
|
|
167
|
+
"",
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
for (let i = 0; i < options.length; i++) {
|
|
171
|
+
const option = options[i];
|
|
172
|
+
if (!option) continue;
|
|
173
|
+
const selected = i === this.selectedIndex;
|
|
174
|
+
const prefix = selected ? this.settingsTheme.cursor : " ";
|
|
175
|
+
const label = this.settingsTheme.value(` ${option}`, selected);
|
|
176
|
+
lines.push(`${prefix}${label}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
lines.push("");
|
|
180
|
+
|
|
181
|
+
const explanationBox = new Box(1, 0, (s: string) => s);
|
|
182
|
+
explanationBox.addChild(
|
|
183
|
+
new Markdown(
|
|
184
|
+
explanations[this.selectedIndex] ?? "",
|
|
185
|
+
0,
|
|
186
|
+
0,
|
|
187
|
+
getMarkdownTheme(),
|
|
188
|
+
{
|
|
189
|
+
color: (s: string) => this.theme.fg("text", s),
|
|
190
|
+
},
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
lines.push(...explanationBox.render(Math.max(1, width)));
|
|
195
|
+
|
|
196
|
+
return lines;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
handleInput(data: string): void {
|
|
200
|
+
if (matchesKey(data, Key.up) || data === "k") {
|
|
201
|
+
this.selectedIndex = this.selectedIndex === 0 ? 1 : 0;
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (matchesKey(data, Key.down) || data === "j") {
|
|
205
|
+
this.selectedIndex = this.selectedIndex === 1 ? 0 : 1;
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (matchesKey(data, Key.enter)) {
|
|
210
|
+
this.state.pathAccessEnabled = this.selectedIndex === 0;
|
|
211
|
+
this.onSelect();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
132
216
|
class FinishStep implements Component {
|
|
133
217
|
private readonly recapMarkdown = new Markdown("", 2, 0, getMarkdownTheme());
|
|
134
218
|
|
|
@@ -142,7 +226,7 @@ class FinishStep implements Component {
|
|
|
142
226
|
}
|
|
143
227
|
|
|
144
228
|
render(width: number): string[] {
|
|
145
|
-
const
|
|
229
|
+
const defaultsPart =
|
|
146
230
|
this.state.applyBuiltinDefaults === true
|
|
147
231
|
? [
|
|
148
232
|
"You selected **Recommended defaults**.",
|
|
@@ -150,7 +234,7 @@ class FinishStep implements Component {
|
|
|
150
234
|
"Guardrails will start with built-in protection, including:",
|
|
151
235
|
"- secret files like `.env`, `.env.local`, `.env.production`, `.dev.vars`",
|
|
152
236
|
"- safe exceptions like `.env.example` and `*.sample.env`",
|
|
153
|
-
"- confirmation before running dangerous commands like `rm -rf`, `sudo`, `dd
|
|
237
|
+
"- confirmation before running dangerous commands like `rm -rf`, `sudo`, `dd of=`",
|
|
154
238
|
].join("\n")
|
|
155
239
|
: [
|
|
156
240
|
"You selected **Minimal setup**.",
|
|
@@ -160,6 +244,12 @@ class FinishStep implements Component {
|
|
|
160
244
|
"You can configure policies later with `/guardrails:settings`.",
|
|
161
245
|
].join("\n");
|
|
162
246
|
|
|
247
|
+
const pathAccessPart = this.state.pathAccessEnabled
|
|
248
|
+
? "\n\n**Path access**: enabled (ask mode). The agent will prompt before accessing files outside the working directory."
|
|
249
|
+
: "\n\n**Path access**: disabled. No path restrictions.";
|
|
250
|
+
|
|
251
|
+
const content = defaultsPart + pathAccessPart;
|
|
252
|
+
|
|
163
253
|
this.recapMarkdown.setText(content);
|
|
164
254
|
return [...this.recapMarkdown.render(Math.max(1, width)), ""];
|
|
165
255
|
}
|
|
@@ -177,6 +267,7 @@ export function createOnboardingWizard(
|
|
|
177
267
|
): Component {
|
|
178
268
|
const state: OnboardingState = {
|
|
179
269
|
applyBuiltinDefaults: null,
|
|
270
|
+
pathAccessEnabled: null,
|
|
180
271
|
};
|
|
181
272
|
|
|
182
273
|
let markWelcomeComplete: (() => void) | null = null;
|
|
@@ -210,6 +301,14 @@ export function createOnboardingWizard(
|
|
|
210
301
|
ctx.goNext();
|
|
211
302
|
}),
|
|
212
303
|
},
|
|
304
|
+
{
|
|
305
|
+
label: "Path access",
|
|
306
|
+
build: (ctx) =>
|
|
307
|
+
new PathAccessStep(theme, state, () => {
|
|
308
|
+
ctx.markComplete();
|
|
309
|
+
ctx.goNext();
|
|
310
|
+
}),
|
|
311
|
+
},
|
|
213
312
|
{
|
|
214
313
|
label: "Recap",
|
|
215
314
|
build: (ctx) =>
|
|
@@ -219,21 +318,32 @@ export function createOnboardingWizard(
|
|
|
219
318
|
finalize({
|
|
220
319
|
completed: true,
|
|
221
320
|
applyBuiltinDefaults: state.applyBuiltinDefaults,
|
|
321
|
+
pathAccessEnabled: state.pathAccessEnabled,
|
|
222
322
|
});
|
|
223
323
|
}),
|
|
224
324
|
},
|
|
225
325
|
],
|
|
226
326
|
onComplete: () => {
|
|
227
327
|
if (state.applyBuiltinDefaults === null) {
|
|
228
|
-
finalize({
|
|
328
|
+
finalize({
|
|
329
|
+
completed: false,
|
|
330
|
+
applyBuiltinDefaults: null,
|
|
331
|
+
pathAccessEnabled: null,
|
|
332
|
+
});
|
|
229
333
|
return;
|
|
230
334
|
}
|
|
231
335
|
finalize({
|
|
232
336
|
completed: true,
|
|
233
337
|
applyBuiltinDefaults: state.applyBuiltinDefaults,
|
|
338
|
+
pathAccessEnabled: state.pathAccessEnabled,
|
|
234
339
|
});
|
|
235
340
|
},
|
|
236
|
-
onCancel: () =>
|
|
341
|
+
onCancel: () =>
|
|
342
|
+
finalize({
|
|
343
|
+
completed: false,
|
|
344
|
+
applyBuiltinDefaults: null,
|
|
345
|
+
pathAccessEnabled: null,
|
|
346
|
+
}),
|
|
237
347
|
hintSuffix: "Enter select/continue",
|
|
238
348
|
minContentHeight: 12,
|
|
239
349
|
});
|
|
@@ -256,8 +366,9 @@ export function createOnboardingWizard(
|
|
|
256
366
|
|
|
257
367
|
export function buildOnboardedConfig(
|
|
258
368
|
applyBuiltinDefaults: boolean,
|
|
369
|
+
pathAccessEnabled?: boolean | null,
|
|
259
370
|
): GuardrailsConfig {
|
|
260
|
-
|
|
371
|
+
const config: GuardrailsConfig = {
|
|
261
372
|
version: CURRENT_VERSION,
|
|
262
373
|
applyBuiltinDefaults,
|
|
263
374
|
onboarding: {
|
|
@@ -266,6 +377,11 @@ export function buildOnboardedConfig(
|
|
|
266
377
|
version: CURRENT_VERSION,
|
|
267
378
|
},
|
|
268
379
|
};
|
|
380
|
+
if (pathAccessEnabled) {
|
|
381
|
+
config.features = { ...config.features, pathAccess: true };
|
|
382
|
+
config.pathAccess = { mode: "ask" };
|
|
383
|
+
}
|
|
384
|
+
return config;
|
|
269
385
|
}
|
|
270
386
|
|
|
271
387
|
export function isOnboardingPending(config: GuardrailsConfig | null): boolean {
|
|
@@ -40,6 +40,10 @@ const FEATURE_UI: Record<FeatureKey, { label: string; description: string }> = {
|
|
|
40
40
|
description:
|
|
41
41
|
"Prompt for confirmation on dangerous commands (rm -rf, sudo, etc.)",
|
|
42
42
|
},
|
|
43
|
+
pathAccess: {
|
|
44
|
+
label: "Path access",
|
|
45
|
+
description: "Restrict tool access to the current working directory",
|
|
46
|
+
},
|
|
43
47
|
};
|
|
44
48
|
|
|
45
49
|
const POLICY_EXAMPLES: Array<{
|
|
@@ -1210,6 +1214,31 @@ export function registerGuardrailsSettings(pi: ExtensionAPI): void {
|
|
|
1210
1214
|
label: `Policies (${policyRules.length})`,
|
|
1211
1215
|
items: policyItems,
|
|
1212
1216
|
},
|
|
1217
|
+
{
|
|
1218
|
+
label: "Path Access",
|
|
1219
|
+
items: [
|
|
1220
|
+
{
|
|
1221
|
+
id: "pathAccess.mode",
|
|
1222
|
+
label: "Mode",
|
|
1223
|
+
description:
|
|
1224
|
+
"allow: no restrictions, ask: prompt for outside paths, block: deny all outside paths",
|
|
1225
|
+
currentValue: scopedConfig.pathAccess?.mode ?? "(inherited)",
|
|
1226
|
+
values: ["allow", "ask", "block"],
|
|
1227
|
+
},
|
|
1228
|
+
{
|
|
1229
|
+
id: "pathAccess.allowedPaths",
|
|
1230
|
+
label: "Allowed paths",
|
|
1231
|
+
description:
|
|
1232
|
+
"Paths always allowed (trailing / for directories). Supports ~/",
|
|
1233
|
+
currentValue: count("pathAccess.allowedPaths"),
|
|
1234
|
+
submenu: patternConfigSubmenu(
|
|
1235
|
+
"pathAccess.allowedPaths",
|
|
1236
|
+
"Allowed Paths",
|
|
1237
|
+
"file",
|
|
1238
|
+
),
|
|
1239
|
+
},
|
|
1240
|
+
],
|
|
1241
|
+
},
|
|
1213
1242
|
{
|
|
1214
1243
|
label: "Permission Gate",
|
|
1215
1244
|
items: [
|
package/src/config.ts
CHANGED
|
@@ -53,6 +53,13 @@ export interface PolicyRule {
|
|
|
53
53
|
enabled?: boolean;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export type PathAccessMode = "allow" | "ask" | "block";
|
|
57
|
+
|
|
58
|
+
export interface PathAccessConfig {
|
|
59
|
+
mode?: PathAccessMode;
|
|
60
|
+
allowedPaths?: string[];
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
export interface GuardrailsConfig {
|
|
57
64
|
version?: string;
|
|
58
65
|
enabled?: boolean;
|
|
@@ -66,12 +73,14 @@ export interface GuardrailsConfig {
|
|
|
66
73
|
features?: {
|
|
67
74
|
policies?: boolean;
|
|
68
75
|
permissionGate?: boolean;
|
|
76
|
+
pathAccess?: boolean;
|
|
69
77
|
// Deprecated. Kept only for migration.
|
|
70
78
|
protectEnvFiles?: boolean;
|
|
71
79
|
};
|
|
72
80
|
policies?: {
|
|
73
81
|
rules?: PolicyRule[];
|
|
74
82
|
};
|
|
83
|
+
pathAccess?: PathAccessConfig;
|
|
75
84
|
// Deprecated. Kept only for migration.
|
|
76
85
|
envFiles?: {
|
|
77
86
|
protectedPatterns?: PatternConfig[];
|
|
@@ -101,10 +110,15 @@ export interface ResolvedConfig {
|
|
|
101
110
|
features: {
|
|
102
111
|
policies: boolean;
|
|
103
112
|
permissionGate: boolean;
|
|
113
|
+
pathAccess: boolean;
|
|
104
114
|
};
|
|
105
115
|
policies: {
|
|
106
116
|
rules: PolicyRule[];
|
|
107
117
|
};
|
|
118
|
+
pathAccess: {
|
|
119
|
+
mode: PathAccessMode;
|
|
120
|
+
allowedPaths: string[];
|
|
121
|
+
};
|
|
108
122
|
permissionGate: {
|
|
109
123
|
patterns: DangerousPattern[];
|
|
110
124
|
/** When true, use hardcoded structural matchers for built-in patterns.
|
|
@@ -199,6 +213,11 @@ const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
199
213
|
features: {
|
|
200
214
|
policies: true,
|
|
201
215
|
permissionGate: true,
|
|
216
|
+
pathAccess: false,
|
|
217
|
+
},
|
|
218
|
+
pathAccess: {
|
|
219
|
+
mode: "ask",
|
|
220
|
+
allowedPaths: [],
|
|
202
221
|
},
|
|
203
222
|
policies: {
|
|
204
223
|
rules: [
|
|
@@ -277,13 +296,24 @@ const DEFAULT_CONFIG: ResolvedConfig = {
|
|
|
277
296
|
patterns: [
|
|
278
297
|
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
279
298
|
{ pattern: "sudo", description: "superuser command" },
|
|
280
|
-
{ pattern: "dd
|
|
299
|
+
{ pattern: "dd of=", description: "disk write operation" },
|
|
281
300
|
{ pattern: "mkfs.", description: "filesystem format" },
|
|
282
301
|
{
|
|
283
302
|
pattern: "chmod -R 777",
|
|
284
303
|
description: "insecure recursive permissions",
|
|
285
304
|
},
|
|
286
305
|
{ pattern: "chown -R", description: "recursive ownership change" },
|
|
306
|
+
{ pattern: "doas", description: "privileged command execution" },
|
|
307
|
+
{ pattern: "pkexec", description: "privileged command execution" },
|
|
308
|
+
{ pattern: "shred", description: "secure file overwrite" },
|
|
309
|
+
{ pattern: "wipefs", description: "filesystem signature wipe" },
|
|
310
|
+
{ pattern: "blkdiscard", description: "block device discard" },
|
|
311
|
+
{ pattern: "fdisk", description: "disk partitioning" },
|
|
312
|
+
{ pattern: "parted", description: "disk partitioning" },
|
|
313
|
+
{
|
|
314
|
+
pattern: "docker run --privileged",
|
|
315
|
+
description: "container with privileged mode",
|
|
316
|
+
},
|
|
287
317
|
],
|
|
288
318
|
useBuiltinMatchers: true,
|
|
289
319
|
requireConfirmation: true,
|
|
@@ -337,6 +367,19 @@ export const configLoader = new ConfigLoader<GuardrailsConfig, ResolvedConfig>(
|
|
|
337
367
|
resolved.permissionGate.patterns = customPatterns;
|
|
338
368
|
resolved.permissionGate.useBuiltinMatchers = false;
|
|
339
369
|
}
|
|
370
|
+
// Merge allowedPaths across scopes (additive)
|
|
371
|
+
const mergedPaths = new Set<string>();
|
|
372
|
+
for (const paths of [
|
|
373
|
+
global?.pathAccess?.allowedPaths,
|
|
374
|
+
local?.pathAccess?.allowedPaths,
|
|
375
|
+
memory?.pathAccess?.allowedPaths,
|
|
376
|
+
]) {
|
|
377
|
+
if (paths) {
|
|
378
|
+
for (const p of paths) mergedPaths.add(p);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
resolved.pathAccess.allowedPaths = [...mergedPaths];
|
|
382
|
+
|
|
340
383
|
return resolved;
|
|
341
384
|
},
|
|
342
385
|
},
|
package/src/hooks/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import type { ResolvedConfig } from "../config";
|
|
3
|
+
import { setupPathAccessHook } from "./path-access";
|
|
3
4
|
import { setupPermissionGateHook } from "./permission-gate";
|
|
4
5
|
import { setupPoliciesHook } from "./policies";
|
|
5
6
|
|
|
6
7
|
export function setupGuardrailsHooks(pi: ExtensionAPI, config: ResolvedConfig) {
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
setupPathAccessHook(pi); // boundary check — runs first
|
|
9
|
+
setupPoliciesHook(pi, config); // policy rules — runs second
|
|
10
|
+
setupPermissionGateHook(pi, config); // dangerous commands — runs third
|
|
9
11
|
}
|