@komarspn/pi-permission-system 16.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.
Files changed (203) hide show
  1. package/CHANGELOG.md +2234 -0
  2. package/LICENSE +21 -0
  3. package/README.md +158 -0
  4. package/config/config.example.json +39 -0
  5. package/package.json +82 -0
  6. package/schemas/permissions.schema.json +158 -0
  7. package/src/active-agent.ts +72 -0
  8. package/src/async-cache.ts +21 -0
  9. package/src/bash-arity.ts +210 -0
  10. package/src/builtin-tool-input-formatters.ts +82 -0
  11. package/src/canonicalize-path.ts +30 -0
  12. package/src/common.ts +121 -0
  13. package/src/config-loader.ts +432 -0
  14. package/src/config-modal.ts +259 -0
  15. package/src/config-paths.ts +47 -0
  16. package/src/config-reporter.ts +34 -0
  17. package/src/config-store.ts +222 -0
  18. package/src/decision-audit.ts +75 -0
  19. package/src/decision-reporter.ts +41 -0
  20. package/src/denial-messages.ts +232 -0
  21. package/src/expand-home.ts +28 -0
  22. package/src/extension-config.ts +79 -0
  23. package/src/extension-paths.ts +66 -0
  24. package/src/forwarded-permissions/io.ts +404 -0
  25. package/src/forwarded-permissions/permission-forwarder.ts +580 -0
  26. package/src/forwarding-manager.ts +74 -0
  27. package/src/gate-prompter.ts +12 -0
  28. package/src/handlers/before-agent-start.ts +94 -0
  29. package/src/handlers/gates/bash-command.ts +75 -0
  30. package/src/handlers/gates/bash-external-directory.ts +127 -0
  31. package/src/handlers/gates/bash-path-extractor.ts +15 -0
  32. package/src/handlers/gates/bash-path.ts +152 -0
  33. package/src/handlers/gates/bash-program.ts +1143 -0
  34. package/src/handlers/gates/bash-token-classification.ts +105 -0
  35. package/src/handlers/gates/candidate-check.ts +32 -0
  36. package/src/handlers/gates/descriptor.ts +81 -0
  37. package/src/handlers/gates/external-directory-messages.ts +20 -0
  38. package/src/handlers/gates/external-directory.ts +133 -0
  39. package/src/handlers/gates/helpers.ts +76 -0
  40. package/src/handlers/gates/path.ts +91 -0
  41. package/src/handlers/gates/runner.ts +186 -0
  42. package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
  43. package/src/handlers/gates/skill-input.ts +46 -0
  44. package/src/handlers/gates/skill-read.ts +87 -0
  45. package/src/handlers/gates/tool-call-gate-pipeline.ts +129 -0
  46. package/src/handlers/gates/tool.ts +102 -0
  47. package/src/handlers/gates/types.ts +13 -0
  48. package/src/handlers/index.ts +3 -0
  49. package/src/handlers/lifecycle.ts +95 -0
  50. package/src/handlers/permission-gate-handler.ts +190 -0
  51. package/src/handlers/tool-call-boundary.ts +91 -0
  52. package/src/index.ts +225 -0
  53. package/src/input-normalizer.ts +157 -0
  54. package/src/logging.ts +113 -0
  55. package/src/mcp-targets.ts +170 -0
  56. package/src/node-modules-discovery.ts +76 -0
  57. package/src/normalize.ts +43 -0
  58. package/src/path-utils.ts +355 -0
  59. package/src/pattern-suggest.ts +132 -0
  60. package/src/permission-dialog.ts +138 -0
  61. package/src/permission-event-rpc.ts +223 -0
  62. package/src/permission-events.ts +266 -0
  63. package/src/permission-forwarding.ts +188 -0
  64. package/src/permission-gate.ts +94 -0
  65. package/src/permission-manager.ts +392 -0
  66. package/src/permission-merge.ts +32 -0
  67. package/src/permission-prompter.ts +142 -0
  68. package/src/permission-prompts.ts +93 -0
  69. package/src/permission-resolver.ts +109 -0
  70. package/src/permission-session.ts +189 -0
  71. package/src/permission-ui-prompt.ts +127 -0
  72. package/src/permissions-service.ts +63 -0
  73. package/src/persistent-approval-recorder.ts +139 -0
  74. package/src/policy-loader.ts +350 -0
  75. package/src/prompting-gateway.ts +104 -0
  76. package/src/rule.ts +188 -0
  77. package/src/scope-merge.ts +72 -0
  78. package/src/service-lifecycle.ts +49 -0
  79. package/src/service.ts +163 -0
  80. package/src/session-approval-recorder.ts +6 -0
  81. package/src/session-approval.ts +43 -0
  82. package/src/session-logger.ts +91 -0
  83. package/src/session-rules.ts +79 -0
  84. package/src/skill-prompt-sanitizer.ts +292 -0
  85. package/src/status.ts +35 -0
  86. package/src/subagent-context.ts +104 -0
  87. package/src/subagent-lifecycle-events.ts +72 -0
  88. package/src/subagent-registry.ts +105 -0
  89. package/src/synthesize.ts +92 -0
  90. package/src/system-prompt-sanitizer.ts +274 -0
  91. package/src/tool-access-extractor-registry.ts +68 -0
  92. package/src/tool-input-formatter-registry.ts +67 -0
  93. package/src/tool-input-preview.ts +34 -0
  94. package/src/tool-input-prompt-formatters.ts +63 -0
  95. package/src/tool-preview-formatter.ts +207 -0
  96. package/src/tool-registry.ts +148 -0
  97. package/src/types.ts +64 -0
  98. package/src/wildcard-matcher.ts +120 -0
  99. package/src/yolo-mode.ts +30 -0
  100. package/test/active-agent.test.ts +155 -0
  101. package/test/async-cache.test.ts +48 -0
  102. package/test/bash-arity.test.ts +144 -0
  103. package/test/bash-external-directory.test.ts +956 -0
  104. package/test/builtin-tool-input-formatters.test.ts +109 -0
  105. package/test/canonicalize-path.test.ts +93 -0
  106. package/test/common.test.ts +287 -0
  107. package/test/composition-root.test.ts +603 -0
  108. package/test/config-loader.test.ts +740 -0
  109. package/test/config-modal.test.ts +320 -0
  110. package/test/config-paths.test.ts +83 -0
  111. package/test/config-pipeline.test.ts +90 -0
  112. package/test/config-reporter.test.ts +147 -0
  113. package/test/config-store.test.ts +466 -0
  114. package/test/decision-audit.test.ts +72 -0
  115. package/test/decision-reporter.test.ts +112 -0
  116. package/test/denial-messages.test.ts +656 -0
  117. package/test/detect-permissive-bash-fallback.test.ts +56 -0
  118. package/test/expand-home.test.ts +93 -0
  119. package/test/extension-config.test.ts +129 -0
  120. package/test/extension-paths.test.ts +108 -0
  121. package/test/forwarded-permissions/io.test.ts +251 -0
  122. package/test/forwarding-manager.test.ts +194 -0
  123. package/test/handlers/before-agent-start.test.ts +317 -0
  124. package/test/handlers/external-directory-integration.test.ts +623 -0
  125. package/test/handlers/external-directory-session-dedup.test.ts +430 -0
  126. package/test/handlers/external-directory-symlink-acceptance.test.ts +149 -0
  127. package/test/handlers/gates/bash-command-metamorphic.test.ts +83 -0
  128. package/test/handlers/gates/bash-command.test.ts +191 -0
  129. package/test/handlers/gates/bash-external-directory.test.ts +269 -0
  130. package/test/handlers/gates/bash-path.test.ts +337 -0
  131. package/test/handlers/gates/bash-program.test.ts +410 -0
  132. package/test/handlers/gates/bash-token-classification.test.ts +241 -0
  133. package/test/handlers/gates/candidate-check.test.ts +52 -0
  134. package/test/handlers/gates/external-directory-messages.test.ts +61 -0
  135. package/test/handlers/gates/external-directory.test.ts +259 -0
  136. package/test/handlers/gates/helpers.test.ts +177 -0
  137. package/test/handlers/gates/path.test.ts +294 -0
  138. package/test/handlers/gates/runner.test.ts +447 -0
  139. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
  140. package/test/handlers/gates/skill-input.test.ts +131 -0
  141. package/test/handlers/gates/skill-read.test.ts +158 -0
  142. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +252 -0
  143. package/test/handlers/gates/tool.test.ts +223 -0
  144. package/test/handlers/input-events.test.ts +168 -0
  145. package/test/handlers/input.test.ts +199 -0
  146. package/test/handlers/lifecycle.test.ts +221 -0
  147. package/test/handlers/tool-call-boundary.test.ts +145 -0
  148. package/test/handlers/tool-call-events.test.ts +277 -0
  149. package/test/handlers/tool-call.test.ts +395 -0
  150. package/test/handlers/validate-requested-tool.test.ts +92 -0
  151. package/test/helpers/gate-fixtures.ts +323 -0
  152. package/test/helpers/handler-fixtures.ts +335 -0
  153. package/test/helpers/make-fake-pi.ts +100 -0
  154. package/test/helpers/manager-harness.ts +112 -0
  155. package/test/helpers/session-fixtures.ts +204 -0
  156. package/test/input-normalizer.test.ts +367 -0
  157. package/test/logging.test.ts +51 -0
  158. package/test/mcp-targets.test.ts +233 -0
  159. package/test/node-modules-discovery.test.ts +97 -0
  160. package/test/normalize.test.ts +247 -0
  161. package/test/path-utils.test.ts +650 -0
  162. package/test/pattern-suggest.test.ts +248 -0
  163. package/test/permission-dialog.test.ts +241 -0
  164. package/test/permission-event-rpc.test.ts +541 -0
  165. package/test/permission-events.test.ts +402 -0
  166. package/test/permission-forwarder.test.ts +369 -0
  167. package/test/permission-forwarding.test.ts +315 -0
  168. package/test/permission-gate.test.ts +305 -0
  169. package/test/permission-manager-unified.test.ts +3368 -0
  170. package/test/permission-merge.test.ts +61 -0
  171. package/test/permission-prompter.test.ts +518 -0
  172. package/test/permission-prompts.test.ts +363 -0
  173. package/test/permission-resolver.test.ts +265 -0
  174. package/test/permission-session.test.ts +363 -0
  175. package/test/permission-ui-prompt.test.ts +146 -0
  176. package/test/permissions-service.test.ts +177 -0
  177. package/test/persistent-approval-recorder.test.ts +133 -0
  178. package/test/pi-infrastructure-read.test.ts +369 -0
  179. package/test/policy-loader.test.ts +561 -0
  180. package/test/prompting-gateway.test.ts +230 -0
  181. package/test/rule.test.ts +604 -0
  182. package/test/scope-merge.test.ts +116 -0
  183. package/test/service-lifecycle.test.ts +163 -0
  184. package/test/service.test.ts +308 -0
  185. package/test/session-approval.test.ts +75 -0
  186. package/test/session-logger.test.ts +200 -0
  187. package/test/session-rules.test.ts +304 -0
  188. package/test/session-start.test.ts +112 -0
  189. package/test/skill-prompt-sanitizer.test.ts +374 -0
  190. package/test/status.test.ts +10 -0
  191. package/test/subagent-context.test.ts +326 -0
  192. package/test/subagent-lifecycle-events.test.ts +132 -0
  193. package/test/subagent-registry.test.ts +145 -0
  194. package/test/synthesize.test.ts +300 -0
  195. package/test/system-prompt-sanitizer.test.ts +382 -0
  196. package/test/tool-access-extractor-registry.test.ts +77 -0
  197. package/test/tool-input-formatter-registry.test.ts +75 -0
  198. package/test/tool-input-preview.test.ts +129 -0
  199. package/test/tool-input-prompt-formatters.test.ts +115 -0
  200. package/test/tool-preview-formatter.test.ts +458 -0
  201. package/test/tool-registry.test.ts +197 -0
  202. package/test/wildcard-matcher.test.ts +424 -0
  203. package/test/yolo-mode.test.ts +188 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MasuRii and Christopher D. Lasher
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,158 @@
1
+ <p align="center">
2
+ <img src="docs/assets/logo.png" alt="pi-permission-system logo">
3
+ </p>
4
+
5
+ # @gotgenes/pi-permission-system
6
+
7
+ [![npm version](https://img.shields.io/npm/v/@gotgenes/pi-permission-system?style=flat&logo=npm&logoColor=white)](https://www.npmjs.com/package/@gotgenes/pi-permission-system) [![CI](https://img.shields.io/github/actions/workflow/status/gotgenes/pi-packages/ci.yml?style=flat&logo=github&label=CI)](https://github.com/gotgenes/pi-packages/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg?style=flat)](https://opensource.org/licenses/MIT) [![TypeScript](https://img.shields.io/badge/TypeScript-6.x-3178C6?style=flat&logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![pnpm](https://img.shields.io/badge/pnpm-%3E%3D11-F69220?style=flat&logo=pnpm&logoColor=white)](https://pnpm.io/) [![Pi Package](https://img.shields.io/badge/Pi-Package-6366F1?style=flat)](https://pi.mariozechner.at/)
8
+
9
+ Permission enforcement extension for the [Pi](https://pi.mariozechner.at/) coding agent that provides centralized, deterministic permission gates over tool, bash, MCP, skill, and special operations.
10
+
11
+ > **Fork notice:** This package is a full fork of [MasuRii/pi-permission-system](https://github.com/MasuRii/pi-permission-system), published to npm as `@gotgenes/pi-permission-system`.
12
+ > It has diverged substantially from upstream in config format, internal architecture, and permission model.
13
+
14
+ ## What It Does
15
+
16
+ - **Hides disallowed tools** before the agent starts — no wasted turns probing for blocked tools
17
+ - **Enforces allow / ask / deny** at tool-call time with UI confirmation dialogs
18
+ - **Controls bash commands** with wildcard pattern matching (`git *: ask`, `rm -rf *: deny`)
19
+ - **Gates MCP and skill access** at server, tool, and skill-name granularity
20
+ - **Protects sensitive file patterns** — cross-cutting `path` rules deny `.env`, `~/.ssh/*`, etc. across all tools and bash at once
21
+ - **Guards external paths** — prompts before file tools or bash commands reach outside `cwd`
22
+ - **Fails closed** — an internal gate error blocks the tool (with a `gate_error` review-log entry), and an unparseable bash command prompts (`ask`) rather than passing silently
23
+ - **Forwards prompts from subagents** — `ask` policies work even in non-UI execution contexts
24
+ - **Broadcasts UI prompt events** — `permissions:ui_prompt` fires only when the permission system is about to invoke the active user-facing permission UI
25
+ - **Native [`@gotgenes/pi-subagents`](https://github.com/gotgenes/pi-subagents) integration** — in-process child sessions register with the permission system automatically, enabling per-agent policy enforcement and `ask`-state forwarding to the parent UI without configuration
26
+
27
+ ## Install
28
+
29
+ ```bash
30
+ pi install npm:@gotgenes/pi-permission-system
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ 1. Create the global config file at `~/.pi/agent/extensions/pi-permission-system/config.json`:
36
+
37
+ ```jsonc
38
+ {
39
+ "permission": {
40
+ "*": "allow",
41
+ "path": {
42
+ "*": "allow",
43
+ "*.env": "deny",
44
+ "*.env.*": "deny",
45
+ "*.env.example": "allow"
46
+ },
47
+ "bash": {
48
+ "*": "ask",
49
+ "rm -rf *": "deny",
50
+ "sudo *": "ask"
51
+ },
52
+ "external_directory": "ask"
53
+ }
54
+ }
55
+ ```
56
+
57
+ 2. Start Pi — the extension automatically loads and enforces your policy.
58
+
59
+ All permissions use one of three states:
60
+
61
+ | State | Behavior |
62
+ | ------- | ---------------------------------------- |
63
+ | `allow` | Permits the action silently |
64
+ | `deny` | Blocks the action with an error message |
65
+ | `ask` | Prompts the user for confirmation via UI |
66
+
67
+ When the dialog prompts, you can approve once or approve a pattern for the rest of the session.
68
+ See [docs/session-approvals.md](docs/session-approvals.md) for details on session-scoped rules and pattern suggestions.
69
+
70
+ The `path` surface is a cross-cutting gate that applies to **all** file access — Pi tools, bash commands, MCP calls, and extension tools alike.
71
+ Extension and MCP tools that operate on paths (via `input.path`, MCP's `input.arguments.path`, or a registered access extractor) are gated by default, so a `path` deny cannot be overridden by a per-tool allow — making it the right place to protect sensitive files like `.env` or `~/.ssh/*` from every tool at once.
72
+
73
+ For per-tool path patterns (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns are matched against the file path from `input.path`.
74
+ This lets you express rules like "allow reads but deny `.env` files" at the individual tool level.
75
+ When Pi's current working directory is known, relative path inputs also match their cwd-normalized absolute form, so `src/App.jsx` can match both `src/*` and `/workspace/project/*`.
76
+
77
+ The `external_directory` surface is the CWD-boundary gate: it decides whether reaching **outside** the working tree is allowed, and accepts a pattern map so you can allow specific outside-CWD directories without opening up all external access.
78
+ This is the right surface for silencing repeated prompts on a local cache like `~/.cargo/registry` — allow it here, not on `path`:
79
+
80
+ ```jsonc
81
+ {
82
+ "permission": {
83
+ "external_directory": {
84
+ "*": "ask",
85
+ "~/.cargo/registry/*": "allow"
86
+ }
87
+ }
88
+ }
89
+ ```
90
+
91
+ The trailing `*` is greedy and crosses subdirectory boundaries, so it allows every file beneath the directory; a bare `~/.cargo/registry` matches only the directory entry itself.
92
+
93
+ Four layers compose with most-restrictive-wins: `path` (cross-cutting) → `external_directory` (CWD boundary) → per-tool patterns → `bash` command patterns.
94
+ Because `ask` is more restrictive than `allow`, a `path` allow cannot loosen an `external_directory: ask` boundary — allow outside-CWD directories on `external_directory`.
95
+ See [docs/configuration.md](docs/configuration.md) for the full recipe.
96
+
97
+ ## Configuration
98
+
99
+ Config lives in one JSON file per scope:
100
+
101
+ | Scope | Path |
102
+ | ------- | --------------------------------------------------------- |
103
+ | Global | `~/.pi/agent/extensions/pi-permission-system/config.json` |
104
+ | Project | `<cwd>/.pi/extensions/pi-permission-system/config.json` |
105
+
106
+ Project overrides global; per-agent YAML frontmatter overrides both.
107
+
108
+ Within a surface map like `bash` or `mcp`, **last matching rule wins** — put broad catch-alls first and specific overrides after.
109
+
110
+ For the full reference — all surfaces, runtime knobs, per-agent overrides, merge semantics, and common recipes — see [docs/configuration.md](docs/configuration.md).
111
+
112
+ ## Upgrading
113
+
114
+ ### 16.0.0 — the bash gate now fails closed
115
+
116
+ The permission gate fails closed: an internal gate error blocks the tool (with a `gate_error` review-log entry) instead of running it ungated, and a non-empty bash command that cannot be parsed resolves to `ask` (sentinel `<unparseable-bash-command>`) rather than falling through to a permissive top-level `*`.
117
+ Commands that previously slipped through silently on the error or empty-parse path now block or prompt.
118
+
119
+ If you relied on the old permissive behavior for bash, set an explicit permissive bash policy — `"bash": { "*": "allow" }` — which also suppresses the new startup warning emitted when a top-level `"*": "allow"` leaves bash ungated.
120
+
121
+ ## Documentation
122
+
123
+ | Document | Contents |
124
+ | ------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
125
+ | [docs/configuration.md](docs/configuration.md) | Full policy reference, runtime knobs, per-agent overrides, recipes |
126
+ | [docs/session-approvals.md](docs/session-approvals.md) | Session-scoped rules, pattern suggestions, bash arity table |
127
+ | [docs/cross-extension-api.md](docs/cross-extension-api.md) | Cross-extension service accessor, event bus integration, prompt and decision broadcasts |
128
+ | [docs/subagent-integration.md](docs/subagent-integration.md) | Permission forwarding, coexistence with subagent extensions |
129
+ | [docs/guides/permission-frontmatter-for-subagent-extensions.md](docs/guides/permission-frontmatter-for-subagent-extensions.md) | Convention guide for subagent extension authors |
130
+ | [docs/opencode-compatibility.md](docs/opencode-compatibility.md) | OpenCode compatibility — shared concepts, divergences, porting guide |
131
+ | [docs/troubleshooting.md](docs/troubleshooting.md) | Common issues, diagnostic logging, threat model |
132
+ | [docs/migration/legacy-to-flat.md](docs/migration/legacy-to-flat.md) | Migration from pre-v2 config layout |
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ pnpm run check # Type-check TypeScript (no emit)
138
+ pnpm run lint # Biome + ESLint + lint:md
139
+ pnpm run lint:md # rumdl on README and docs
140
+ pnpm run test # Run tests from ./test
141
+ pnpm run test:watch # Run tests in watch mode
142
+ ```
143
+
144
+ ### Pre-commit hooks
145
+
146
+ This project uses [prek](https://prek.j178.dev/) to run Biome, ESLint, and rumdl on staged files before each commit.
147
+ Run `pnpm install` to set up hooks automatically.
148
+
149
+ ## Acknowledgments
150
+
151
+ This project began as a fork of [MasuRii/pi-permission-system](https://github.com/MasuRii/pi-permission-system).
152
+ Thank you to [MasuRii](https://github.com/MasuRii) for the original work that made this possible.
153
+
154
+ Thank you to the [OpenCode](https://opencode.ai) team for the permission model design that inspired the flat config format and evaluation semantics used in this extension.
155
+
156
+ ## License
157
+
158
+ [MIT](LICENSE)
@@ -0,0 +1,39 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/gotgenes/pi-permission-system/main/schemas/permissions.schema.json",
3
+
4
+ "debugLog": false,
5
+ "permissionReviewLog": true,
6
+ "yoloMode": false,
7
+
8
+ "toolInputPreviewMaxLength": 400,
9
+ "toolTextSummaryMaxLength": 120,
10
+
11
+ "piInfrastructureReadPaths": [],
12
+
13
+ "permission": {
14
+ "*": "ask",
15
+ "path": {
16
+ "*": "allow",
17
+ "*.env": "deny",
18
+ "*.env.*": "deny",
19
+ "*.env.example": "allow"
20
+ },
21
+ "read": "allow",
22
+ "write": "deny",
23
+ "edit": "deny",
24
+ "bash": {
25
+ "*": "ask",
26
+ "git *": "ask",
27
+ "git status": "allow",
28
+ "git diff": "allow",
29
+ "npm *": { "action": "deny", "reason": "Use pnpm instead" }
30
+ },
31
+ "mcp": { "*": "ask", "mcp_status": "allow", "mcp_list": "allow" },
32
+ "skill": { "*": "ask" },
33
+ "external_directory": {
34
+ "*": "ask",
35
+ "~/development/*": "allow",
36
+ "~/.cargo/registry/*": "allow"
37
+ }
38
+ }
39
+ }
package/package.json ADDED
@@ -0,0 +1,82 @@
1
+ {
2
+ "name": "@komarspn/pi-permission-system",
3
+ "version": "16.0.2",
4
+ "description": "Permission enforcement extension for the Pi coding agent.",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/service.ts"
8
+ },
9
+ "imports": {
10
+ "#src/*": "./src/*",
11
+ "#test/*": "./test/*"
12
+ },
13
+ "files": [
14
+ "src",
15
+ "test",
16
+ "config/config.example.json",
17
+ "schemas/permissions.schema.json",
18
+ "README.md",
19
+ "CHANGELOG.md",
20
+ "LICENSE"
21
+ ],
22
+ "keywords": [
23
+ "pi-package",
24
+ "pi",
25
+ "pi-extension",
26
+ "pi-coding-agent",
27
+ "coding-agent",
28
+ "permissions",
29
+ "policy",
30
+ "access-control",
31
+ "authorization",
32
+ "security"
33
+ ],
34
+ "author": {
35
+ "name": "Chris Lasher"
36
+ },
37
+ "license": "MIT",
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "git+https://github.com/komarspn/pi-packages.git",
41
+ "directory": "packages/pi-permission-system"
42
+ },
43
+ "homepage": "https://github.com/komarspn/pi-packages/tree/main/packages/pi-permission-system#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/komarspn/pi-packages/issues"
46
+ },
47
+ "engines": {
48
+ "node": ">=22"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ },
53
+ "pi": {
54
+ "extensions": [
55
+ "./src/index.ts"
56
+ ]
57
+ },
58
+ "peerDependencies": {
59
+ "@earendil-works/pi-coding-agent": ">=0.79.0",
60
+ "@earendil-works/pi-tui": ">=0.79.0"
61
+ },
62
+ "devDependencies": {
63
+ "@biomejs/biome": "^2.4.16",
64
+ "@earendil-works/pi-coding-agent": "0.79.1",
65
+ "@earendil-works/pi-tui": "0.79.1",
66
+ "@types/node": "^22.15.3",
67
+ "rumdl": "^0.2.10",
68
+ "typescript": "^6.0.3",
69
+ "vitest": "^4.1.8"
70
+ },
71
+ "dependencies": {
72
+ "tree-sitter-bash": "^0.25.1",
73
+ "web-tree-sitter": "^0.26.9"
74
+ },
75
+ "scripts": {
76
+ "check": "tsc --noEmit",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest",
79
+ "lint:md": "rumdl check *.md docs/**/*.md",
80
+ "lint": "biome check . && eslint . && pnpm run lint:md"
81
+ }
82
+ }
@@ -0,0 +1,158 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://raw.githubusercontent.com/gotgenes/pi-permission-system/main/schemas/permissions.schema.json",
4
+ "title": "PI Permission System Configuration",
5
+ "description": "Unified config file combining runtime knobs and flat permission policy for pi-permission-system.",
6
+ "markdownDescription": "Unified config file combining runtime knobs and flat 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).",
7
+ "type": "object",
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "$schema": {
11
+ "description": "JSON Schema URI for editor autocomplete and validation.",
12
+ "type": "string"
13
+ },
14
+ "debugLog": {
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.",
17
+ "type": "boolean",
18
+ "default": false
19
+ },
20
+ "permissionReviewLog": {
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.",
23
+ "type": "boolean",
24
+ "default": true
25
+ },
26
+ "yoloMode": {
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.",
29
+ "type": "boolean",
30
+ "default": false
31
+ },
32
+ "toolInputPreviewMaxLength": {
33
+ "description": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts. Omit to use the default (200). Set to a large value to disable truncation.",
34
+ "markdownDescription": "Maximum character length of the inline-JSON tool-input preview shown in permission prompts.\n\nOmit to use the default (200). Set to a large value (e.g. `10000`) to effectively disable truncation and see the full input.",
35
+ "type": "integer",
36
+ "minimum": 1
37
+ },
38
+ "toolTextSummaryMaxLength": {
39
+ "description": "Maximum character length of inline pattern/path summaries (e.g. grep patterns, find globs, ls paths) in permission prompts. Omit to use the default (80).",
40
+ "markdownDescription": "Maximum character length of inline pattern/path summaries (e.g. grep patterns, find globs, ls paths) shown in permission prompts.\n\nOmit to use the default (80). Increase this when working with long regexes or deep paths that are being cut off.",
41
+ "type": "integer",
42
+ "minimum": 1
43
+ },
44
+ "piInfrastructureReadPaths": {
45
+ "description": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the external_directory gate. Supports ~ expansion and wildcard patterns (* and ?).",
46
+ "markdownDescription": "Additional directories to auto-allow for reads as Pi infrastructure, bypassing the `external_directory` gate.\n\nThe extension auto-discovers the global node_modules root (walks up from the extension's install path; falls back to `npm root -g` from a dev checkout), Pi's own install directory (via the coding-agent `getPackageDir()` API), `agentDir`, `agentDir/git`, and project-local `.pi/npm/` and `.pi/git/`. Add entries here for edge cases where auto-discovery is insufficient (e.g. custom `npmCommand` pointing to pnpm).\n\nSupports `~`/`$HOME` expansion. Entries may be plain directory prefixes or wildcard patterns using `*` (matches any characters, including `/`) and `?` (matches exactly one character). `**` and `*` are equivalent — both cross directory boundaries.\n\nOn Windows, matching is case-insensitive and tolerant of either path separator.",
47
+ "type": "array",
48
+ "items": {
49
+ "type": "string",
50
+ "minLength": 1
51
+ },
52
+ "default": []
53
+ },
54
+ "permission": {
55
+ "description": "Flat permission policy. Each key is a surface name; values are a PermissionState string (catch-all) or a pattern→action map.",
56
+ "markdownDescription": "Flat permission policy.\n\nEach top-level key is a surface name:\n- `\"*\"` — universal fallback (replaces `defaultPolicy.tools` from the legacy format)\n- Tool names (`read`, `write`, `bash`, `mcp`, `skill`, `external_directory`, `path`, etc.)\n\nA **string** value is shorthand for `{ \"*\": action }` (surface-level catch-all).\nAn **object** value maps wildcard patterns to actions — last matching pattern wins.\n\nFor built-in file tools (`read`, `write`, `edit`, `find`, `grep`, `ls`), patterns are matched against the file path from `input.path`. For example, `\"read\": { \"*\": \"allow\", \"*.env\": \"deny\" }` allows reads but denies `.env` files.\n\nWhen Pi's current working directory is known, relative path inputs also match their cwd-normalized absolute form, so `src/App.jsx` can match both `src/*` and `/workspace/project/*`. Bash path tokens use the effective directory after literal `cd` commands for this matching; non-literal `cd \"$DIR\"` style commands remain conservative.\n\nThe `path` surface is a cross-cutting gate that applies to **all** file access: Pi tools, bash commands, MCP calls (via `input.arguments.path`), and extension tools (via `input.path` or a registered access extractor). A `path` deny cannot be overridden by a per-tool allow. Use it to protect sensitive files (`.env`, `~/.ssh/*`) from all path-aware tools at once.\n\nThe `external_directory` surface gates access **outside** the working directory. Give it a pattern map to allow specific outside-CWD directories without opening all external access — e.g. `\"external_directory\": { \"*\": \"ask\", \"~/.cargo/registry/*\": \"allow\" }` to silence repeated prompts on a local cache. The trailing `*` is greedy and crosses subdirectory boundaries; a bare `~/.cargo/registry` matches only the directory entry itself. Because layers compose with most-restrictive-wins, a `path` allow cannot loosen an `external_directory: ask` boundary — allow outside-CWD directories here, not on `path`.\n\n**Merge order (lowest → highest precedence):** global → project → per-agent frontmatter.",
57
+ "type": "object",
58
+ "propertyNames": {
59
+ "description": "A surface name or the universal fallback key '*'.",
60
+ "type": "string",
61
+ "minLength": 1
62
+ },
63
+ "additionalProperties": {
64
+ "oneOf": [
65
+ {
66
+ "$ref": "#/$defs/permissionState",
67
+ "description": "Catch-all shorthand: equivalent to { \"*\": action }."
68
+ },
69
+ {
70
+ "$ref": "#/$defs/permissionMap",
71
+ "description": "Pattern→action map for this surface."
72
+ }
73
+ ]
74
+ },
75
+ "examples": [
76
+ {
77
+ "*": "ask",
78
+ "path": {
79
+ "*": "allow",
80
+ "*.env": "deny",
81
+ "*.env.*": "deny",
82
+ "*.env.example": "allow"
83
+ },
84
+ "read": "allow",
85
+ "write": "deny",
86
+ "edit": "deny",
87
+ "bash": {
88
+ "*": "ask",
89
+ "git *": "ask",
90
+ "git status": "allow",
91
+ "git diff": "allow"
92
+ },
93
+ "mcp": { "*": "ask", "mcp_status": "allow", "exa:*": "allow" },
94
+ "skill": { "*": "ask", "librarian": "allow" },
95
+ "external_directory": { "*": "ask", "~/.cargo/registry/*": "allow" }
96
+ }
97
+ ]
98
+ }
99
+ },
100
+ "$defs": {
101
+ "permissionState": {
102
+ "description": "A permission decision: allow (permit silently), deny (block with error), or ask (prompt the user for confirmation).",
103
+ "oneOf": [
104
+ {
105
+ "const": "allow",
106
+ "description": "Permit the action silently with no user interaction."
107
+ },
108
+ {
109
+ "const": "deny",
110
+ "description": "Block the action with an error message. The agent is told not to retry."
111
+ },
112
+ {
113
+ "const": "ask",
114
+ "description": "Prompt the user for confirmation via the interactive UI before proceeding."
115
+ }
116
+ ]
117
+ },
118
+ "permissionMap": {
119
+ "description": "A map of wildcard patterns to permission states. Last matching pattern wins.",
120
+ "markdownDescription": "A map of wildcard patterns to permission states.\n\nUse `*` for wildcard matching. When multiple patterns match, the **last matching rule wins** — put broad catch-alls first and specific overrides after them.\n\nPattern keys support home directory expansion:\n- `~/path` or `$HOME/path` — expanded to the OS home directory at match time.\n- `~` or `$HOME` alone — expands to the home directory itself.\n\nThe stored pattern is always shown in logs and approval dialogs as written (e.g. `~/dev/*`).",
121
+ "type": "object",
122
+ "propertyNames": {
123
+ "description": "A non-empty pattern string. Use * for wildcard matching. Prefix with ~/ or $HOME/ for home-relative paths.",
124
+ "type": "string",
125
+ "minLength": 1
126
+ },
127
+ "additionalProperties": {
128
+ "oneOf": [
129
+ {
130
+ "$ref": "#/$defs/permissionState",
131
+ "description": "A permission decision for this pattern."
132
+ },
133
+ {
134
+ "$ref": "#/$defs/denyWithReason",
135
+ "description": "Deny this pattern with an optional custom reason."
136
+ }
137
+ ]
138
+ }
139
+ },
140
+ "denyWithReason": {
141
+ "type": "object",
142
+ "description": "Deny with an optional custom reason shown to the agent when the action is blocked.",
143
+ "properties": {
144
+ "action": {
145
+ "const": "deny",
146
+ "description": "The permission decision \u2014 must be \"deny\"."
147
+ },
148
+ "reason": {
149
+ "type": "string",
150
+ "maxLength": 500,
151
+ "description": "Optional reason shown to the agent when this action is denied."
152
+ }
153
+ },
154
+ "required": ["action"],
155
+ "additionalProperties": false
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Minimal session-entry view: the only fields {@link getActiveAgentName}
3
+ * reads off each entry. Narrowing to this structural slice (rather than the
4
+ * SDK `SessionEntry` discriminated union) keeps callers and test fixtures free
5
+ * of the union's nine unrelated variants.
6
+ */
7
+ export interface SessionEntryView {
8
+ type: string;
9
+ customType?: string;
10
+ data?: unknown;
11
+ }
12
+
13
+ /**
14
+ * Narrow context for {@link getActiveAgentName} — it reads only the session
15
+ * entries. A full `ExtensionContext` satisfies this structurally.
16
+ */
17
+ export interface ActiveAgentContext {
18
+ sessionManager: { getEntries(): readonly SessionEntryView[] };
19
+ }
20
+
21
+ /**
22
+ * Matches the `<active_agent name="...">` tag injected by pi-agent-router
23
+ * into the system prompt to identify which agent definition is active.
24
+ */
25
+ export const ACTIVE_AGENT_TAG_REGEX =
26
+ /<active_agent\s+name=["']([^"']+)["'][^>]*>/i;
27
+
28
+ export function normalizeAgentName(value: unknown): string | null {
29
+ if (typeof value !== "string") {
30
+ return null;
31
+ }
32
+
33
+ const trimmed = value.trim();
34
+ return trimmed ? trimmed : null;
35
+ }
36
+
37
+ export function getActiveAgentName(ctx: ActiveAgentContext): string | null {
38
+ const entries = ctx.sessionManager.getEntries();
39
+ for (let i = entries.length - 1; i >= 0; i--) {
40
+ const entry = entries[i];
41
+ if (entry.type !== "custom" || entry.customType !== "active_agent") {
42
+ continue;
43
+ }
44
+
45
+ const data = entry.data as { name?: unknown } | undefined;
46
+ const normalizedName = normalizeAgentName(data?.name);
47
+ if (normalizedName) {
48
+ return normalizedName;
49
+ }
50
+
51
+ if (data?.name === null) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ export function getActiveAgentNameFromSystemPrompt(
60
+ systemPrompt: string | undefined,
61
+ ): string | null {
62
+ if (!systemPrompt) {
63
+ return null;
64
+ }
65
+
66
+ const match = ACTIVE_AGENT_TAG_REGEX.exec(systemPrompt);
67
+ if (!match?.[1]) {
68
+ return null;
69
+ }
70
+
71
+ return normalizeAgentName(match[1]);
72
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Memoize an async factory, but drop a rejected result so the next call
3
+ * retries.
4
+ *
5
+ * On success the resolved promise is cached and shared across all callers (the
6
+ * factory runs once). On failure the cache is cleared before the rejection is
7
+ * re-thrown, so a transient init failure does not poison the memo for the
8
+ * process lifetime — the next call re-invokes the factory.
9
+ */
10
+ export function memoizeAsyncWithRetry<T>(
11
+ factory: () => Promise<T>,
12
+ ): () => Promise<T> {
13
+ let cached: Promise<T> | null = null;
14
+ return () => {
15
+ cached ??= factory().catch((error: unknown) => {
16
+ cached = null; // poisoned result cleared → next call re-attempts
17
+ throw error;
18
+ });
19
+ return cached;
20
+ };
21
+ }