@gotgenes/pi-permission-system 5.9.0 → 5.11.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.
@@ -5,12 +5,12 @@
5
5
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
6
  import { describe, expect, it, vi } from "vitest";
7
7
 
8
- import { handleToolCall } from "../../src/handlers/tool-call";
9
- import type { HandlerDeps } from "../../src/handlers/types";
8
+ import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
10
9
  import type { PermissionDecisionEvent } from "../../src/permission-events";
11
10
  import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
12
- import type { SessionState } from "../../src/runtime";
13
- import type { PermissionCheckResult } from "../../src/types";
11
+ import type { PermissionSession } from "../../src/permission-session";
12
+ import type { ToolRegistry } from "../../src/tool-registry";
13
+ import type { PermissionCheckResult, PermissionState } from "../../src/types";
14
14
 
15
15
  // ── helpers ────────────────────────────────────────────────────────────────
16
16
 
@@ -67,53 +67,56 @@ function makeCheckResult(
67
67
  };
68
68
  }
69
69
 
70
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
70
+ function makeSession(
71
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
72
+ ): PermissionSession {
71
73
  return {
72
- runtimeContext: null,
73
- permissionManager: {
74
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
75
- } as unknown as SessionState["permissionManager"],
76
- activeSkillEntries: [],
77
- lastKnownActiveAgentName: null,
78
- lastActiveToolsCacheKey: null,
79
- lastPromptStateCacheKey: null,
80
- sessionRules: {
81
- approve: vi.fn(),
82
- getRuleset: vi.fn().mockReturnValue([]),
83
- clear: vi.fn(),
84
- } as unknown as SessionState["sessionRules"],
74
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
75
+ activate: vi.fn(),
76
+ resolveAgentName: vi.fn().mockReturnValue(null),
77
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
78
+ getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
79
+ getSessionRuleset: vi.fn().mockReturnValue([]),
80
+ approveSessionRule: vi.fn(),
81
+ getActiveSkillEntries: vi.fn().mockReturnValue([]),
82
+ getInfrastructureDirs: vi
83
+ .fn()
84
+ .mockReturnValue(["/test/agent", "/test/agent/git"]),
85
+ getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
86
+ canPrompt: vi.fn().mockReturnValue(true),
87
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
85
88
  ...overrides,
86
- };
89
+ } as unknown as PermissionSession;
87
90
  }
88
91
 
89
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
92
+ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
90
93
  return {
91
- session: makeSession(),
92
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
93
- piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
94
- getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
95
- events: makeEvents(),
96
- createPermissionManagerForCwd: vi.fn(),
97
- refreshExtensionConfig: vi.fn(),
98
- logResolvedConfigPaths: vi.fn(),
99
- resolveAgentName: vi.fn().mockReturnValue(null),
100
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
101
- promptPermission: vi
102
- .fn()
103
- .mockResolvedValue({ approved: true, state: "approved" }),
104
- createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
105
- forwarding: { start: vi.fn(), stop: vi.fn() },
106
- stopPermissionRpcHandlers: vi.fn(),
107
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
108
- setActiveTools: vi.fn(),
94
+ getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
95
+ setActive: vi.fn(),
109
96
  ...overrides,
110
97
  };
111
98
  }
112
99
 
100
+ function makeHandler(overrides?: {
101
+ session?: Partial<Record<keyof PermissionSession, unknown>>;
102
+ toolRegistry?: Partial<ToolRegistry>;
103
+ }): {
104
+ handler: PermissionGateHandler;
105
+ events: ReturnType<typeof makeEvents>;
106
+ session: PermissionSession;
107
+ } {
108
+ const session = makeSession(overrides?.session);
109
+ const events = makeEvents();
110
+ const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
111
+ const handler = new PermissionGateHandler(session, events, toolRegistry);
112
+ return { handler, events, session };
113
+ }
114
+
113
115
  /** Extract all permissions:decision payloads from the events.emit mock. */
