pi-permission-system 0.3.1 → 0.4.1

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 CHANGED
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.1] - 2026-04-01
9
+
10
+ ### Changed
11
+ - Updated npm keywords for improved discoverability (`pi-coding-agent`, `coding-agent`, `access-control`, `authorization`, `security`)
12
+ - Updated README permission prompt example image
13
+ - Added Related Pi Extensions cross-linking section to README
14
+
15
+ ## [0.4.0] - 2026-04-01
16
+
17
+ ### Added
18
+ - System prompt sanitizer now removes inactive tool guidelines from the `Guidelines:` section
19
+ - Guideline filtering based on allowed tools (e.g., removes task/mcp/bash/write guidance when tools are denied)
20
+ - New `TOOL_GUIDELINE_RULES` configuration for extensible guideline filtering
21
+ - Helper functions: `findSection()`, `removeLineSection()`, `sanitizeGuidelinesSection()`
22
+
23
+ ### Changed
24
+ - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.64.0
25
+ - Updated `@sinclair/typebox` peer dependency to ^0.34.49
26
+ - Refactored system prompt sanitizer to handle both `Available tools:` and `Guidelines:` sections
27
+
28
+ ### Tests
29
+ - Added tests for system prompt sanitizer removing Available tools section
30
+ - Added tests for guideline filtering based on allowed tools
31
+ - Added tests for inactive built-in write/edit/task/mcp guidance removal
32
+
8
33
  ## [0.3.1] - 2026-03-24
9
34
 
10
35
  ### Added
package/README.md CHANGED
@@ -1,11 +1,12 @@
1
1
  # 🔐 pi-permission-system
2
2
 
