@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
@@ -0,0 +1,305 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import type { PermissionPromptDecision } from "#src/permission-dialog";
3
+ import {
4
+ applyPermissionGate,
5
+ type PermissionGateParams,
6
+ } from "#src/permission-gate";
7
+
8
+ function makeParams(
9
+ overrides: Partial<PermissionGateParams> = {},
10
+ ): PermissionGateParams {
11
+ return {
12
+ state: "allow",
13
+ canConfirm: true,
14
+ promptForApproval: vi.fn<() => Promise<PermissionPromptDecision>>(),
15
+ writeLog: vi.fn(),
16
+ logContext: { source: "test" },
17
+ messages: {
18
+ denyReason: "Denied by policy.",
19
+ unavailableReason: "No interactive UI available.",
20
+ userDeniedReason: (d) =>
21
+ d.denialReason
22
+ ? `User denied. Reason: ${d.denialReason}.`
23
+ : "User denied.",
24
+ },
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe("applyPermissionGate", () => {
30
+ describe("deny branch", () => {
31
+ it("returns block with deny reason when state is deny", async () => {
32
+ const params = makeParams({ state: "deny" });
33
+ const result = await applyPermissionGate(params);
34
+ expect(result).toEqual({
35
+ action: "block",
36
+ reason: "Denied by policy.",
37
+ });
38
+ });
39
+
40
+ it("calls writeLog with policy_denied resolution", async () => {
41
+ const params = makeParams({
42
+ state: "deny",
43
+ logContext: { source: "tool_call", toolName: "bash" },
44
+ });
45
+ await applyPermissionGate(params);
46
+ expect(params.writeLog).toHaveBeenCalledOnce();
47
+ expect(params.writeLog).toHaveBeenCalledWith(
48
+ "permission_request.blocked",
49
+ {
50
+ source: "tool_call",
51
+ toolName: "bash",
52
+ resolution: "policy_denied",
53
+ },
54
+ );
55
+ });
56
+
57
+ it("does not call promptForApproval when state is deny", async () => {
58
+ const params = makeParams({ state: "deny" });
59
+ await applyPermissionGate(params);
60
+ expect(params.promptForApproval).not.toHaveBeenCalled();
61
+ });
62
+ });
63
+
64
+ describe("ask branch — unavailable", () => {
65
+ it("returns block with unavailable reason when canConfirm is false", async () => {
66
+ const params = makeParams({ state: "ask", canConfirm: false });
67
+ const result = await applyPermissionGate(params);
68
+ expect(result).toEqual({
69
+ action: "block",
70
+ reason: "No interactive UI available.",
71
+ });
72
+ });
73
+
74
+ it("calls writeLog with confirmation_unavailable resolution", async () => {
75
+ const params = makeParams({
76
+ state: "ask",
77
+ canConfirm: false,
78
+ logContext: { source: "skill_read", skillName: "foo" },
79
+ });
80
+ await applyPermissionGate(params);
81
+ expect(params.writeLog).toHaveBeenCalledOnce();
82
+ expect(params.writeLog).toHaveBeenCalledWith(
83
+ "permission_request.blocked",
84
+ {
85
+ source: "skill_read",
86
+ skillName: "foo",
87
+ resolution: "confirmation_unavailable",
88
+ },
89
+ );
90
+ });
91
+
92
+ it("does not call promptForApproval when canConfirm is false", async () => {
93
+ const params = makeParams({ state: "ask", canConfirm: false });
94
+ await applyPermissionGate(params);
95
+ expect(params.promptForApproval).not.toHaveBeenCalled();
96
+ });
97
+ });
98
+
99
+ describe("ask branch — user rejects", () => {
100
+ it("returns block with user-denied reason when user rejects", async () => {
101
+ const decision: PermissionPromptDecision = {
102
+ approved: false,
103
+ state: "denied",
104
+ };
105
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
106
+ const params = makeParams({
107
+ state: "ask",
108
+ canConfirm: true,
109
+ promptForApproval,
110
+ });
111
+ const result = await applyPermissionGate(params);
112
+ expect(result).toEqual({ action: "block", reason: "User denied." });
113
+ });
114
+
115
+ it("passes denial reason through userDeniedReason formatter", async () => {
116
+ const decision: PermissionPromptDecision = {
117
+ approved: false,
118
+ state: "denied_with_reason",
119
+ denialReason: "not now",
120
+ };
121
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
122
+ const params = makeParams({
123
+ state: "ask",
124
+ canConfirm: true,
125
+ promptForApproval,
126
+ });
127
+ const result = await applyPermissionGate(params);
128
+ expect(result).toEqual({
129
+ action: "block",
130
+ reason: "User denied. Reason: not now.",
131
+ });
132
+ });
133
+
134
+ it("does not call writeLog when user rejects (logged by promptPermission)", async () => {
135
+ const decision: PermissionPromptDecision = {
136
+ approved: false,
137
+ state: "denied",
138
+ };
139
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
140
+ const params = makeParams({
141
+ state: "ask",
142
+ canConfirm: true,
143
+ promptForApproval,
144
+ });
145
+ await applyPermissionGate(params);
146
+ expect(params.writeLog).not.toHaveBeenCalled();
147
+ });
148
+ });
149
+
150
+ describe("ask branch — user approves", () => {
151
+ it("returns allow when user approves", async () => {
152
+ const decision: PermissionPromptDecision = {
153
+ approved: true,
154
+ state: "approved",
155
+ };
156
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
157
+ const params = makeParams({
158
+ state: "ask",
159
+ canConfirm: true,
160
+ promptForApproval,
161
+ });
162
+ const result = await applyPermissionGate(params);
163
+ expect(result).toEqual({ action: "allow" });
164
+ });
165
+
166
+ it("does not call writeLog when user approves", async () => {
167
+ const decision: PermissionPromptDecision = {
168
+ approved: true,
169
+ state: "approved",
170
+ };
171
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
172
+ const params = makeParams({
173
+ state: "ask",
174
+ canConfirm: true,
175
+ promptForApproval,
176
+ });
177
+ await applyPermissionGate(params);
178
+ expect(params.writeLog).not.toHaveBeenCalled();
179
+ });
180
+ });
181
+
182
+ describe("ask branch — approved_for_session with sessionApproval", () => {
183
+ it("attaches sessionApproval to result when decision is approved_for_session and param provided", async () => {
184
+ const decision: PermissionPromptDecision = {
185
+ approved: true,
186
+ state: "approved_for_session",
187
+ };
188
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
189
+ const params = makeParams({
190
+ state: "ask",
191
+ canConfirm: true,
192
+ promptForApproval,
193
+ sessionApproval: { surface: "bash", pattern: "git *" },
194
+ });
195
+ const result = await applyPermissionGate(params);
196
+ expect(result).toEqual({
197
+ action: "allow",
198
+ sessionApproval: { surface: "bash", pattern: "git *" },
199
+ });
200
+ });
201
+
202
+ it("attaches project persistent approval when decision is approved_for_project", async () => {
203
+ const decision: PermissionPromptDecision = {
204
+ approved: true,
205
+ state: "approved_for_project",
206
+ };
207
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
208
+ const params = makeParams({
209
+ state: "ask",
210
+ canConfirm: true,
211
+ promptForApproval,
212
+ });
213
+ const result = await applyPermissionGate(params);
214
+ expect(result).toEqual({
215
+ action: "allow",
216
+ persistentApprovalScope: "project",
217
+ });
218
+ });
219
+
220
+ it("attaches global persistent approval when decision is approved_globally", async () => {
221
+ const decision: PermissionPromptDecision = {
222
+ approved: true,
223
+ state: "approved_globally",
224
+ };
225
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
226
+ const params = makeParams({
227
+ state: "ask",
228
+ canConfirm: true,
229
+ promptForApproval,
230
+ });
231
+ const result = await applyPermissionGate(params);
232
+ expect(result).toEqual({
233
+ action: "allow",
234
+ persistentApprovalScope: "global",
235
+ });
236
+ });
237
+
238
+ it("does not attach sessionApproval when decision is approved (once)", async () => {
239
+ const decision: PermissionPromptDecision = {
240
+ approved: true,
241
+ state: "approved",
242
+ };
243
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
244
+ const params = makeParams({
245
+ state: "ask",
246
+ canConfirm: true,
247
+ promptForApproval,
248
+ sessionApproval: { surface: "bash", pattern: "git *" },
249
+ });
250
+ const result = await applyPermissionGate(params);
251
+ expect(result).toEqual({ action: "allow" });
252
+ });
253
+
254
+ it("does not attach sessionApproval when no sessionApproval param", async () => {
255
+ const decision: PermissionPromptDecision = {
256
+ approved: true,
257
+ state: "approved_for_session",
258
+ };
259
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
260
+ const params = makeParams({
261
+ state: "ask",
262
+ canConfirm: true,
263
+ promptForApproval,
264
+ });
265
+ const result = await applyPermissionGate(params);
266
+ expect(result).toEqual({ action: "allow" });
267
+ });
268
+
269
+ it("does not attach sessionApproval when user denies", async () => {
270
+ const decision: PermissionPromptDecision = {
271
+ approved: false,
272
+ state: "denied",
273
+ };
274
+ const promptForApproval = vi.fn().mockResolvedValue(decision);
275
+ const params = makeParams({
276
+ state: "ask",
277
+ canConfirm: true,
278
+ promptForApproval,
279
+ sessionApproval: { surface: "bash", pattern: "git *" },
280
+ });
281
+ const result = await applyPermissionGate(params);
282
+ expect(result).toEqual({ action: "block", reason: "User denied." });
283
+ });
284
+ });
285
+
286
+ describe("allow branch", () => {
287
+ it("returns allow immediately when state is allow", async () => {
288
+ const params = makeParams({ state: "allow" });
289
+ const result = await applyPermissionGate(params);
290
+ expect(result).toEqual({ action: "allow" });
291
+ });
292
+
293
+ it("does not call writeLog when state is allow", async () => {
294
+ const params = makeParams({ state: "allow" });
295
+ await applyPermissionGate(params);
296
+ expect(params.writeLog).not.toHaveBeenCalled();
297
+ });
298
+
299
+ it("does not call promptForApproval when state is allow", async () => {
300
+ const params = makeParams({ state: "allow" });
301
+ await applyPermissionGate(params);
302
+ expect(params.promptForApproval).not.toHaveBeenCalled();
303
+ });
304
+ });
305
+ });