pi-permissions 1.0.0 → 1.0.2
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 +66 -30
- package/extensions/rules.ts +105 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,33 +1,42 @@
|
|
|
1
|
-
# pi-permissions
|
|
1
|
+
# pi-permissions [](https://www.npmjs.com/package/pi-permissions) [](./LICENSE)
|
|
2
2
|
|
|
3
|
-
A [pi](https://github.com/badlogic/pi) extension that provides configurable allow/deny permission rules for tool calls —
|
|
3
|
+
A [pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) extension that provides **configurable allow/deny permission rules** for tool calls — control which bash commands, file reads, writes, and edits the agent can perform.
|
|
4
|
+
|
|
5
|
+
- Define **allow** rules to whitelist specific commands
|
|
6
|
+
- Define **deny** rules to block dangerous operations
|
|
7
|
+
- Deny always wins — block `git push` even if `git *` is allowed
|
|
8
|
+
- Supports glob patterns with `*` wildcards
|
|
9
|
+
- Project-local or global configuration
|
|
4
10
|
|
|
5
11
|
## Install
|
|
6
12
|
|
|
7
13
|
```bash
|
|
8
|
-
# From npm
|
|
9
14
|
pi install npm:pi-permissions
|
|
15
|
+
```
|
|
10
16
|
|
|
11
|
-
|
|
12
|
-
pi install https://github.com/NicoAvanzDev/pi-permissions
|
|
17
|
+
Alternative install methods
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
pi install -l https://github.com/NicoAvanzDev/pi-permissions
|
|
19
|
+
From the public git repo:
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
pi
|
|
21
|
+
```bash
|
|
22
|
+
pi install git:github.com/NicoAvanzDev/pi-permissions
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
From a local clone:
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
```bash
|
|
28
|
+
pi install .
|
|
29
|
+
```
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
31
|
+
Load without installing:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pi --no-extensions -e npm:pi-permissions
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
29
38
|
|
|
30
|
-
|
|
39
|
+
Create `.pi/permissions.json` in your project (or `~/.pi/agent/permissions.json` for global rules):
|
|
31
40
|
|
|
32
41
|
```json
|
|
33
42
|
{
|
|
@@ -38,30 +47,42 @@ Create a `permissions.json` file in one of these locations:
|
|
|
38
47
|
"Bash(git commit *)",
|
|
39
48
|
"Bash(git diff *)",
|
|
40
49
|
"Bash(git status)",
|
|
41
|
-
"Bash(git log *)"
|
|
42
|
-
"Bash(* --version)",
|
|
43
|
-
"Bash(* --help *)"
|
|
50
|
+
"Bash(git log *)"
|
|
44
51
|
],
|
|
45
52
|
"deny": [
|
|
46
53
|
"Bash(git push *)",
|
|
47
|
-
"Bash(git add .)",
|
|
48
54
|
"Bash(rm -rf *)",
|
|
49
55
|
"Write(*.env)",
|
|
50
|
-
"Write(.env.*)",
|
|
51
56
|
"Edit(*.env)"
|
|
52
57
|
]
|
|
53
58
|
}
|
|
54
59
|
}
|
|
55
60
|
```
|
|
56
61
|
|
|
57
|
-
|
|
62
|
+
On session start, the extension loads your rules and reports how many were found. Any tool call that matches a deny rule is blocked before execution.
|
|
63
|
+
|
|
64
|
+
### Example session
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
> pi
|
|
68
|
+
Permissions loaded: 6 allow, 4 deny rules
|
|
69
|
+
|
|
70
|
+
> (agent tries to run `git push origin main`)
|
|
71
|
+
⛔ Blocked: Bash command matches deny rule "Bash(git push *)"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
After editing `permissions.json`, type `/reload` in pi to apply the changes.
|
|
75
|
+
|
|
76
|
+
## How it works
|
|
77
|
+
|
|
78
|
+
### Rule format
|
|
58
79
|
|
|
59
80
|
Rules use the format `Tool(pattern)` where:
|
|
60
81
|
|
|
61
82
|
- **Tool** — the pi tool name: `Bash`, `Write`, `Edit`, `Read`
|
|
62
83
|
- **pattern** — a glob pattern where `*` matches any characters
|
|
63
84
|
|
|
64
|
-
### Supported
|
|
85
|
+
### Supported tools
|
|
65
86
|
|
|
66
87
|
| Tool | What the pattern matches against |
|
|
67
88
|
|------|----------------------------------|
|
|
@@ -70,18 +91,20 @@ Rules use the format `Tool(pattern)` where:
|
|
|
70
91
|
| `Edit(pattern)` | The file path being edited |
|
|
71
92
|
| `Read(pattern)` | The file path being read |
|
|
72
93
|
|
|
73
|
-
### Evaluation
|
|
94
|
+
### Evaluation logic
|
|
74
95
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
96
|
+
For every tool call the extension:
|
|
97
|
+
|
|
98
|
+
1. **checks deny rules first** — if any deny rule matches, the call is blocked
|
|
99
|
+
2. **checks allow rules next** — if allow rules exist for that tool, the call must match at least one
|
|
100
|
+
3. **passes through** — if no allow rules exist for the tool, all calls are permitted (unless denied)
|
|
78
101
|
|
|
79
102
|
This means:
|
|
80
103
|
- Use **deny** to block specific dangerous commands
|
|
81
104
|
- Use **allow** to whitelist only approved commands (everything else is blocked)
|
|
82
105
|
- **Deny always wins** over allow
|
|
83
106
|
|
|
84
|
-
### Pattern
|
|
107
|
+
### Pattern examples
|
|
85
108
|
|
|
86
109
|
| Pattern | Matches | Doesn't match |
|
|
87
110
|
|---------|---------|---------------|
|
|
@@ -91,9 +114,22 @@ This means:
|
|
|
91
114
|
| `Write(*.env)` | `.env`, `app.env` | `.env.example` |
|
|
92
115
|
| `Write(secrets/*)` | `secrets/key.pem` | `src/secrets.ts` |
|
|
93
116
|
|
|
94
|
-
##
|
|
117
|
+
## Configuration
|
|
118
|
+
|
|
119
|
+
| Location | Scope |
|
|
120
|
+
|----------|-------|
|
|
121
|
+
| `.pi/permissions.json` | Project-local (checked first) |
|
|
122
|
+
| `~/.pi/agent/permissions.json` | Global fallback |
|
|
123
|
+
|
|
124
|
+
The extension checks the project-local path first. If not found, it falls back to the global config.
|
|
95
125
|
|
|
96
|
-
|
|
126
|
+
## Notes
|
|
127
|
+
|
|
128
|
+
- rules are loaded once on session start
|
|
129
|
+
- use `/reload` to pick up changes without restarting
|
|
130
|
+
- deny rules are always evaluated before allow rules
|
|
131
|
+
- if no allow rules exist for a tool type, all calls for that tool are permitted
|
|
132
|
+
- glob `*` matches any sequence of characters (including path separators in file patterns)
|
|
97
133
|
|
|
98
134
|
## License
|
|
99
135
|
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export interface PermissionsConfig {
|
|
2
|
+
permissions?: {
|
|
3
|
+
allow?: string[];
|
|
4
|
+
deny?: string[];
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ParsedRule {
|
|
9
|
+
tool: string;
|
|
10
|
+
pattern: RegExp;
|
|
11
|
+
original: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function globToRegex(glob: string): RegExp {
|
|
15
|
+
const escaped = glob.replace(/([.+?^${}()|[\]\\])/g, "\\$1").replace(/\*/g, ".*");
|
|
16
|
+
return new RegExp(`^${escaped}$`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function parseRule(rule: string): ParsedRule | null {
|
|
20
|
+
const match = rule.match(/^(\w+)\((.+)\)$/);
|
|
21
|
+
if (!match) return null;
|
|
22
|
+
const [, tool, glob] = match;
|
|
23
|
+
return {
|
|
24
|
+
tool: tool.toLowerCase(),
|
|
25
|
+
pattern: globToRegex(glob),
|
|
26
|
+
original: rule,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function parseRules(rules: string[]): ParsedRule[] {
|
|
31
|
+
return rules.map(parseRule).filter((r): r is ParsedRule => r !== null);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Map pi tool names to permission tool names
|
|
35
|
+
export function getToolKey(toolName: string): string {
|
|
36
|
+
switch (toolName) {
|
|
37
|
+
case "bash":
|
|
38
|
+
case "write":
|
|
39
|
+
case "edit":
|
|
40
|
+
case "read":
|
|
41
|
+
return toolName;
|
|
42
|
+
default:
|
|
43
|
+
return toolName;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Extract the matchable string from tool input
|
|
48
|
+
export function getMatchTarget(toolName: string, input: Record<string, unknown>): string | null {
|
|
49
|
+
switch (toolName) {
|
|
50
|
+
case "bash":
|
|
51
|
+
return (input.command as string)?.trim() ?? null;
|
|
52
|
+
case "write":
|
|
53
|
+
case "edit":
|
|
54
|
+
case "read":
|
|
55
|
+
return (input.path as string) ?? null;
|
|
56
|
+
default:
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface EvalResult {
|
|
62
|
+
blocked: boolean;
|
|
63
|
+
reason?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Evaluate whether a tool call should be blocked.
|
|
68
|
+
* - Deny rules are checked first (deny always wins)
|
|
69
|
+
* - If allow rules exist for the tool, the call must match at least one
|
|
70
|
+
* - No rules = no restrictions
|
|
71
|
+
*/
|
|
72
|
+
export function evaluate(
|
|
73
|
+
toolName: string,
|
|
74
|
+
input: Record<string, unknown>,
|
|
75
|
+
allowRules: ParsedRule[],
|
|
76
|
+
denyRules: ParsedRule[],
|
|
77
|
+
): EvalResult {
|
|
78
|
+
const toolKey = getToolKey(toolName);
|
|
79
|
+
const target = getMatchTarget(toolName, input);
|
|
80
|
+
if (!target) return { blocked: false };
|
|
81
|
+
|
|
82
|
+
// Deny rules first — deny always wins
|
|
83
|
+
for (const rule of denyRules) {
|
|
84
|
+
if (rule.tool === toolKey && rule.pattern.test(target)) {
|
|
85
|
+
return {
|
|
86
|
+
blocked: true,
|
|
87
|
+
reason: `Blocked by deny rule: ${rule.original}\nCommand: ${target}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// If allow rules exist for this tool, must match at least one
|
|
93
|
+
const toolAllowRules = allowRules.filter((r) => r.tool === toolKey);
|
|
94
|
+
if (toolAllowRules.length > 0) {
|
|
95
|
+
const allowed = toolAllowRules.some((rule) => rule.pattern.test(target));
|
|
96
|
+
if (!allowed) {
|
|
97
|
+
return {
|
|
98
|
+
blocked: true,
|
|
99
|
+
reason: `Blocked: no allow rule matched for ${toolName}.\nCommand: ${target}\nAllowed patterns: ${toolAllowRules.map((r) => r.original).join(", ")}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { blocked: false };
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-permissions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Configurable allow/deny permission rules for pi tool calls — control which bash commands, file reads, writes, and edits the agent can perform.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"extensions/permissions.ts",
|
|
23
|
+
"extensions/rules.ts",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"type": "module",
|