@gotgenes/pi-permission-system 3.0.0 → 3.0.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,18 @@ 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
+ ## [3.0.1](https://github.com/gotgenes/pi-permission-system/compare/v3.0.0...v3.0.1) (2026-05-03)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * add descriptions to all JSON schema entities ([cb3a7ce](https://github.com/gotgenes/pi-permission-system/commit/cb3a7ce5257111159312ebb95a9f75dd4e4a9527))
14
+ * enrich JSON schema with examples, defaults, and markdown descriptions ([6f38d7e](https://github.com/gotgenes/pi-permission-system/commit/6f38d7edf01b950f20e2fab8604a398bf725a6c4))
15
+ * plan index.ts split into focused modules ([#21](https://github.com/gotgenes/pi-permission-system/issues/21)) ([ccd736a](https://github.com/gotgenes/pi-permission-system/commit/ccd736a83a4af208044eec925c80555ee645e344))
16
+ * **retro:** add retro notes for issue [#10](https://github.com/gotgenes/pi-permission-system/issues/10) ([31e59d6](https://github.com/gotgenes/pi-permission-system/commit/31e59d6172da7b0e9f02894afd2ba66a292de168))
17
+ * **retro:** correct formatting friction attribution ([#10](https://github.com/gotgenes/pi-permission-system/issues/10)) ([2e96b7b](https://github.com/gotgenes/pi-permission-system/commit/2e96b7bc1007a2ef55f95b29c40b8417c2c6c52f))
18
+ * update plan with Phase 2 unit tests using DI and vitest mocks ([#21](https://github.com/gotgenes/pi-permission-system/issues/21)) ([ad7f5fe](https://github.com/gotgenes/pi-permission-system/commit/ad7f5feeb856c84142a2a4258a9e173c8006532d))
19
+
8
20
  ## [3.0.0](https://github.com/gotgenes/pi-permission-system/compare/v2.0.0...v3.0.0) (2026-05-03)
9
21
 
10
22
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -3,70 +3,138 @@
3
3
  "$id": "https://raw.githubusercontent.com/gotgenes/pi-permission-system/main/schemas/permissions.schema.json",
4
4
  "title": "PI Permission System Configuration",
5
5
  "description": "Unified config file combining runtime knobs and permission policy for pi-permission-system.",
6
+ "markdownDescription": "Unified config file combining runtime knobs and permission policy for [pi-permission-system](https://github.com/gotgenes/pi-permission-system).\n\nPlace at `~/.pi/agent/extensions/pi-permission-system/config.json` (global) or `<project>/.pi/extensions/pi-permission-system/config.json` (project).",
6
7
  "type": "object",
7
8
  "additionalProperties": false,
8
9
  "properties": {
9
10
  "$schema": {
11
+ "description": "JSON Schema URI for editor autocomplete and validation.",
10
12
  "type": "string"
11
13
  },
12
14
  "debugLog": {
13
15
  "description": "Write verbose permission-system diagnostics to the extension logs directory.",
16
+ "markdownDescription": "Write verbose permission-system diagnostics to `logs/pi-permission-system-debug.jsonl` under the extension config directory.",
14
17
  "type": "boolean",
15
18
  "default": false
16
19
  },
17
20
  "permissionReviewLog": {
18
21
  "description": "Write permission request and decision audit events to the extension logs directory.",
22
+ "markdownDescription": "Write permission request and decision audit events to `logs/pi-permission-system-permission-review.jsonl` under the extension config directory.",
19
23
  "type": "boolean",
20
24
  "default": true
21
25
  },
22
26
  "yoloMode": {
23
27
  "description": "Auto-approve ask-state permission checks, including subagent approval forwarding.",
28
+ "markdownDescription": "Auto-approve `ask`-state permission checks, including subagent approval forwarding.\n\n⚠️ **Use with caution** — this disables all interactive confirmation prompts.",
24
29
  "type": "boolean",
25
30
  "default": false
26
31
  },
27
32
  "defaultPolicy": {
33
+ "description": "Fallback permission state per category when no specific rule matches.",
34
+ "markdownDescription": "Fallback permission state per category when no specific rule matches.\n\nAll fields default to `\"ask\"` when omitted.",
28
35
  "type": "object",
29
36
  "additionalProperties": false,
30
37
  "properties": {
31
38
  "tools": {
32
- "$ref": "#/$defs/permissionState"
39
+ "description": "Default permission for registered tools when no exact-name rule matches in the tools map.",
40
+ "markdownDescription": "Default permission for registered tools when no exact-name rule matches in the `tools` map.",
41
+ "$ref": "#/$defs/permissionState",
42
+ "default": "ask"
33
43
  },
34
44
  "bash": {
35
- "$ref": "#/$defs/permissionState"
45
+ "description": "Default permission for bash commands when no pattern in the bash map matches.",
46
+ "markdownDescription": "Default permission for bash commands when no pattern in the `bash` map matches.",
47
+ "$ref": "#/$defs/permissionState",
48
+ "default": "ask"
36
49
  },
37
50
  "mcp": {
38
- "$ref": "#/$defs/permissionState"
51
+ "description": "Default permission for MCP operations when no target pattern in the mcp map matches.",
52
+ "markdownDescription": "Default permission for MCP operations when no target pattern in the `mcp` map matches.",
53
+ "$ref": "#/$defs/permissionState",
54
+ "default": "ask"
39
55
  },
40
56
  "skills": {
41
- "$ref": "#/$defs/permissionState"
57
+ "description": "Default permission for skill loading when no pattern in the skills map matches.",
58
+ "markdownDescription": "Default permission for skill loading when no pattern in the `skills` map matches.",
59
+ "$ref": "#/$defs/permissionState",
60
+ "default": "ask"
42
61
  },
43
62
  "special": {
44
- "$ref": "#/$defs/permissionState"
63
+ "description": "Default permission for special checks (doom_loop, external_directory) when no explicit rule matches.",
64
+ "markdownDescription": "Default permission for special checks (`doom_loop`, `external_directory`) when no explicit rule matches.",
65
+ "$ref": "#/$defs/permissionState",
66
+ "default": "ask"
45
67
  }
46
68
  }
47
69
  },
48
70
  "tools": {
49
71
  "description": "Exact-name permissions for registered tools. Use this map for the canonical Pi built-ins and any extension-provided or third-party tools.",
50
- "$ref": "#/$defs/permissionMap"
72
+ "markdownDescription": "Exact-name permissions for registered tools. Use this map for the canonical Pi built-ins (`read`, `write`, `edit`, `bash`, `grep`, `find`, `ls`) and any extension-provided or third-party tools.\n\nNo wildcards — keys must match the registered tool name exactly.",
73
+ "$ref": "#/$defs/permissionMap",
74
+ "examples": [
75
+ {
76
+ "read": "allow",
77
+ "write": "deny",
78
+ "edit": "ask",
79
+ "mcp": "allow"
80
+ }
81
+ ]
51
82
  },
52
83
  "bash": {
53
- "$ref": "#/$defs/permissionMap"
84
+ "description": "Wildcard pattern permissions for bash commands. Patterns use * globs matched against the full command string. Last matching rule wins.",
85
+ "markdownDescription": "Wildcard pattern permissions for bash commands. Patterns use `*` globs matched against the full command string.\n\n**Last matching rule wins** — put broad catch-all rules first and specific overrides after them.",
86
+ "$ref": "#/$defs/permissionMap",
87
+ "examples": [
88
+ {
89
+ "git status": "allow",
90
+ "git diff": "allow",
91
+ "git *": "ask",
92
+ "rm -rf *": "deny"
93
+ }
94
+ ]
54
95
  },
55
96
  "mcp": {
56
- "description": "Pattern-based permissions for targets invoked through a registered `mcp` tool when available.",
57
- "$ref": "#/$defs/permissionMap"
97
+ "description": "Pattern-based permissions for targets invoked through a registered mcp tool when available.",
98
+ "markdownDescription": "Pattern-based permissions for targets invoked through a registered `mcp` tool when available.\n\nTargets include server names (`myServer`), qualified tool names (`myServer:search`, `myServer_search`), and baseline operations (`mcp_status`, `mcp_list`, `mcp_connect`, `mcp_describe`, `mcp_search`).",
99
+ "$ref": "#/$defs/permissionMap",
100
+ "examples": [
101
+ {
102
+ "mcp_status": "allow",
103
+ "mcp_list": "allow",
104
+ "myServer:*": "ask",
105
+ "dangerousServer": "deny"
106
+ }
107
+ ]
58
108
  },
59
109
  "skills": {
60
- "$ref": "#/$defs/permissionMap"
110
+ "description": "Wildcard pattern permissions for skill names. Controls which skills can be loaded or read from disk.",
111
+ "markdownDescription": "Wildcard pattern permissions for skill names. Controls which skills can be loaded or read from disk.\n\nUse `\"*\": \"ask\"` to require confirmation for all skills, or `\"dangerous-*\": \"deny\"` to block a family of skills.",
112
+ "$ref": "#/$defs/permissionMap",
113
+ "examples": [
114
+ {
115
+ "*": "ask",
116
+ "dangerous-*": "deny"
117
+ }
118
+ ]
61
119
  },
62
120
  "special": {
121
+ "description": "Reserved permission checks for special runtime behaviors.",
63
122
  "type": "object",
64
123
  "additionalProperties": false,
65
124
  "properties": {
66
125
  "doom_loop": {
126
+ "description": "Controls doom loop detection behavior.",
67
127
  "$ref": "#/$defs/permissionState"
68
128
  },
69
129
  "external_directory": {
130
+ "description": "Enforces permission checks for path-bearing file tools (read, write, edit, find, grep, ls) when they target paths outside the active working directory.",
131
+ "markdownDescription": "Enforces permission checks for path-bearing file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`) when they target paths outside the active working directory.\n\nEvaluated **before** the normal tool permission check — so `tools.read: \"allow\"` can permit ordinary reads while `external_directory: \"ask\"` still requires confirmation for paths outside `cwd`.",
132
+ "$ref": "#/$defs/permissionState"
133
+ },
134
+ "tool_call_limit": {
135
+ "description": "Deprecated and ignored. This key has no effect and should be removed from your config.",
136
+ "markdownDescription": "⚠️ **Deprecated and ignored.** This key has no effect and should be removed from your config.",
137
+ "deprecated": true,
70
138
  "$ref": "#/$defs/permissionState"
71
139
  }
72
140
  }
@@ -74,12 +142,28 @@
74
142
  },
75
143
  "$defs": {
76
144
  "permissionState": {
77
- "type": "string",
78
- "enum": ["allow", "deny", "ask"]
145
+ "description": "A permission decision: allow (permit silently), deny (block with error), or ask (prompt the user for confirmation).",
146
+ "oneOf": [
147
+ {
148
+ "const": "allow",
149
+ "description": "Permit the action silently with no user interaction."
150
+ },
151
+ {
152
+ "const": "deny",
153
+ "description": "Block the action with an error message. The agent is told not to retry."
154
+ },
155
+ {
156
+ "const": "ask",
157
+ "description": "Prompt the user for confirmation via the interactive UI before proceeding."
158
+ }
159
+ ]
79
160
  },
80
161
  "permissionMap": {
162
+ "description": "A map of name or wildcard patterns to permission states. Keys are matched against the relevant target (tool name, bash command, MCP target, or skill name).",
163
+ "markdownDescription": "A map of name or wildcard patterns to permission states.\n\nKeys are matched against the relevant target. Use `*` for wildcard matching. When multiple patterns match, the **last matching rule wins**.",
81
164
  "type": "object",
82
165
  "propertyNames": {
166
+ "description": "A non-empty pattern string. Use * for wildcard matching.",
83
167
  "type": "string",
84
168
  "minLength": 1
85
169
  },
@@ -0,0 +1,58 @@
1
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+
3
+ /**
4
+ * Matches the `<active_agent name="...">` tag injected by pi-agent-router
5
+ * into the system prompt to identify which agent definition is active.
6
+ */
7
+ export const ACTIVE_AGENT_TAG_REGEX =
8
+ /<active_agent\s+name=["']([^"']+)["'][^>]*>/i;
9
+
10
+ export function normalizeAgentName(value: unknown): string | null {
11
+ if (typeof value !== "string") {
12
+ return null;
13
+ }
14
+
15
+ const trimmed = value.trim();
16
+ return trimmed ? trimmed : null;
17
+ }
18
+
19
+ export function getActiveAgentName(ctx: ExtensionContext): string | null {
20
+ const entries = ctx.sessionManager.getEntries();
21
+ for (let i = entries.length - 1; i >= 0; i--) {
22
+ const entry = entries[i] as {
23
+ type: string;
24
+ customType?: string;
25
+ data?: unknown;
26
+ };
27
+ if (entry.type !== "custom" || entry.customType !== "active_agent") {
28
+ continue;
29
+ }
30
+
31
+ const data = entry.data as { name?: unknown } | undefined;
32
+ const normalizedName = normalizeAgentName(data?.name);
33
+ if (normalizedName) {
34
+ return normalizedName;
35
+ }
36
+
37
+ if (data?.name === null) {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ export function getActiveAgentNameFromSystemPrompt(
46
+ systemPrompt: string | undefined,
47
+ ): string | null {
48
+ if (!systemPrompt) {
49
+ return null;
50
+ }
51
+
52
+ const match = systemPrompt.match(ACTIVE_AGENT_TAG_REGEX);
53
+ if (!match?.[1]) {
54
+ return null;
55
+ }
56
+
57
+ return normalizeAgentName(match[1]);
58
+ }
@@ -0,0 +1,113 @@
1
+ import { homedir } from "node:os";
2
+ import { join, normalize, resolve, sep } from "node:path";
3
+
4
+ import { getNonEmptyString, toRecord } from "./common.js";
5
+
6
+ export const PATH_BEARING_TOOLS = new Set([
7
+ "read",
8
+ "write",
9
+ "edit",
10
+ "find",
11
+ "grep",
12
+ "ls",
13
+ ]);
14
+
15
+ export function normalizePathForComparison(
16
+ pathValue: string,
17
+ cwd: string,
18
+ ): string {
19
+ const trimmed = pathValue.trim().replace(/^['"]|['"]$/g, "");
20
+ if (!trimmed) {
21
+ return "";
22
+ }
23
+
24
+ let normalizedPath = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
25
+
26
+ if (normalizedPath === "~") {
27
+ normalizedPath = homedir();
28
+ } else if (
29
+ normalizedPath.startsWith("~/") ||
30
+ normalizedPath.startsWith("~\\")
31
+ ) {
32
+ normalizedPath = join(homedir(), normalizedPath.slice(2));
33
+ }
34
+
35
+ const absolutePath = resolve(cwd, normalizedPath);
36
+ const normalizedAbsolutePath = normalize(absolutePath);
37
+ return process.platform === "win32"
38
+ ? normalizedAbsolutePath.toLowerCase()
39
+ : normalizedAbsolutePath;
40
+ }
41
+
42
+ export function isPathWithinDirectory(
43
+ pathValue: string,
44
+ directory: string,
45
+ ): boolean {
46
+ if (!pathValue || !directory) {
47
+ return false;
48
+ }
49
+
50
+ if (pathValue === directory) {
51
+ return true;
52
+ }
53
+
54
+ const prefix = directory.endsWith(sep) ? directory : `${directory}${sep}`;
55
+ return pathValue.startsWith(prefix);
56
+ }
57
+
58
+ export function getPathBearingToolPath(
59
+ toolName: string,
60
+ input: unknown,
61
+ ): string | null {
62
+ if (!PATH_BEARING_TOOLS.has(toolName)) {
63
+ return null;
64
+ }
65
+
66
+ return getNonEmptyString(toRecord(input).path);
67
+ }
68
+
69
+ export function isPathOutsideWorkingDirectory(
70
+ pathValue: string,
71
+ cwd: string,
72
+ ): boolean {
73
+ const normalizedCwd = normalizePathForComparison(cwd, cwd);
74
+ const normalizedPath = normalizePathForComparison(pathValue, cwd);
75
+ return Boolean(
76
+ normalizedCwd &&
77
+ normalizedPath &&
78
+ !isPathWithinDirectory(normalizedPath, normalizedCwd),
79
+ );
80
+ }
81
+
82
+ export function formatExternalDirectoryHardStopHint(): string {
83
+ return "Hard stop: this external directory permission denial is policy-enforced. Do not retry this path, do not attempt a filesystem bypass, and report the block to the user.";
84
+ }
85
+
86
+ export function formatExternalDirectoryAskPrompt(
87
+ toolName: string,
88
+ pathValue: string,
89
+ cwd: string,
90
+ agentName?: string,
91
+ ): string {
92
+ const subject = agentName ? `Agent '${agentName}'` : "Current agent";
93
+ return `${subject} requested tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. Allow this external directory access?`;
94
+ }
95
+
96
+ export function formatExternalDirectoryDenyReason(
97
+ toolName: string,
98
+ pathValue: string,
99
+ cwd: string,
100
+ agentName?: string,
101
+ ): string {
102
+ const subject = agentName ? `Agent '${agentName}'` : "Current agent";
103
+ return `${subject} is not permitted to run tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. ${formatExternalDirectoryHardStopHint()}`;
104
+ }
105
+
106
+ export function formatExternalDirectoryUserDeniedReason(
107
+ toolName: string,
108
+ pathValue: string,
109
+ denialReason?: string,
110
+ ): string {
111
+ const reasonSuffix = denialReason ? ` Reason: ${denialReason}.` : "";
112
+ return `User denied external directory access for tool '${toolName}' path '${pathValue}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
113
+ }