pi-permission-system 0.3.0 → 0.4.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 CHANGED
@@ -5,6 +5,38 @@ 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.0] - 2026-04-01
9
+
10
+ ### Added
11
+ - System prompt sanitizer now removes inactive tool guidelines from the `Guidelines:` section
12
+ - Guideline filtering based on allowed tools (e.g., removes task/mcp/bash/write guidance when tools are denied)
13
+ - New `TOOL_GUIDELINE_RULES` configuration for extensible guideline filtering
14
+ - Helper functions: `findSection()`, `removeLineSection()`, `sanitizeGuidelinesSection()`
15
+
16
+ ### Changed
17
+ - Updated `@mariozechner/pi-coding-agent` and `@mariozechner/pi-tui` peer dependencies to ^0.64.0
18
+ - Updated `@sinclair/typebox` peer dependency to ^0.34.49
19
+ - Refactored system prompt sanitizer to handle both `Available tools:` and `Guidelines:` sections
20
+
21
+ ### Tests
22
+ - Added tests for system prompt sanitizer removing Available tools section
23
+ - Added tests for guideline filtering based on allowed tools
24
+ - Added tests for inactive built-in write/edit/task/mcp guidance removal
25
+
26
+ ## [0.3.1] - 2026-03-24
27
+
28
+ ### Added
29
+ - Permission system status module (`status.ts`) to expose yolo mode status to the UI
30
+ - `syncPermissionSystemStatus()` function to sync status with the TUI status bar
31
+ - `PERMISSION_SYSTEM_STATUS_KEY` and `PERMISSION_SYSTEM_YOLO_STATUS_VALUE` constants for status identification
32
+
33
+ ### Changed
34
+ - Integrated status sync on config load, config save, and extension unload
35
+ - Status is only exposed when yolo mode is enabled
36
+
37
+ ### Tests
38
+ - Added test for permission-system status being undefined when yolo mode is disabled and "yolo" when enabled
39
+
8
40
  ## [0.3.0] - 2026-03-23
9
41
 
10
42
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🔐 pi-permission-system
2
2
 
3
- [![Version](https://img.shields.io/badge/version-0.3.0-blue.svg)](package.json)
3
+ [![Version](https://img.shields.io/badge/version-0.3.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.
package/package.json CHANGED
@@ -1,61 +1,61 @@
1
- {
2
- "name": "pi-permission-system",
3
- "version": "0.3.0",
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.0",
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.64.0",
58
+ "@mariozechner/pi-tui": "^0.64.0",
59
+ "@sinclair/typebox": "^0.34.49"
60
+ }
61
+ }
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import { PermissionManager } from "./permission-manager.js";
29
29
  import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer.js";
30
30
  import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-registry.js";
31
31
  import type { PermissionCheckResult, PermissionState } from "./types.js";
32
+ import { PERMISSION_SYSTEM_STATUS_KEY, syncPermissionSystemStatus } from "./status.js";
32
33
  import { canResolveAskPermissionRequest, shouldAutoApprovePermissionState } from "./yolo-mode.js";
33
34
 
34
35
  const PI_AGENT_DIR = join(homedir(), ".pi", "agent");
@@ -906,6 +907,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
906
907
  const result = loadPermissionSystemConfig();
907
908
  setExtensionConfig(result.config);
908
909
 
910
+ if (runtimeContext?.hasUI) {
911
+ syncPermissionSystemStatus(runtimeContext, result.config);
912
+ }
913
+
909
914
  if (result.warning && result.warning !== lastConfigWarning) {
910
915
  lastConfigWarning = result.warning;
911
916
  notifyWarning(result.warning);
@@ -933,6 +938,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
933
938
  }
934
939
 
935
940
  setExtensionConfig(normalized);
941
+ syncPermissionSystemStatus(ctx, normalized);
936
942
  lastConfigWarning = null;
937
943
 
938
944
  writeDebugLog("config.saved", {
@@ -1142,6 +1148,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1142
1148
  });
1143
1149
 
1144
1150
  pi.on("session_shutdown", async () => {
1151
+ runtimeContext?.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
1145
1152
  runtimeContext = null;
1146
1153
  stopForwardedPermissionPolling();
1147
1154
  });
package/src/status.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ import { EXTENSION_ID, type PermissionSystemExtensionConfig } from "./extension-config.js";
4
+ import { isYoloModeEnabled } from "./yolo-mode.js";
5
+
6
+ export const PERMISSION_SYSTEM_STATUS_KEY = EXTENSION_ID;
7
+ export const PERMISSION_SYSTEM_YOLO_STATUS_VALUE = "yolo";
8
+
9
+ type PermissionStatusContext = Pick<ExtensionContext, "hasUI" | "ui"> | Pick<ExtensionCommandContext, "ui">;
10
+
11
+ export function getPermissionSystemStatus(config: PermissionSystemExtensionConfig): string | undefined {
12
+ return isYoloModeEnabled(config) ? PERMISSION_SYSTEM_YOLO_STATUS_VALUE : undefined;
13
+ }
14
+
15
+ export function syncPermissionSystemStatus(
16
+ ctx: PermissionStatusContext,
17
+ config: PermissionSystemExtensionConfig,
18
+ ): void {
19
+ ctx.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, getPermissionSystemStatus(config));
20
+ }
@@ -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
@@ -13,6 +13,8 @@ import {
13
13
  } from "./permission-forwarding.js";
14
14
  import { PermissionManager } from "./permission-manager.js";
15
15
  import { checkRequestedToolRegistration, getToolNameFromValue } from "./tool-registry.js";
16
+ import { getPermissionSystemStatus } from "./status.js";
17
+ import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer.js";
16
18
  import type { GlobalPermissionConfig } from "./types.js";
17
19
  import { canResolveAskPermissionRequest, shouldAutoApprovePermissionState } from "./yolo-mode.js";
18
20
 
@@ -198,6 +200,72 @@ runTest("Yolo mode resolves ask permissions without UI or delegation forwarding"
198
200
  );
199
201
  });
200
202
 
203
+ runTest("Permission-system status is only exposed when yolo mode is enabled", () => {
204
+ assert.equal(getPermissionSystemStatus(DEFAULT_EXTENSION_CONFIG), undefined);
205
+ assert.equal(
206
+ getPermissionSystemStatus({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: true }),
207
+ "yolo",
208
+ );
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
+
201
269
  runTest("Permission-system logger respects debug toggle and keeps review log enabled by default", () => {
202
270
  const baseDir = mkdtempSync(join(tmpdir(), "pi-permission-system-logs-"));
203
271
  const logsDir = join(baseDir, "logs");