pi-permission-system 0.4.5 → 0.4.6

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
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.6] - 2026-04-28
11
+
12
+ ### Added
13
+ - Added bounded, sanitized tool input previews to permission review logs for non-bash/non-MCP tool calls, inspired by PR #10 from @DevkumarPatel.
14
+
15
+ ### Changed
16
+ - Reused the extension's safe JSON serialization path for generic tool approval previews so circular values and BigInts are summarized without raw full-input logging.
17
+ - Updated `@mariozechner/pi-ai`, `@mariozechner/pi-coding-agent`, and `@mariozechner/pi-tui` peer dependencies to `^0.70.5`.
18
+
10
19
  ## [0.4.5] - 2026-04-27
11
20
 
12
21
  ### Fixed
package/README.md CHANGED
@@ -93,6 +93,7 @@ The extension integrates via Pi's lifecycle hooks:
93
93
  - Extension-provided tools like `task`, `mcp`, and third-party tools are handled by exact registered name instead of private built-in hardcodes
94
94
  - When a subagent hits an `ask` permission without direct UI access, the request can be forwarded to the main interactive session for confirmation
95
95
  - Generic extension-tool approval prompts include a bounded input preview; built-in file tools use concise human-readable summaries instead of raw multiline JSON
96
+ - Permission review logs include bounded `toolInputPreview` values for non-bash/non-MCP tool calls so approvals can be audited without writing raw full payloads
96
97
  - Path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) evaluate `special.external_directory` before their normal tool permission when an explicit path points outside `ctx.cwd`
97
98
 
98
99
  ## Configuration
@@ -432,7 +433,7 @@ Default global logs directory: ~/.pi/agent/extensions/pi-permission-system/logs/
432
433
  Actual global logs directory: $PI_CODING_AGENT_DIR/extensions/pi-permission-system/logs when PI_CODING_AGENT_DIR is set
433
434
  ```
434
435
 
435
- - `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history
436
+ - `pi-permission-system-permission-review.jsonl` — enabled by default for permission review/audit history, including bounded `toolInputPreview` values for non-bash/non-MCP tool calls
436
437
  - `pi-permission-system-debug.jsonl` — disabled by default and intended for troubleshooting
437
438
 
438
439
  ### Architecture
package/package.json CHANGED
@@ -1,66 +1,66 @@
1
- {
2
- "name": "pi-permission-system",
3
- "version": "0.4.5",
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
- "tests",
14
- "config.json",
15
- "config/config.example.json",
16
- "schemas/permissions.schema.json",
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 ./tests/permission-system.test.ts && bun ./tests/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-ai": "^0.70.2",
62
- "@mariozechner/pi-coding-agent": "^0.70.2",
63
- "@mariozechner/pi-tui": "^0.70.2",
64
- "@sinclair/typebox": "^0.34.49"
65
- }
66
- }
1
+ {
2
+ "name": "pi-permission-system",
3
+ "version": "0.4.6",
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
+ "tests",
14
+ "config.json",
15
+ "config/config.example.json",
16
+ "schemas/permissions.schema.json",
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 ./tests/permission-system.test.ts && bun ./tests/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-ai": "^0.70.5",
62
+ "@mariozechner/pi-coding-agent": "^0.70.5",
63
+ "@mariozechner/pi-tui": "^0.70.5",
64
+ "@sinclair/typebox": "^0.34.49"
65
+ }
66
+ }
package/src/index.ts CHANGED
@@ -22,7 +22,7 @@ import {
22
22
  savePermissionSystemConfig,
23
23
  type PermissionSystemExtensionConfig,
24
24
  } from "./extension-config.js";
25
- import { createPermissionSystemLogger } from "./logging.js";
25
+ import { createPermissionSystemLogger, safeJsonStringify } from "./logging.js";
26
26
  import { registerPermissionSystemCommand } from "./config-modal.js";
