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 CHANGED
@@ -1,33 +1,42 @@
1
- # pi-permissions
1
+ # pi-permissions [![npm version](https://img.shields.io/npm/v/pi-permissions)](https://www.npmjs.com/package/pi-permissions) [![license](https://img.shields.io/npm/l/pi-permissions)](./LICENSE)
2
2
 
3
- A [pi](https://github.com/badlogic/pi) extension that provides configurable allow/deny permission rules for tool calls — similar to Claude's permission system.
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
- # From GitHub
12
- pi install https://github.com/NicoAvanzDev/pi-permissions
17
+ Alternative install methods
13
18
 
14
- # Project-local (shared with team)
15
- pi install -l https://github.com/NicoAvanzDev/pi-permissions
19
+ From the public git repo:
16
20
 
17
- # Try without installing
18
- pi -e npm:pi-permissions
21
+ ```bash
22
+ pi install git:github.com/NicoAvanzDev/pi-permissions
19
23
  ```
20
24
 
21
- ## Configuration
25
+ From a local clone:
22
26
 
23
- Create a `permissions.json` file in one of these locations:
27
+ ```bash
28
+ pi install .
29
+ ```
24
30
 
25
- | Location | Scope |
26
- |----------|-------|
27
- | `.pi/permissions.json` | Project-local (checked first) |
28
- | `~/.pi/agent/permissions.json` | Global fallback |
31
+ Load without installing:
32
+
33
+ ```bash
34
+ pi --no-extensions -e npm:pi-permissions
35
+ ```
36
+
37
+ ## Quick start
29
38
 
30
- ### Example
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
- ## Rule Format
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 Tools
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 Logic
94
+ ### Evaluation logic
74
95
 
75
- 1. **Deny rules are checked first** — if any deny rule matches, the call is blocked
76
- 2. **Allow rules are checked next** — if allow rules exist for the tool, the call must match at least one
77
- 3. **No rules = no restrictions** — if no allow rules exist for a tool, all calls pass (unless denied)
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 Examples
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
- ## Reloading
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
- After editing `permissions.json`, type `/reload` in pi to apply the changes.
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.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",