@ssweens/pi-leash 0.12.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/package.json +83 -0
- package/src/config.ts +285 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/permission-gate.ts +925 -0
- package/src/hooks/policies.ts +315 -0
- package/src/index.ts +38 -0
- package/src/lib/executor.ts +280 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/model-resolver.ts +47 -0
- package/src/lib/timing.ts +42 -0
- package/src/lib/types.ts +115 -0
- package/src/utils/events.ts +32 -0
- package/src/utils/glob-expander.ts +128 -0
- package/src/utils/matching.ts +111 -0
- package/src/utils/shell-utils.ts +139 -0
- package/src/vendor/aliou-sh/NOTICE.md +13 -0
- package/src/vendor/aliou-sh/ast.d.ts +186 -0
- package/src/vendor/aliou-sh/index.d.ts +3 -0
- package/src/vendor/aliou-sh/index.js +1397 -0
- package/src/vendor/aliou-sh/parse.d.ts +3 -0
- package/src/vendor/aliou-sh/upstream.package.json +55 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.12.0] - 2026-05-14
|
|
6
|
+
### Added
|
|
7
|
+
- Sudo password caching (opt-in, in-memory, 5-minute TTL by default). The sudo password dialog now renders a Tab-toggleable `[ ] Remember password for N min (in-memory only)` checkbox. When checked, the password is cached in module memory for `sudoMode.cacheTtl` (default 300_000 ms) so consecutive sudo invocations skip the password re-entry step. The approval dialog (allow/deny) still runs every time — only the password step is bypassed when the cache is warm.
|
|
8
|
+
- New config under `permissionGate.sudoMode`:
|
|
9
|
+
- `cacheEnabled` (default `true`) — when `false` the checkbox is hidden and the cache lookup short-circuits.
|
|
10
|
+
- `cacheTtl` (default `300000` ms) — how long a remembered password lives in memory.
|
|
11
|
+
- Cache is cleared on TTL expiry (timer is `unref()`'d so it never keeps the loop alive), on `incorrect password` stderr (with an explicit notification that the cached password was rejected), on `session_shutdown`, and on process `exit` / `SIGINT` / `SIGTERM`. Passwords are never written to disk, logs, or telemetry.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- Dropped `@changesets/cli` workflow in favor of manual version + CHANGELOG management to match the rest of the @ssweens pi-packages repo. The `changeset` / `version` scripts are gone; `release` is now a plain `npm publish` shortcut.
|
|
15
|
+
|
|
16
|
+
### Notes
|
|
17
|
+
- First public npm release. Prior 0.1.0–0.11.0 versions existed only in the source repo and were not published.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ssweens
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Pi Leash
|
|
2
|
+
|
|
3
|
+
Security hooks for Pi to reduce accidental destructive actions and secret-file access.
|
|
4
|
+
|
|
5
|
+
Forked from [`@aliou/pi-guardrails`](https://github.com/aliou/pi-guardrails) (MIT) with added **sudo mode** for secure password handling and an opt-in in-memory password cache. See [Credits & Attribution](#credits--attribution) below for details.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pi install npm:@ssweens/pi-leash
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or from git:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pi install git:github.com/ssweens/pi-leash
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
- **policies**: named file-protection rules with per-rule protection levels.
|
|
22
|
+
- **permission-gate**: detects dangerous bash commands and asks for confirmation.
|
|
23
|
+
- **optional command explainer**: can call a small LLM to explain a dangerous command inline in the confirmation dialog.
|
|
24
|
+
- **sudo mode** (opt-in): securely handles sudo commands by prompting for passwords and executing with `sudo -S`.
|
|
25
|
+
|
|
26
|
+
## Configuration
|
|
27
|
+
|
|
28
|
+
Pi Leash reads from:
|
|
29
|
+
|
|
30
|
+
**`~/.pi/agent/settings/pi-leash.json`**
|
|
31
|
+
|
|
32
|
+
Create this file to customize settings. All fields are optional — sensible defaults are used when not specified.
|
|
33
|
+
|
|
34
|
+
### Example config
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"features": {
|
|
40
|
+
"policies": true,
|
|
41
|
+
"permissionGate": true
|
|
42
|
+
},
|
|
43
|
+
"permissionGate": {
|
|
44
|
+
"sudoMode": {
|
|
45
|
+
"enabled": true,
|
|
46
|
+
"timeout": 30000,
|
|
47
|
+
"preserveEnv": false
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Current schema
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"enabled": true,
|
|
58
|
+
"features": {
|
|
59
|
+
"policies": true,
|
|
60
|
+
"permissionGate": true
|
|
61
|
+
},
|
|
62
|
+
"policies": {
|
|
63
|
+
"rules": [
|
|
64
|
+
{
|
|
65
|
+
"id": "secret-files",
|
|
66
|
+
"description": "Files containing secrets",
|
|
67
|
+
"patterns": [
|
|
68
|
+
{ "pattern": ".env" },
|
|
69
|
+
{ "pattern": ".env.local" },
|
|
70
|
+
{ "pattern": ".env.production" },
|
|
71
|
+
{ "pattern": ".env.prod" },
|
|
72
|
+
{ "pattern": ".dev.vars" }
|
|
73
|
+
],
|
|
74
|
+
"allowedPatterns": [
|
|
75
|
+
{ "pattern": ".env.example" },
|
|
76
|
+
{ "pattern": ".env.sample" },
|
|
77
|
+
{ "pattern": ".env.test" },
|
|
78
|
+
{ "pattern": "*.example.env" },
|
|
79
|
+
{ "pattern": "*.sample.env" },
|
|
80
|
+
{ "pattern": "*.test.env" }
|
|
81
|
+
],
|
|
82
|
+
"protection": "noAccess",
|
|
83
|
+
"onlyIfExists": true
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"permissionGate": {
|
|
88
|
+
"patterns": [
|
|
89
|
+
{ "pattern": "rm -rf", "description": "recursive force delete" },
|
|
90
|
+
{ "pattern": "sudo", "description": "superuser command" },
|
|
91
|
+
{ "pattern": "git checkout", "description": "branch switch or discard uncommitted changes" }
|
|
92
|
+
],
|
|
93
|
+
"requireConfirmation": true,
|
|
94
|
+
"allowedPatterns": [],
|
|
95
|
+
"autoDenyPatterns": [],
|
|
96
|
+
"explainCommands": false,
|
|
97
|
+
"explainModel": null,
|
|
98
|
+
"explainTimeout": 5000,
|
|
99
|
+
"sudoMode": {
|
|
100
|
+
"enabled": false,
|
|
101
|
+
"timeout": 30000,
|
|
102
|
+
"preserveEnv": false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
All fields optional. Missing fields use defaults.
|
|
109
|
+
|
|
110
|
+
## Policies
|
|
111
|
+
|
|
112
|
+
Each rule has:
|
|
113
|
+
|
|
114
|
+
- `id`: stable identifier
|
|
115
|
+
- `patterns`: files to match (glob by default, regex if `regex: true`). Patterns with `/` match full path; patterns without `/` match basename only.
|
|
116
|
+
- `allowedPatterns`: exceptions
|
|
117
|
+
- `protection`:
|
|
118
|
+
- `noAccess`: block `read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`
|
|
119
|
+
- `readOnly`: block `write`, `edit`, `bash`
|
|
120
|
+
- `none`: no protection
|
|
121
|
+
- `onlyIfExists` (default true)
|
|
122
|
+
- `blockMessage` with `{file}` placeholder
|
|
123
|
+
- `enabled` (default true)
|
|
124
|
+
|
|
125
|
+
When multiple rules match, strongest protection wins: `noAccess > readOnly > none`.
|
|
126
|
+
|
|
127
|
+
## Permission gate
|
|
128
|
+
|
|
129
|
+
Detects dangerous bash commands and prompts user confirmation.
|
|
130
|
+
|
|
131
|
+
Built-in dangerous patterns are matched structurally (AST-based):
|
|
132
|
+
|
|
133
|
+
- `rm -rf`
|
|
134
|
+
- `sudo`
|
|
135
|
+
- `dd if=`
|
|
136
|
+
- `mkfs.`
|
|
137
|
+
- `chmod -R 777`
|
|
138
|
+
- `chown -R`
|
|
139
|
+
- `git checkout`
|
|
140
|
+
|
|
141
|
+
You can add custom dangerous patterns via `permissionGate.patterns`.
|
|
142
|
+
|
|
143
|
+
### Explain commands (opt-in)
|
|
144
|
+
|
|
145
|
+
If enabled, guardrails calls an LLM before showing the confirmation dialog and displays a short explanation.
|
|
146
|
+
|
|
147
|
+
Config fields:
|
|
148
|
+
|
|
149
|
+
- `permissionGate.explainCommands` (boolean)
|
|
150
|
+
- `permissionGate.explainModel` (`provider/model-id`)
|
|
151
|
+
- `permissionGate.explainTimeout` (ms)
|
|
152
|
+
|
|
153
|
+
Failures/timeouts degrade gracefully: dialog still shows without explanation.
|
|
154
|
+
|
|
155
|
+
### Sudo mode (opt-in)
|
|
156
|
+
|
|
157
|
+
When enabled, sudo commands prompt for the sudo password and execute securely using `sudo -S`. The password is masked during input and cleared from memory immediately after execution.
|
|
158
|
+
|
|
159
|
+
Config fields:
|
|
160
|
+
|
|
161
|
+
- `permissionGate.sudoMode.enabled` (boolean) - Enable sudo mode
|
|
162
|
+
- `permissionGate.sudoMode.timeout` (number, ms) - Command timeout (default: 30000)
|
|
163
|
+
- `permissionGate.sudoMode.preserveEnv` (boolean) - Preserve environment with `sudo -E` (default: false)
|
|
164
|
+
|
|
165
|
+
**Example config:**
|
|
166
|
+
```json
|
|
167
|
+
{
|
|
168
|
+
"permissionGate": {
|
|
169
|
+
"sudoMode": {
|
|
170
|
+
"enabled": true,
|
|
171
|
+
"timeout": 60000,
|
|
172
|
+
"preserveEnv": false
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Security notes:**
|
|
179
|
+
- Passwords are never logged or stored to disk
|
|
180
|
+
- Password input is masked (••••)
|
|
181
|
+
- Password buffer is overwritten with asterisks after use
|
|
182
|
+
- Uses `sudo -S` for secure stdin-based password delivery
|
|
183
|
+
- Failed authentication shows an error notification
|
|
184
|
+
|
|
185
|
+
## Events
|
|
186
|
+
|
|
187
|
+
Pi Leash emits events for other extensions:
|
|
188
|
+
|
|
189
|
+
### `leash:blocked`
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
interface LeashBlockedEvent {
|
|
193
|
+
feature: "policies" | "permissionGate";
|
|
194
|
+
toolName: string;
|
|
195
|
+
input: Record<string, unknown>;
|
|
196
|
+
reason: string;
|
|
197
|
+
userDenied?: boolean;
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `leash:dangerous`
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
interface LeashDangerousEvent {
|
|
205
|
+
command: string;
|
|
206
|
+
description: string;
|
|
207
|
+
pattern: string;
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Credits & Attribution
|
|
212
|
+
|
|
213
|
+
Pi Leash is a fork of [`@aliou/pi-guardrails`](https://github.com/aliou/pi-guardrails) by [@aliou](https://github.com/aliou), used under the terms of the MIT License. The original package provided the file-protection policy engine, the dangerous-command permission gate, and the optional command-explainer integration that this fork inherits.
|
|
214
|
+
|
|
215
|
+
This fork adds:
|
|
216
|
+
- Secure sudo mode with masked password input and `sudo -S` execution.
|
|
217
|
+
- Opt-in in-memory sudo password caching with a per-prompt `Remember for N min` toggle.
|
|
218
|
+
- Self-contained shell parser: the `@aliou/sh` parser used by structural command matching is vendored into `src/vendor/aliou-sh` (also MIT, see [`NOTICE.md`](src/vendor/aliou-sh/NOTICE.md)) so local path installs do not depend on external module resolution.
|
|
219
|
+
- Various stability fixes to the sudo approval flow, dialog key handling, and dangerous-pattern detection.
|
|
220
|
+
|
|
221
|
+
Both pi-leash and the upstream `@aliou/pi-guardrails` are MIT-licensed. See [`LICENSE`](LICENSE) for the full text.
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ssweens/pi-leash",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"author": "ssweens (forked from @aliou/pi-guardrails)",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"pi-package",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"pi",
|
|
12
|
+
"leash",
|
|
13
|
+
"guardrails",
|
|
14
|
+
"security",
|
|
15
|
+
"sudo",
|
|
16
|
+
"permissions"
|
|
17
|
+
],
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "https://github.com/ssweens/pi-leash"
|
|
21
|
+
},
|
|
22
|
+
"pi": {
|
|
23
|
+
"extensions": [
|
|
24
|
+
"./src/index.ts"
|
|
25
|
+
]
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"src",
|
|
32
|
+
"README.md",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"LICENSE"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@mariozechner/pi-agent-core": ">=0.52.7",
|
|
39
|
+
"@mariozechner/pi-ai": ">=0.52.7",
|
|
40
|
+
"@mariozechner/pi-coding-agent": ">=0.52.7",
|
|
41
|
+
"@mariozechner/pi-tui": ">=0.51.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@aliou/biome-plugins": "^0.3.2",
|
|
45
|
+
"@biomejs/biome": "^2.3.13",
|
|
46
|
+
"@mariozechner/pi-agent-core": "0.52.7",
|
|
47
|
+
"@mariozechner/pi-ai": "0.52.7",
|
|
48
|
+
"@mariozechner/pi-coding-agent": "0.52.7",
|
|
49
|
+
"@sinclair/typebox": "^0.34.48",
|
|
50
|
+
"@types/node": "^25.0.10",
|
|
51
|
+
"husky": "^9.1.7",
|
|
52
|
+
"typescript": "^5.9.3"
|
|
53
|
+
},
|
|
54
|
+
"pnpm": {
|
|
55
|
+
"overrides": {
|
|
56
|
+
"@mariozechner/pi-ai": "$@mariozechner/pi-coding-agent",
|
|
57
|
+
"@mariozechner/pi-tui": "$@mariozechner/pi-coding-agent"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"scripts": {
|
|
61
|
+
"typecheck": "tsc --noEmit",
|
|
62
|
+
"lint": "biome check",
|
|
63
|
+
"format": "biome check --write",
|
|
64
|
+
"check:lockfile": "pnpm install --frozen-lockfile --ignore-scripts",
|
|
65
|
+
"prepare": "[ -d .git ] && husky || true",
|
|
66
|
+
"release": "npm publish"
|
|
67
|
+
},
|
|
68
|
+
"packageManager": "pnpm@10.26.1",
|
|
69
|
+
"peerDependenciesMeta": {
|
|
70
|
+
"@mariozechner/pi-agent-core": {
|
|
71
|
+
"optional": true
|
|
72
|
+
},
|
|
73
|
+
"@mariozechner/pi-ai": {
|
|
74
|
+
"optional": true
|
|
75
|
+
},
|
|
76
|
+
"@mariozechner/pi-coding-agent": {
|
|
77
|
+
"optional": true
|
|
78
|
+
},
|
|
79
|
+
"@mariozechner/pi-tui": {
|
|
80
|
+
"optional": true
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration schema for the Pi Leash extension.
|
|
3
|
+
*
|
|
4
|
+
* Reads from ~/.pi/agent/settings/pi-leash.json (if present).
|
|
5
|
+
* All fields are optional — sensible defaults are used when not specified.
|
|
6
|
+
*
|
|
7
|
+
* Example config:
|
|
8
|
+
* {
|
|
9
|
+
* "enabled": true,
|
|
10
|
+
* "features": {
|
|
11
|
+
* "policies": true,
|
|
12
|
+
* "permissionGate": true
|
|
13
|
+
* },
|
|
14
|
+
* "permissionGate": {
|
|
15
|
+
* "sudoMode": {
|
|
16
|
+
* "enabled": true,
|
|
17
|
+
* "timeout": 30000
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { join } from "node:path";
|
|
25
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
26
|
+
|
|
27
|
+
export function getConfigPath(): string {
|
|
28
|
+
return join(getAgentDir(), "settings", "pi-leash.json");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A pattern with explicit matching mode.
|
|
33
|
+
* Default: glob for files, substring for commands.
|
|
34
|
+
* regex: true means full regex matching.
|
|
35
|
+
*/
|
|
36
|
+
export interface PatternConfig {
|
|
37
|
+
pattern: string;
|
|
38
|
+
regex?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Permission gate pattern. When regex is false (default), the pattern
|
|
43
|
+
* is matched as substring against the raw command string.
|
|
44
|
+
* When regex is true, uses full regex against the raw string.
|
|
45
|
+
*/
|
|
46
|
+
export interface DangerousPattern extends PatternConfig {
|
|
47
|
+
description: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Protection level for a policy rule.
|
|
52
|
+
*/
|
|
53
|
+
export type Protection = "none" | "readOnly" | "noAccess";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A named policy rule. Matches files by patterns and enforces a protection level.
|
|
57
|
+
*/
|
|
58
|
+
export interface PolicyRule {
|
|
59
|
+
/** Stable identifier used for deduplication across scopes. */
|
|
60
|
+
id: string;
|
|
61
|
+
/** Optional display name for settings/UI. */
|
|
62
|
+
name?: string;
|
|
63
|
+
/** Human-readable description. */
|
|
64
|
+
description?: string;
|
|
65
|
+
/** File patterns to protect. */
|
|
66
|
+
patterns: PatternConfig[];
|
|
67
|
+
/** Optional exceptions. */
|
|
68
|
+
allowedPatterns?: PatternConfig[];
|
|
69
|
+
/** Protection level. */
|
|
70
|
+
protection: Protection;
|
|
71
|
+
/** Block only when file exists on disk. Default true. */
|
|
72
|
+
onlyIfExists?: boolean;
|
|
73
|
+
/** Message shown when blocked; supports {file} placeholder. */
|
|
74
|
+
blockMessage?: string;
|
|
75
|
+
/** Per-rule toggle. Default true. */
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* User-facing guardrails configuration.
|
|
81
|
+
* All fields are optional - defaults are applied.
|
|
82
|
+
*/
|
|
83
|
+
export interface GuardrailsConfig {
|
|
84
|
+
/** Enable/disable guardrails entirely. Default: true */
|
|
85
|
+
enabled?: boolean;
|
|
86
|
+
/** Feature toggles */
|
|
87
|
+
features?: {
|
|
88
|
+
/** Enable file protection policies. Default: true */
|
|
89
|
+
policies?: boolean;
|
|
90
|
+
/** Enable permission gate for dangerous commands. Default: true */
|
|
91
|
+
permissionGate?: boolean;
|
|
92
|
+
};
|
|
93
|
+
/** File protection policies */
|
|
94
|
+
policies?: {
|
|
95
|
+
/** Custom rules to add to the default secret-files rule */
|
|
96
|
+
rules?: PolicyRule[];
|
|
97
|
+
};
|
|
98
|
+
/** Permission gate configuration */
|
|
99
|
+
permissionGate?: {
|
|
100
|
+
/** Additional dangerous patterns to watch for */
|
|
101
|
+
patterns?: DangerousPattern[];
|
|
102
|
+
/** Require confirmation before executing dangerous commands. Default: true */
|
|
103
|
+
requireConfirmation?: boolean;
|
|
104
|
+
/** Patterns that bypass the permission gate */
|
|
105
|
+
allowedPatterns?: PatternConfig[];
|
|
106
|
+
/** Patterns that are automatically denied */
|
|
107
|
+
autoDenyPatterns?: PatternConfig[];
|
|
108
|
+
/** Use LLM to explain commands before confirmation. Default: false */
|
|
109
|
+
explainCommands?: boolean;
|
|
110
|
+
/** Model to use for explanations (format: provider/model-id) */
|
|
111
|
+
explainModel?: string;
|
|
112
|
+
/** Timeout for explanation requests. Default: 5000 */
|
|
113
|
+
explainTimeout?: number;
|
|
114
|
+
/** Sudo mode configuration */
|
|
115
|
+
sudoMode?: {
|
|
116
|
+
/** Enable sudo password prompts and execution. Default: false */
|
|
117
|
+
enabled?: boolean;
|
|
118
|
+
/** Command timeout in milliseconds. Default: 30000 */
|
|
119
|
+
timeout?: number;
|
|
120
|
+
/** Preserve environment with sudo -E. Default: false */
|
|
121
|
+
preserveEnv?: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* Show a "Remember password for N minutes" toggle in the sudo password
|
|
124
|
+
* dialog. When the user opts in, the password is cached in-memory only
|
|
125
|
+
* (never written to disk) for `cacheTtl` milliseconds so subsequent
|
|
126
|
+
* sudo prompts skip the password step. The approval dialog still runs
|
|
127
|
+
* every time. Default: true.
|
|
128
|
+
*/
|
|
129
|
+
cacheEnabled?: boolean;
|
|
130
|
+
/** Cache TTL in milliseconds. Default: 300000 (5 minutes). */
|
|
131
|
+
cacheTtl?: number;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Resolved configuration with all defaults applied.
|
|
138
|
+
*/
|
|
139
|
+
export interface ResolvedConfig {
|
|
140
|
+
enabled: boolean;
|
|
141
|
+
features: {
|
|
142
|
+
policies: boolean;
|
|
143
|
+
permissionGate: boolean;
|
|
144
|
+
};
|
|
145
|
+
policies: {
|
|
146
|
+
rules: PolicyRule[];
|
|
147
|
+
};
|
|
148
|
+
permissionGate: {
|
|
149
|
+
patterns: DangerousPattern[];
|
|
150
|
+
useBuiltinMatchers: boolean;
|
|
151
|
+
requireConfirmation: boolean;
|
|
152
|
+
allowedPatterns: PatternConfig[];
|
|
153
|
+
autoDenyPatterns: PatternConfig[];
|
|
154
|
+
explainCommands: boolean;
|
|
155
|
+
explainModel: string | null;
|
|
156
|
+
explainTimeout: number;
|
|
157
|
+
sudoMode: {
|
|
158
|
+
enabled: boolean;
|
|
159
|
+
timeout: number;
|
|
160
|
+
preserveEnv: boolean;
|
|
161
|
+
cacheEnabled: boolean;
|
|
162
|
+
cacheTtl: number;
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const DEFAULT_SECRET_FILES_RULE: PolicyRule = {
|
|
168
|
+
id: "secret-files",
|
|
169
|
+
description: "Files containing secrets",
|
|
170
|
+
patterns: [
|
|
171
|
+
{ pattern: ".env" },
|
|
172
|
+
{ pattern: ".env.local" },
|
|
173
|
+
{ pattern: ".env.production" },
|
|
174
|
+
{ pattern: ".env.prod" },
|
|
175
|
+
{ pattern: ".dev.vars" },
|
|
176
|
+
],
|
|
177
|
+
allowedPatterns: [
|
|
178
|
+
{ pattern: "*.example.env" },
|
|
179
|
+
{ pattern: "*.sample.env" },
|
|
180
|
+
{ pattern: "*.test.env" },
|
|
181
|
+
{ pattern: ".env.example" },
|
|
182
|
+
{ pattern: ".env.sample" },
|
|
183
|
+
{ pattern: ".env.test" },
|
|
184
|
+
],
|
|
185
|
+
protection: "noAccess",
|
|
186
|
+
onlyIfExists: true,
|
|
187
|
+
blockMessage:
|
|
188
|
+
"Accessing {file} is not allowed. This file contains secrets. " +
|
|
189
|
+
"Explain to the user why you want to access this file, and if changes are needed ask the user to make them.",
|
|
190
|
+
enabled: true,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const DEFAULT_DANGEROUS_PATTERNS: DangerousPattern[] = [
|
|
194
|
+
{ pattern: "rm -rf", description: "recursive force delete" },
|
|
195
|
+
{ pattern: "sudo", description: "superuser command" },
|
|
196
|
+
{ pattern: "dd if=", description: "disk write operation" },
|
|
197
|
+
{ pattern: "mkfs.", description: "filesystem format" },
|
|
198
|
+
{ pattern: "chmod -R 777", description: "insecure recursive permissions" },
|
|
199
|
+
{ pattern: "chown -R", description: "recursive ownership change" },
|
|
200
|
+
{
|
|
201
|
+
pattern: "git checkout",
|
|
202
|
+
description: "branch switch or discard uncommitted changes",
|
|
203
|
+
},
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
function mergeConfig(userConfig: GuardrailsConfig): ResolvedConfig {
|
|
207
|
+
// Build policies: default secret-files rule + user rules
|
|
208
|
+
const policies: ResolvedConfig["policies"] = {
|
|
209
|
+
rules: [DEFAULT_SECRET_FILES_RULE, ...(userConfig.policies?.rules ?? [])],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// Build permission gate settings
|
|
213
|
+
const pg = userConfig.permissionGate ?? {};
|
|
214
|
+
const permissionGate: ResolvedConfig["permissionGate"] = {
|
|
215
|
+
patterns: [...DEFAULT_DANGEROUS_PATTERNS, ...(pg.patterns ?? [])],
|
|
216
|
+
useBuiltinMatchers: true,
|
|
217
|
+
requireConfirmation: pg.requireConfirmation ?? true,
|
|
218
|
+
allowedPatterns: pg.allowedPatterns ?? [],
|
|
219
|
+
autoDenyPatterns: pg.autoDenyPatterns ?? [],
|
|
220
|
+
explainCommands: pg.explainCommands ?? false,
|
|
221
|
+
explainModel: pg.explainModel ?? null,
|
|
222
|
+
explainTimeout: pg.explainTimeout ?? 5000,
|
|
223
|
+
sudoMode: {
|
|
224
|
+
enabled: pg.sudoMode?.enabled ?? false,
|
|
225
|
+
timeout: pg.sudoMode?.timeout ?? 30000,
|
|
226
|
+
preserveEnv: pg.sudoMode?.preserveEnv ?? false,
|
|
227
|
+
cacheEnabled: pg.sudoMode?.cacheEnabled ?? true,
|
|
228
|
+
cacheTtl: pg.sudoMode?.cacheTtl ?? 300000,
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
enabled: userConfig.enabled ?? true,
|
|
234
|
+
features: {
|
|
235
|
+
policies: userConfig.features?.policies ?? true,
|
|
236
|
+
permissionGate: userConfig.features?.permissionGate ?? true,
|
|
237
|
+
},
|
|
238
|
+
policies,
|
|
239
|
+
permissionGate,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
let _cachedConfig: ResolvedConfig | null = null;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Load and merge configuration from global settings.
|
|
247
|
+
* Caches the result for the session.
|
|
248
|
+
*/
|
|
249
|
+
export function loadConfig(): ResolvedConfig {
|
|
250
|
+
if (_cachedConfig !== null) {
|
|
251
|
+
return _cachedConfig;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const configPath = getConfigPath();
|
|
255
|
+
let userConfig: GuardrailsConfig = {};
|
|
256
|
+
|
|
257
|
+
if (existsSync(configPath)) {
|
|
258
|
+
try {
|
|
259
|
+
userConfig = JSON.parse(
|
|
260
|
+
readFileSync(configPath, "utf-8"),
|
|
261
|
+
) as GuardrailsConfig;
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.warn(`[pi-leash] Failed to parse ${configPath}: ${err}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
_cachedConfig = mergeConfig(userConfig);
|
|
268
|
+
return _cachedConfig;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Clear the cached configuration.
|
|
273
|
+
* Call this if the config file changes during a session.
|
|
274
|
+
*/
|
|
275
|
+
export function clearConfigCache(): void {
|
|
276
|
+
_cachedConfig = null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Get the current configuration (cached).
|
|
281
|
+
* Same as loadConfig() but semantically clearer when you know it's already loaded.
|
|
282
|
+
*/
|
|
283
|
+
export function getConfig(): ResolvedConfig {
|
|
284
|
+
return loadConfig();
|
|
285
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ResolvedConfig } from "../config";
|
|
3
|
+
import { setupPermissionGateHook } from "./permission-gate";
|
|
4
|
+
import { setupPoliciesHook } from "./policies";
|
|
5
|
+
|
|
6
|
+
export function setupLeashHooks(pi: ExtensionAPI, config: ResolvedConfig) {
|
|
7
|
+
setupPoliciesHook(pi, config);
|
|
8
|
+
setupPermissionGateHook(pi, config);
|
|
9
|
+
}
|