27
27
  import {
28
28
  createPermissionForwardingLocation,
@@ -69,6 +69,7 @@ type PermissionRequestEvent = {
69
69
  path?: string;
70
70
  command?: string;
71
71
  target?: string;
72
+ toolInputPreview?: string;
72
73
  agentName?: string | null;
73
74
  };
74
75
 
@@ -313,6 +314,7 @@ function formatUserDeniedReason(result: PermissionCheckResult, denialReason?: st
313
314
  }
314
315
 
315
316
  const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
317
+ const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
316
318
  const TOOL_TEXT_SUMMARY_MAX_LENGTH = 80;
317
319
 
318
320
  function truncateInlineText(value: string, maxLength: number): string {
@@ -406,30 +408,17 @@ function formatSearchInputForPrompt(toolName: string, input: Record<string, unkn
406
408
  return parts.length > 0 ? `for ${parts.join(", ")}` : "";
407
409
  }
408
410
 
409
- function formatJsonInputForPrompt(input: unknown): string {
410
- if (input === undefined || input === null) {
411
- return "";
412
- }
413
-
414
- if (typeof input === "object" && !Array.isArray(input) && Object.keys(input as Record<string, unknown>).length === 0) {
415
- return "";
416
- }
417
-
418
- let serialized: string;
419
- try {
420
- serialized = JSON.stringify(input);
421
- } catch {
422
- return "";
423
- }
424
-
411
+ function serializeToolInputPreview(input: unknown): string {
412
+ const serialized = safeJsonStringify(input);
425
413
  if (!serialized || serialized === "{}" || serialized === "null") {
426
414
  return "";
427
415
  }
428
416
 
429
- const inline = serialized
430
- .replace(/\\r\\n|\\n|\\r|\\t/g, " ")
431
- .replace(/\s+/g, " ")
432
- .trim();
417
+ return serialized.replace(/\s+/g, " ").trim();
418
+ }
419
+
420
+ function formatJsonInputForPrompt(input: unknown): string {
421
+ const inline = serializeToolInputPreview(input);
433
422
  return inline ? `with input ${truncateInlineText(inline, TOOL_INPUT_PREVIEW_MAX_LENGTH)}` : "";
434
423
  }
435
424
 
@@ -519,10 +508,29 @@ function formatExternalDirectoryUserDeniedReason(
519
508
  return `User denied external directory access for tool '${toolName}' path '${pathValue}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
520
509
  }
521
510
 
522
- function getPermissionLogContext(result: PermissionCheckResult): { command?: string; target?: string } {
511
+ function formatGenericToolInputForLog(input: unknown): string | undefined {
512
+ const inline = serializeToolInputPreview(input);
513
+ return inline ? `input ${truncateInlineText(inline, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)}` : undefined;
514
+ }
515
+
516
+ function getToolInputPreviewForLog(result: PermissionCheckResult, input: unknown): string | undefined {
517
+ if (result.toolName === "bash" || result.toolName === "mcp" || result.source === "mcp") {
518
+ return undefined;
519
+ }
520
+
521
+ if (PATH_BEARING_TOOLS.has(result.toolName)) {
522
+ const inputPreview = formatToolInputForPrompt(result.toolName, input);
523
+ return inputPreview ? truncateInlineText(inputPreview, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH) : undefined;
524
+ }
525
+
526
+ return formatGenericToolInputForLog(input);
527
+ }
528
+
529
+ function getPermissionLogContext(result: PermissionCheckResult, input: unknown): { command?: string; target?: string; toolInputPreview?: string } {
523
530
  return {
524
531
  command: result.command,
525
532
  target: result.target,
533
+ toolInputPreview: getToolInputPreviewForLog(result, input),
526
534
  };
527
535
  }
528
536
 
@@ -1114,6 +1122,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1114
1122
  path?: string;
1115
1123
  command?: string;
1116
1124
  target?: string;
1125
+ toolInputPreview?: string;
1117
1126
  resolution?: string;
1118
1127
  denialReason?: string;
1119
1128
  },
@@ -1129,6 +1138,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1129
1138
  path: details.path ?? null,
1130
1139
  command: details.command ?? null,
1131
1140
  target: details.target ?? null,
1141
+ toolInputPreview: details.toolInputPreview ?? null,
1132
1142
  resolution: details.resolution ?? null,
1133
1143
  denialReason: details.denialReason ?? null,
1134
1144
  });
@@ -1147,6 +1157,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1147
1157
  path?: string;
1148
1158
  command?: string;
1149
1159
  target?: string;
1160
+ toolInputPreview?: string;
1150
1161
  },
1151
1162
  ): Promise<PermissionPromptDecision> => {
1152
1163
  if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
@@ -1162,6 +1173,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1162
1173
  path: details.path,
1163
1174
  command: details.command,
1164
1175
  target: details.target,
1176
+ toolInputPreview: details.toolInputPreview,
1165
1177
  agentName: details.agentName,
1166
1178
  });
1167
1179
  return { approved: true, state: "approved" };
@@ -1179,6 +1191,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1179
1191
  path: details.path,
1180
1192
  command: details.command,
1181
1193
  target: details.target,
1194
+ toolInputPreview: details.toolInputPreview,
1182
1195
  agentName: details.agentName,
1183
1196
  });
1184
1197
 
@@ -1199,6 +1212,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1199
1212
  path: details.path,
1200
1213
  command: details.command,
1201
1214
  target: details.target,
1215
+ toolInputPreview: details.toolInputPreview,
1202
1216
  agentName: details.agentName,
1203
1217
  });
1204
1218
 
@@ -1563,7 +1577,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
1563
1577
  }
1564
1578
 
1565
1579
  const check = permissionManager.checkPermission(toolName, input, agentName ?? undefined);
1566
- const permissionLogContext = getPermissionLogContext(check);
1580
+ const permissionLogContext = getPermissionLogContext(check, input);
1567
1581
 
1568
1582
  if (check.state === "deny") {
1569
1583
  writeReviewLog("permission_request.blocked", {
package/src/logging.ts CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  type PermissionSystemExtensionConfig,
10
10
  } from "./extension-config.js";
11
11
 
12
- function safeJsonStringify(value: unknown): string {
12
+ export function safeJsonStringify(value: unknown): string | undefined {
13
13
  const seen = new WeakSet<object>();
14
14
  return JSON.stringify(value, (_key, currentValue) => {
15
15
  if (currentValue instanceof Error) {
@@ -66,6 +66,9 @@ export function createPermissionSystemLogger(options: PermissionSystemLoggerOpti
66
66
  event,
67
67
  ...details,
68
68
  });
69
+ if (!line) {
70
+ return `Failed to write permission-system ${stream} log '${path}': event could not be serialized.`;
71
+ }
69
72
  appendFileSync(path, `${line}\n`, "utf-8");
70
73
  return undefined;
71
74
  } catch (error) {