3
- [![Version](https://img.shields.io/badge/version-0.3.1-blue.svg)](package.json)
3
+ [![Version](https://img.shields.io/badge/version-0.4.1-blue.svg)](package.json)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
5
5
 
6
6
  Permission enforcement extension for the Pi coding agent that provides centralized, deterministic permission gates for tool, bash, MCP, skill, and special operations.
7
7
 
8
- ![Permission Prompt Example](https://raw.githubusercontent.com/MasuRii/pi-permission-system/main/asset/pi-permission-system.png)
8
+ <img width="1360" height="752" alt="image" src="https://github.com/user-attachments/assets/3e85190a-17fa-4d94-ac8e-efa54337df5d" />
9
+
9
10
 
10
11
  ## Features
11
12
 
@@ -473,6 +474,13 @@ npm run check # Run lint + test
473
474
 
474
475
  ---
475
476
 
477
+ ## Related Pi Extensions
478
+
479
+ - [pi-multi-auth](https://github.com/MasuRii/pi-multi-auth) — Multi-provider credential management and quota-aware rotation
480
+ - [pi-tool-display](https://github.com/MasuRii/pi-tool-display) — Compact tool rendering and diff visualization
481
+ - [pi-rtk-optimizer](https://github.com/MasuRii/pi-rtk-optimizer) — RTK command rewriting and output compaction
482
+ - [pi-MUST-have-extension](https://github.com/MasuRii/pi-MUST-have-extension) — RFC 2119 keyword normalization for prompts
483
+
476
484
  ## License
477
485
 
478
486
  [MIT](LICENSE)
package/package.json CHANGED
@@ -1,61 +1,65 @@
1
- {
2
- "name": "pi-permission-system",
3
- "version": "0.3.1",
4
- "description": "Permission enforcement extension for the Pi coding agent.",
5
- "type": "module",
6
- "main": "./index.ts",
7
- "exports": {
8
- ".": "./index.ts"
9
- },
10
- "files": [
11
- "index.ts",
12
- "src",
13
- "config.json",
14
- "config/config.example.json",
15
- "schemas/permissions.schema.json",
16
- "asset",
17
- "README.md",
18
- "CHANGELOG.md",
19
- "LICENSE"
20
- ],
21
- "scripts": {
22
- "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
23
- "lint": "npm run build",
24
- "test": "bun ./src/test.ts && bun ./src/config-modal-test.ts",
25
- "check": "npm run lint && npm run test"
26
- },
27
- "keywords": [
28
- "pi-package",
29
- "pi",
30
- "pi-extension",
31
- "permissions",
32
- "policy",
33
- "coding-agent"
34
- ],
35
- "author": "MasuRii",
36
- "license": "MIT",
37
- "repository": {
38
- "type": "git",
39
- "url": "git+https://github.com/MasuRii/pi-permission-system.git"
40
- },
41
- "homepage": "https://github.com/MasuRii/pi-permission-system#readme",
42
- "bugs": {
43
- "url": "https://github.com/MasuRii/pi-permission-system/issues"
44
- },
45
- "engines": {
46
- "node": ">=20"
47
- },
48
- "publishConfig": {
49
- "access": "public"
50
- },
51
- "pi": {
52
- "extensions": [
53
- "./index.ts"
54
- ]
55
- },
56
- "peerDependencies": {
57
- "@mariozechner/pi-coding-agent": "^0.62.0",
58
- "@mariozechner/pi-tui": "^0.62.0",
59
- "@sinclair/typebox": "^0.34.48"
60
- }
61
- }
1
+ {
2
+ "name": "pi-permission-system",
3
+ "version": "0.4.1",
4
+ "description": "Permission enforcement extension for the Pi coding agent.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "config.json",
14
+ "config/config.example.json",
15
+ "schemas/permissions.schema.json",
16
+ "asset",
17
+ "README.md",
18
+ "CHANGELOG.md",
19
+ "LICENSE"
20
+ ],
21
+ "scripts": {
22
+ "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
23
+ "lint": "npm run build",
24
+ "test": "bun ./src/test.ts && bun ./src/config-modal-test.ts",
25
+ "check": "npm run lint && npm run test"
26
+ },
27
+ "keywords": [
28
+ "pi-package",
29
+ "pi",
30
+ "pi-extension",
31
+ "pi-coding-agent",
32
+ "coding-agent",
33
+ "permissions",
34
+ "policy",
35
+ "access-control",
36
+ "authorization",
37
+ "security"
38
+ ],
39
+ "author": "MasuRii",
40
+ "license": "MIT",
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/MasuRii/pi-permission-system.git"
44
+ },
45
+ "homepage": "https://github.com/MasuRii/pi-permission-system#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/MasuRii/pi-permission-system/issues"
48
+ },
49
+ "engines": {
50
+ "node": ">=20"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "pi": {
56
+ "extensions": [
57
+ "./index.ts"
58
+ ]
59
+ },
60
+ "peerDependencies": {
61
+ "@mariozechner/pi-coding-agent": "^0.64.0",
62
+ "@mariozechner/pi-tui": "^0.64.0",
63
+ "@sinclair/typebox": "^0.34.49"
64
+ }
65
+ }
@@ -3,18 +3,59 @@ export interface SanitizeSystemPromptResult {
3
3
  removed: boolean;
4
4
  }
5
5
 
6
- type ToolPromptEntry = {
7
- name: string;
8
- lines: string[];
9
- };
10
-
11
- type ToolPromptSection = {
6
+ type LineSection = {
12
7
  start: number;
13
8
  end: number;
14
- entries: ToolPromptEntry[];
9
+ };
10
+
11
+ type GuidelineRule = {
12
+ matches: (guideline: string) => boolean;
13
+ shouldKeep: (allowedTools: ReadonlySet<string>) => boolean;
15
14
  };
16
15
 
17
16
  const AVAILABLE_TOOLS_SECTION_HEADER = "Available tools:";
17
+ const GUIDELINES_SECTION_HEADER = "Guidelines:";
18
+
19
+ const TOOL_GUIDELINE_RULES: readonly GuidelineRule[] = [
20
+ {
21
+ matches: (guideline) => guideline === "use bash for file operations like ls, rg, find",
22
+ shouldKeep: (allowedTools) => allowedTools.has("bash"),
23
+ },
24
+ {
25
+ matches: (guideline) => guideline === "prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)",
26
+ shouldKeep: (allowedTools) =>
27
+ allowedTools.has("bash") && (allowedTools.has("grep") || allowedTools.has("find") || allowedTools.has("ls")),
28
+ },
29
+ {
30
+ matches: (guideline) =>
31
+ guideline === "use read to examine files before editing. you must use this tool instead of cat or sed."
32
+ || guideline === "use read to examine files instead of cat or sed.",
33
+ shouldKeep: (allowedTools) => allowedTools.has("read"),
34
+ },
35
+ {
36
+ matches: (guideline) => guideline === "use edit for precise changes (old text must match exactly)",
37
+ shouldKeep: (allowedTools) => allowedTools.has("edit"),
38
+ },
39
+ {
40
+ matches: (guideline) => guideline === "use write only for new files or complete rewrites",
41
+ shouldKeep: (allowedTools) => allowedTools.has("write"),
42
+ },
43
+ {
44
+ matches: (guideline) =>
45
+ guideline === "when summarizing your actions, output plain text directly - do not use cat or bash to display what you did",
46
+ shouldKeep: (allowedTools) => allowedTools.has("edit") || allowedTools.has("write"),
47
+ },
48
+ {
49
+ matches: (guideline) =>
50
+ guideline === "use task when work should be delegated to one or more specialized agents instead of handled entirely in the current session.",
51
+ shouldKeep: (allowedTools) => allowedTools.has("task"),
52
+ },
53
+ {
54
+ matches: (guideline) =>
55
+ guideline === "use mcp for mcp discovery first: search by capability, describe one exact tool name, then call it.",
56
+ shouldKeep: (allowedTools) => allowedTools.has("mcp"),
57
+ },
58
+ ];
18
59
 
19
60
  function normalizePrompt(prompt: string): string {
20
61
  return (prompt || "").replace(/\r\n/g, "\n");
@@ -24,77 +65,89 @@ function collapseExtraBlankLines(text: string): string {
24
65
  return text.replace(/\n{3,}/g, "\n\n").trimEnd();
25
66
  }
26
67
 
27
- function parseAvailableToolsSection(systemPrompt: string): ToolPromptSection | null {
28
- const lines = normalizePrompt(systemPrompt).split("\n");
29
- const start = lines.findIndex((line) => line.trim() === AVAILABLE_TOOLS_SECTION_HEADER);
68
+ function normalizeGuidelineText(line: string): string {
69
+ return line.trim().replace(/^[-*]\s+/, "").replace(/\s+/g, " ").toLowerCase();
70
+ }
71
+
72
+ function isTopLevelSectionHeader(line: string): boolean {
73
+ const trimmed = line.trim();
74
+ return trimmed.length > 0 && trimmed.endsWith(":") && !trimmed.startsWith("-");
75
+ }
76
+
77
+ function findSection(lines: readonly string[], header: string): LineSection | null {
78
+ const start = lines.findIndex((line) => line.trim() === header);
30
79
  if (start === -1) {
31
80
  return null;
32
81
  }
33
82
 
34
- const entries: ToolPromptEntry[] = [];
35
- let index = start + 1;
36
-
37
- while (index < lines.length) {
38
- const line = lines[index];
39
- const trimmed = line.trim();
40
-
41
- if (!trimmed) {
42
- index += 1;
43
- continue;
44
- }
45
-
46
- if (!trimmed.startsWith("- ")) {
83
+ let end = lines.length;
84
+ for (let index = start + 1; index < lines.length; index += 1) {
85
+ if (isTopLevelSectionHeader(lines[index])) {
86
+ end = index;
47
87
  break;
48
88
  }
89
+ }
49
90
 
50
- const match = trimmed.match(/^\-\s+([^:]+):/);
51
- if (!match) {
52
- break;
53
- }
91
+ return { start, end };
92
+ }
93
+
94
+ function removeLineSection(lines: readonly string[], section: LineSection | null): { lines: string[]; removed: boolean } {
95
+ if (!section) {
96
+ return { lines: [...lines], removed: false };
97
+ }
54
98
 
55
- const entryLines = [line];
56
- index += 1;
99
+ return {
100
+ lines: [...lines.slice(0, section.start), ...lines.slice(section.end)],
101
+ removed: true,
102
+ };
103
+ }
57
104
 
58
- while (index < lines.length) {
59
- const nextLine = lines[index];
60
- const nextTrimmed = nextLine.trim();
105
+ function shouldKeepGuideline(line: string, allowedTools: ReadonlySet<string>): boolean {
106
+ const normalized = normalizeGuidelineText(line);
61
107
 
62
- if (!nextTrimmed) {
63
- entryLines.push(nextLine);
64
- index += 1;
65
- continue;
66
- }
108
+ for (const rule of TOOL_GUIDELINE_RULES) {
109
+ if (rule.matches(normalized)) {
110
+ return rule.shouldKeep(allowedTools);
111
+ }
112
+ }
67
113
 
68
- if (nextTrimmed.startsWith("- ")) {
69
- break;
70
- }
114
+ return true;
115
+ }
71
116
 
72
- if (!/^\s/.test(nextLine)) {
73
- break;
74
- }
117
+ function sanitizeGuidelinesSection(lines: readonly string[], allowedTools: ReadonlySet<string>): { lines: string[]; removed: boolean } {
118
+ const section = findSection(lines, GUIDELINES_SECTION_HEADER);
119
+ if (!section) {
120
+ return { lines: [...lines], removed: false };
121
+ }
75
122
 
76
- entryLines.push(nextLine);
77
- index += 1;
123
+ const before = lines.slice(0, section.start + 1);
124
+ const after = lines.slice(section.end);
125
+ const body = lines.slice(section.start + 1, section.end);
126
+ const filteredBody = body.filter((line) => {
127
+ const trimmed = line.trim();
128
+ if (!trimmed.startsWith("- ")) {
129
+ return true;
78
130
  }
79
131
 
80
- while (entryLines.length > 0 && entryLines[entryLines.length - 1].trim().length === 0) {
81
- entryLines.pop();
82
- }
132
+ return shouldKeepGuideline(line, allowedTools);
133
+ });
83
134
 
84
- entries.push({
85
- name: match[1].trim(),
86
- lines: entryLines,
87
- });
135
+ const removed = filteredBody.length !== body.length;
136
+ if (!removed) {
137
+ return { lines: [...lines], removed: false };
88
138
  }
89
139
 
90
- if (entries.length === 0) {
91
- return null;
140
+ const hasBullet = filteredBody.some((line) => line.trim().startsWith("- "));
141
+ if (!hasBullet) {
142
+ return {
143
+ lines: [...lines.slice(0, section.start), ...after],
144
+ removed: true,
145
+ };
92
146
  }
93
147
 
94
148
  return {
95
- start,
96
- end: index,
97
- entries,
149
+ lines: [...before, ...filteredBody, ...after],
150
+ removed: true,
98
151
  };
99
152
  }
100
153
 
@@ -102,29 +155,14 @@ export function sanitizeAvailableToolsSection(
102
155
  systemPrompt: string,
103
156
  allowedToolNames: readonly string[],
104
157
  ): SanitizeSystemPromptResult {
105
- const section = parseAvailableToolsSection(systemPrompt);
106
- if (!section) {
107
- return { prompt: systemPrompt, removed: false };
108
- }
109
-
110
158
  const allowedTools = new Set(allowedToolNames.map((toolName) => toolName.trim()).filter(Boolean));
111
- const visibleEntries = section.entries.filter((entry) => allowedTools.has(entry.name));
112
-
113
- if (visibleEntries.length === section.entries.length) {
114
- return { prompt: systemPrompt, removed: false };
115
- }
116
-
117
- const lines = normalizePrompt(systemPrompt).split("\n");
118
- const replacement = visibleEntries.length > 0
119
- ? [lines[section.start], ...visibleEntries.flatMap((entry) => entry.lines)]
120
- : [];
159
+ const normalizedLines = normalizePrompt(systemPrompt).split("\n");
160
+ const removedToolsSection = removeLineSection(normalizedLines, findSection(normalizedLines, AVAILABLE_TOOLS_SECTION_HEADER));
161
+ const sanitizedGuidelines = sanitizeGuidelinesSection(removedToolsSection.lines, allowedTools);
162
+ const removed = removedToolsSection.removed || sanitizedGuidelines.removed;
121
163
 
122
164
  return {
123
- prompt: collapseExtraBlankLines([
124
- ...lines.slice(0, section.start),
125
- ...replacement,
126
- ...lines.slice(section.end),
127
- ].join("\n")),
128
- removed: true,
165
+ prompt: removed ? collapseExtraBlankLines(sanitizedGuidelines.lines.join("\n")) : systemPrompt,
166
+ removed,
129
167
  };
130
168
  }
package/src/test.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  import { PermissionManager } from "./permission-manager.js";
15
15
  import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-registry.js";
16
16
  import { getPermissionSystemStatus } from "./status.js";
17
+ import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer.js";
17
18
  import type { GlobalPermissionConfig } from "./types.js";
18
19
  import { canResolveAskPermissionRequest, shouldAutoApprovePermissionState } from "./yolo-mode.js";
19
20
 
@@ -207,6 +208,64 @@ runTest("Permission-system status is only exposed when yolo mode is enabled", ()
207
208
  );
208
209
  });
209
210
 
211
+ runTest("System prompt sanitizer removes the Available tools section and surrounding boilerplate", () => {
212
+ const prompt = [
213
+ "Available tools:",
214
+ "- read: Read file contents",
215
+ "- mcp: Discover, inspect, and call MCP tools across configured servers",
216
+ "",
217
+ "In addition to the tools above, you may have access to other custom tools depending on the project.",
218
+ "",
219
+ "Guidelines:",
220
+ "- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
221
+ "- Be concise in your responses",
222
+ ].join("\n");
223
+
224
+ const result = sanitizeAvailableToolsSection(prompt, ["read", "mcp"]);
225
+
226
+ assert.equal(result.removed, true);
227
+ assert.equal(result.prompt.includes("Available tools:"), false);
228
+ assert.equal(result.prompt.includes("In addition to the tools above"), false);
229
+ assert.match(result.prompt, /Guidelines:/);
230
+ assert.match(result.prompt, /Use mcp for MCP discovery first/i);
231
+ });
232
+
233
+ runTest("System prompt sanitizer removes denied tool guidelines while keeping global guidance", () => {
234
+ const prompt = [
235
+ "Guidelines:",
236
+ "- Use task when work SHOULD be delegated to one or more specialized agents instead of handled entirely in the current session.",
237
+ "- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
238
+ "- Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)",
239
+ "- Be concise in your responses",
240
+ "- Show file paths clearly when working with files",
241
+ ].join("\n");
242
+
243
+ const result = sanitizeAvailableToolsSection(prompt, ["bash", "grep", "mcp"]);
244
+
245
+ assert.equal(result.removed, true);
246
+ assert.equal(result.prompt.includes("Use task when work SHOULD"), false);
247
+ assert.match(result.prompt, /Use mcp for MCP discovery first/i);
248
+ assert.match(result.prompt, /Prefer grep\/find\/ls tools over bash/i);
249
+ assert.match(result.prompt, /Be concise in your responses/);
250
+ assert.match(result.prompt, /Show file paths clearly when working with files/);
251
+ });
252
+
253
+ runTest("System prompt sanitizer removes inactive built-in write guidance", () => {
254
+ const prompt = [
255
+ "Guidelines:",
256
+ "- Use write only for new files or complete rewrites",
257
+ "- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
258
+ "- Be concise in your responses",
259
+ ].join("\n");
260
+
261
+ const result = sanitizeAvailableToolsSection(prompt, ["read"]);
262
+
263
+ assert.equal(result.removed, true);
264
+ assert.equal(result.prompt.includes("Use write only for new files or complete rewrites"), false);
265
+ assert.equal(result.prompt.includes("do NOT use cat or bash to display what you did"), false);
266
+ assert.match(result.prompt, /Be concise in your responses/);
267
+ });
268
+
210
269
  runTest("Permission-system logger respects debug toggle and keeps review log enabled by default", () => {
211
270
  const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-logs-"));
212
271
  const logsDir = join(baseDir, "logs");