114
- function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
115
- const emitMock = (deps.events as ReturnType<typeof makeEvents>).emit;
116
- return emitMock.mock.calls
116
+ function getDecisionEvents(
117
+ events: ReturnType<typeof makeEvents>,
118
+ ): PermissionDecisionEvent[] {
119
+ return events.emit.mock.calls
117
120
  .filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
118
121
  .map(([, payload]) => payload as PermissionDecisionEvent);
119
122
  }
@@ -122,24 +125,22 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
122
125
 
123
126
  describe("handleToolCall decision events — policy_allow", () => {
124
127
  it("emits allow with policy_allow when checkPermission returns allow", async () => {
125
- const deps = makeDeps({
126
- session: makeSession({
127
- permissionManager: {
128
- checkPermission: vi.fn().mockReturnValue(
129
- makeCheckResult("allow", {
130
- origin: "global",
131
- matchedPattern: "*",
132
- }),
133
- ),
134
- } as unknown as SessionState["permissionManager"],
135
- }),
128
+ const { handler, events } = makeHandler({
129
+ session: {
130
+ checkPermission: vi.fn().mockReturnValue(
131
+ makeCheckResult("allow", {
132
+ origin: "global",
133
+ matchedPattern: "*",
134
+ }),
135
+ ),
136
+ },
136
137
  });
137
138
 
138
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
139
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
139
140
 
140
- const events = getDecisionEvents(deps);
141
- expect(events).toHaveLength(1);
142
- expect(events[0]).toMatchObject({
141
+ const decisions = getDecisionEvents(events);
142
+ expect(decisions).toHaveLength(1);
143
+ expect(decisions[0]).toMatchObject({
143
144
  surface: "read",
144
145
  result: "allow",
145
146
  resolution: "policy_allow",
@@ -153,24 +154,22 @@ describe("handleToolCall decision events — policy_allow", () => {
153
154
 
154
155
  describe("handleToolCall decision events — policy_deny", () => {
155
156
  it("emits deny with policy_deny when checkPermission returns deny", async () => {
156
- const deps = makeDeps({
157
- session: makeSession({
158
- permissionManager: {
159
- checkPermission: vi.fn().mockReturnValue(
160
- makeCheckResult("deny", {
161
- origin: "project",
162
- matchedPattern: "read",
163
- }),
164
- ),
165
- } as unknown as SessionState["permissionManager"],
166
- }),
157
+ const { handler, events } = makeHandler({
158
+ session: {
159
+ checkPermission: vi.fn().mockReturnValue(
160
+ makeCheckResult("deny", {
161
+ origin: "project",
162
+ matchedPattern: "read",
163
+ }),
164
+ ),
165
+ },
167
166
  });
168
167
 
169
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
168
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
170
169
 
171
- const events = getDecisionEvents(deps);
172
- expect(events).toHaveLength(1);
173
- expect(events[0]).toMatchObject({
170
+ const decisions = getDecisionEvents(events);
171
+ expect(decisions).toHaveLength(1);
172
+ expect(decisions[0]).toMatchObject({
174
173
  surface: "read",
175
174
  result: "deny",
176
175
  resolution: "policy_deny",
@@ -182,28 +181,25 @@ describe("handleToolCall decision events — policy_deny", () => {
182
181
 
183
182
  describe("handleToolCall decision events — session_approved", () => {
184
183
  it("emits allow with session_approved when checkPermission returns source:session", async () => {
185
- const deps = makeDeps({
186
- session: makeSession({
187
- permissionManager: {
188
- checkPermission: vi.fn().mockReturnValue(
189
- makeCheckResult("allow", {
190
- source: "session",
191
- matchedPattern: "git *",
192
- }),
193
- ),
194
- } as unknown as SessionState["permissionManager"],
195
- }),
184
+ const { handler, events } = makeHandler({
185
+ session: {
186
+ checkPermission: vi.fn().mockReturnValue(
187
+ makeCheckResult("allow", {
188
+ source: "session",
189
+ matchedPattern: "git *",
190
+ }),
191
+ ),
192
+ },
196
193
  });
197
194
 
198
- await handleToolCall(
199
- deps,
195
+ await handler.handleToolCall(
200
196
  makeToolCallEvent("bash", { input: { command: "git status" } }),
201
197
  makeCtx(),
202
198
  );
203
199
 
204
- const events = getDecisionEvents(deps);
205
- expect(events).toHaveLength(1);
206
- expect(events[0]).toMatchObject({
200
+ const decisions = getDecisionEvents(events);
201
+ expect(decisions).toHaveLength(1);
202
+ expect(decisions[0]).toMatchObject({
207
203
  surface: "bash",
208
204
  result: "allow",
209
205
  resolution: "session_approved",
@@ -215,44 +211,41 @@ describe("handleToolCall decision events — session_approved", () => {
215
211
 
216
212
  describe("handleToolCall decision events — user_approved", () => {
217
213
  it("emits allow with user_approved when state=ask and user approves once", async () => {
218
- const deps = makeDeps({
219
- session: makeSession({
220
- permissionManager: {
221
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
222
- } as unknown as SessionState["permissionManager"],
223
- }),
224
- promptPermission: vi
225
- .fn()
226
- .mockResolvedValue({ approved: true, state: "approved" }),
214
+ const { handler, events } = makeHandler({
215
+ session: {
216
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
217
+ prompt: vi
218
+ .fn()
219
+ .mockResolvedValue({ approved: true, state: "approved" }),
220
+ },
227
221
  });
228
222
 
229
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
223
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
230
224
 
231
- const events = getDecisionEvents(deps);
232
- expect(events).toHaveLength(1);
233
- expect(events[0]).toMatchObject({
225
+ const decisions = getDecisionEvents(events);
226
+ expect(decisions).toHaveLength(1);
227
+ expect(decisions[0]).toMatchObject({
234
228
  result: "allow",
235
229
  resolution: "user_approved",
236
230
  });
237
231
  });
238
232
 
239
233
  it("emits allow with user_approved_for_session when user approves for session", async () => {
240
- const deps = makeDeps({
241
- session: makeSession({
242
- permissionManager: {
243
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
244
- } as unknown as SessionState["permissionManager"],
245
- }),
246
- promptPermission: vi
247
- .fn()
248
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
234
+ const { handler, events } = makeHandler({
235
+ session: {
236
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
237
+ prompt: vi.fn().mockResolvedValue({
238
+ approved: true,
239
+ state: "approved_for_session",
240
+ }),
241
+ },
249
242
  });
250
243
 
251
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
244
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
252
245
 
253
- const events = getDecisionEvents(deps);
254
- expect(events).toHaveLength(1);
255
- expect(events[0]).toMatchObject({
246
+ const decisions = getDecisionEvents(events);
247
+ expect(decisions).toHaveLength(1);
248
+ expect(decisions[0]).toMatchObject({
256
249
  result: "allow",
257
250
  resolution: "user_approved_for_session",
258
251
  });
@@ -263,22 +256,18 @@ describe("handleToolCall decision events — user_approved", () => {
263
256
 
264
257
  describe("handleToolCall decision events — user_denied", () => {
265
258
  it("emits deny with user_denied when state=ask and user denies", async () => {
266
- const deps = makeDeps({
267
- session: makeSession({
268
- permissionManager: {
269
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
270
- } as unknown as SessionState["permissionManager"],
271
- }),
272
- promptPermission: vi
273
- .fn()
274
- .mockResolvedValue({ approved: false, state: "denied" }),
259
+ const { handler, events } = makeHandler({
260
+ session: {
261
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
262
+ prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
263
+ },
275
264
  });
276
265
 
277
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
266
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
278
267
 
279
- const events = getDecisionEvents(deps);
280
- expect(events).toHaveLength(1);
281
- expect(events[0]).toMatchObject({
268
+ const decisions = getDecisionEvents(events);
269
+ expect(decisions).toHaveLength(1);
270
+ expect(decisions[0]).toMatchObject({
282
271
  result: "deny",
283
272
  resolution: "user_denied",
284
273
  });
@@ -289,24 +278,21 @@ describe("handleToolCall decision events — user_denied", () => {
289
278
 
290
279
  describe("handleToolCall decision events — confirmation_unavailable", () => {
291
280
  it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
292
- const deps = makeDeps({
293
- session: makeSession({
294
- permissionManager: {
295
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
296
- } as unknown as SessionState["permissionManager"],
297
- }),
298
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
281
+ const { handler, events } = makeHandler({
282
+ session: {
283
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
284
+ canPrompt: vi.fn().mockReturnValue(false),
285
+ },
299
286
  });
300
287
 
301
- await handleToolCall(
302
- deps,
288
+ await handler.handleToolCall(
303
289
  makeToolCallEvent("read"),
304
290
  makeCtx({ hasUI: false }),
305
291
  );
306
292
 
307
- const events = getDecisionEvents(deps);
308
- expect(events).toHaveLength(1);
309
- expect(events[0]).toMatchObject({
293
+ const decisions = getDecisionEvents(events);
294
+ expect(decisions).toHaveLength(1);
295
+ expect(decisions[0]).toMatchObject({
310
296
  result: "deny",
311
297
  resolution: "confirmation_unavailable",
312
298
  });
@@ -318,23 +304,20 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
318
304
  describe("handleToolCall decision events — infrastructure_auto_allowed", () => {
319
305
  it("emits allow with infrastructure_auto_allowed for Pi infra reads", async () => {
320
306
  const infraDir = "/test/agent";
321
- const deps = makeDeps({
322
- piInfrastructureDirs: [infraDir],
323
- session: makeSession({
324
- permissionManager: {
325
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
326
- } as unknown as SessionState["permissionManager"],
327
- }),
307
+ const { handler, events } = makeHandler({
308
+ session: {
309
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
310
+ getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
311
+ },
328
312
  });
329
313
 
330
314
  const event = makeToolCallEvent("read", {
331
315
  input: { path: `${infraDir}/some-file.json` },
332
316
  });
333
- await handleToolCall(deps, event, makeCtx());
317
+ await handler.handleToolCall(event, makeCtx());
334
318
 
335
- const events = getDecisionEvents(deps);
336
- // One infrastructure_auto_allowed event + one policy_allow for the normal gate
337
- const infraEvents = events.filter(
319
+ const decisions = getDecisionEvents(events);
320
+ const infraEvents = decisions.filter(
338
321
  (e) => e.resolution === "infrastructure_auto_allowed",
339
322
  );
340
323
  expect(infraEvents).toHaveLength(1);
@@ -348,26 +331,23 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
348
331
  // ── auto_approved path (yolo mode) ───────────────────────────────────
349
332
 
350
333
  describe("handleToolCall decision events — auto_approved", () => {
351
- it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
352
- const deps = makeDeps({
353
- session: makeSession({
354
- permissionManager: {
355
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
356
- } as unknown as SessionState["permissionManager"],
357
- }),
358
- // Simulate what PermissionPrompter returns in yolo mode
359
- promptPermission: vi.fn().mockResolvedValue({
360
- approved: true,
361
- state: "approved",
362
- autoApproved: true,
363
- }),
334
+ it("emits allow with auto_approved when prompt returns autoApproved:true", async () => {
335
+ const { handler, events } = makeHandler({
336
+ session: {
337
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
338
+ prompt: vi.fn().mockResolvedValue({
339
+ approved: true,
340
+ state: "approved",
341
+ autoApproved: true,
342
+ }),
343
+ },
364
344
  });
365
345
 
366
- await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
346
+ await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
367
347
 
368
- const events = getDecisionEvents(deps);
369
- expect(events).toHaveLength(1);
370
- expect(events[0]).toMatchObject({
348
+ const decisions = getDecisionEvents(events);
349
+ expect(decisions).toHaveLength(1);
350
+ expect(decisions[0]).toMatchObject({
371
351
  result: "allow",
372
352
  resolution: "auto_approved",
373
353
  });