@gotgenes/pi-permission-system 5.4.0 → 5.5.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.
@@ -1,25 +1,19 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import { evaluateToolGate } from "../../../src/handlers/gates/tool";
4
- import type { ToolCallContext } from "../../../src/handlers/gates/types";
5
- import type { HandlerDeps } from "../../../src/handlers/types";
6
- import type { PermissionEventBus } from "../../../src/permission-events";
4
+ import type {
5
+ ToolCallContext,
6
+ ToolGateDeps,
7
+ } from "../../../src/handlers/gates/types";
7
8
  import type { PermissionCheckResult } from "../../../src/types";
8
9
 
9
- // ── SDK stubs ──────────────────────────────────────────────────────────────
10
- vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
11
- const original =
12
- await importOriginal<typeof import("@mariozechner/pi-coding-agent")>();
13
- return { ...original };
14
- });
15
-
16
10
  // ── helpers ────────────────────────────────────────────────────────────────
17
11
 
18
12
  function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
19
13
  return {
20
14
  toolName: "read",
21
15
  agentName: null,
22
- input: { path: "/test/project/foo.ts" },
16
+ input: {},
23
17
  toolCallId: "tc-1",
24
18
  cwd: "/test/project",
25
19
  ...overrides,
@@ -35,383 +29,145 @@ function makeCheckResult(
35
29
  toolName: "read",
36
30
  source: "tool",
37
31
  origin: "builtin",
32
+ matchedPattern: "*",
38
33
  ...overrides,
39
34
  };
40
35
  }
41
36
 
42
- function makeEvents(): PermissionEventBus {
43
- return { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) };
44
- }
45
-
46
- function makeRuntime(
47
- overrides: Record<string, unknown> = {},
48
- ): HandlerDeps["runtime"] {
37
+ function makeToolGateDeps(overrides: Partial<ToolGateDeps> = {}): ToolGateDeps {
49
38
  return {
50
- config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
51
- runtimeContext: {} as HandlerDeps["runtime"]["runtimeContext"],
52
- permissionManager: {
53
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
54
- },
55
- sessionRules: {
56
- approve: vi.fn(),
57
- getRuleset: vi.fn().mockReturnValue([]),
58
- clear: vi.fn(),
59
- },
39
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
40
+ getSessionRuleset: vi.fn().mockReturnValue([]),
41
+ approveSessionRule: vi.fn(),
60
42
  writeReviewLog: vi.fn(),
61
- ...overrides,
62
- } as unknown as HandlerDeps["runtime"];
63
- }
64
-
65
- function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
66
- const { runtime: runtimeOverrides, events, ...rest } = overrides;
67
- return {
68
- runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
69
- events: events ?? makeEvents(),
70
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
43
+ emitDecision: vi.fn(),
44
+ canConfirm: vi.fn().mockReturnValue(true),
71
45
  promptPermission: vi
72
46
  .fn()
73
47
  .mockResolvedValue({ approved: true, state: "approved" }),
74
- ...rest,
75
- } as unknown as HandlerDeps;
48
+ ...overrides,
49
+ };
76
50
  }
77
51
 
78
52
  // ── tests ──────────────────────────────────────────────────────────────────
79
53
 
80
54
  describe("evaluateToolGate", () => {
81
- // ── Session-rule hit ─────────────────────────────────────────────────────
82
-
83
- it("allows and emits session_approved on session hit", async () => {
84
- const events = makeEvents();
85
- const deps = makeDeps({
86
- runtime: {
87
- permissionManager: {
88
- checkPermission: vi.fn().mockReturnValue(
89
- makeCheckResult("allow", {
90
- source: "session",
91
- toolName: "bash",
92
- command: "git status",
93
- matchedPattern: "git *",
94
- }),
95
- ),
96
- },
97
- },
98
- events,
99
- });
100
- const tcc = makeTcc({
101
- toolName: "bash",
102
- input: { command: "git status" },
103
- });
104
- const result = await evaluateToolGate(tcc, deps);
55
+ it("allows when policy is allow", async () => {
56
+ const deps = makeToolGateDeps();
57
+ const result = await evaluateToolGate(makeTcc(), deps);
105
58
  expect(result).toEqual({ action: "allow" });
106
- expect(events.emit).toHaveBeenCalledWith(
107
- "permissions:decision",
108
- expect.objectContaining({
109
- surface: "bash",
110
- value: "git status",
111
- result: "allow",
112
- resolution: "session_approved",
113
- matchedPattern: "git *",
114
- }),
115
- );
116
59
  });
117
60
 
118
- it("does NOT record session rule on session hit", async () => {
119
- const sessionRules = {
120
- approve: vi.fn(),
121
- getRuleset: vi.fn().mockReturnValue([]),
122
- clear: vi.fn(),
123
- };
124
- const deps = makeDeps({
125
- runtime: {
126
- permissionManager: {
127
- checkPermission: vi.fn().mockReturnValue(
128
- makeCheckResult("allow", {
129
- source: "session",
130
- matchedPattern: "git *",
131
- }),
132
- ),
133
- },
134
- sessionRules,
135
- },
136
- });
137
- const tcc = makeTcc({
138
- toolName: "bash",
139
- input: { command: "git status" },
61
+ it("blocks when policy is deny", async () => {
62
+ const deps = makeToolGateDeps({
63
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
140
64
  });
141
- await evaluateToolGate(tcc, deps);
142
- expect(sessionRules.approve).not.toHaveBeenCalled();
65
+ const result = await evaluateToolGate(makeTcc(), deps);
66
+ expect(result).toMatchObject({ action: "block" });
143
67
  });
144
68
 
145
- // ── Policy allow ─────────────────────────────────────────────────────────
146
-
147
- it("allows and emits policy_allow", async () => {
148
- const events = makeEvents();
149
- const deps = makeDeps({ events });
150
- const tcc = makeTcc();
151
- const result = await evaluateToolGate(tcc, deps);
69
+ it("allows on session-approved fast path", async () => {
70
+ const deps = makeToolGateDeps({
71
+ checkPermission: vi.fn().mockReturnValue(
72
+ makeCheckResult("allow", {
73
+ source: "session",
74
+ matchedPattern: "git *",
75
+ }),
76
+ ),
77
+ });
78
+ const result = await evaluateToolGate(
79
+ makeTcc({ toolName: "bash", input: { command: "git status" } }),
80
+ deps,
81
+ );
152
82
  expect(result).toEqual({ action: "allow" });
153
- expect(events.emit).toHaveBeenCalledWith(
154
- "permissions:decision",
155
- expect.objectContaining({
156
- result: "allow",
157
- resolution: "policy_allow",
158
- }),
83
+ expect(deps.writeReviewLog).toHaveBeenCalledWith(
84
+ "permission_request.session_approved",
85
+ expect.objectContaining({ resolution: "session_approved" }),
86
+ );
87
+ expect(deps.emitDecision).toHaveBeenCalledWith(
88
+ expect.objectContaining({ resolution: "session_approved" }),
159
89
  );
160
90
  });
161
91
 
162
- // ── Policy deny ──────────────────────────────────────────────────────────
163
-
164
- it("blocks and emits policy_deny", async () => {
165
- const events = makeEvents();
166
- const deps = makeDeps({
167
- runtime: {
168
- permissionManager: {
169
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
170
- },
171
- },
172
- events,
92
+ it("blocks when state is ask but canConfirm is false", async () => {
93
+ const deps = makeToolGateDeps({
94
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
95
+ canConfirm: vi.fn().mockReturnValue(false),
173
96
  });
174
- const tcc = makeTcc();
175
- const result = await evaluateToolGate(tcc, deps);
97
+ const result = await evaluateToolGate(makeTcc(), deps);
176
98
  expect(result).toMatchObject({ action: "block" });
177
- expect(events.emit).toHaveBeenCalledWith(
178
- "permissions:decision",
179
- expect.objectContaining({
180
- result: "deny",
181
- resolution: "policy_deny",
182
- }),
183
- );
184
99
  });
185
100
 
186
- // ── Policy ask user approves once ──────────────────────────────────────
187
-
188
- it("allows and emits user_approved when user approves once", async () => {
189
- const events = makeEvents();
190
- const sessionRules = {
191
- approve: vi.fn(),
192
- getRuleset: vi.fn().mockReturnValue([]),
193
- clear: vi.fn(),
194
- };
195
- const deps = makeDeps({
196
- runtime: {
197
- permissionManager: {
198
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
199
- },
200
- sessionRules,
201
- },
202
- events,
101
+ it("allows when state is ask and user approves", async () => {
102
+ const deps = makeToolGateDeps({
103
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
203
104
  promptPermission: vi
204
105
  .fn()
205
106
  .mockResolvedValue({ approved: true, state: "approved" }),
206
107
  });
207
- const tcc = makeTcc();
208
- const result = await evaluateToolGate(tcc, deps);
209
- expect(result).toEqual({ action: "allow" });
210
- expect(sessionRules.approve).not.toHaveBeenCalled();
211
- expect(events.emit).toHaveBeenCalledWith(
212
- "permissions:decision",
213
- expect.objectContaining({
214
- result: "allow",
215
- resolution: "user_approved",
216
- }),
217
- );
218
- });
219
-
220
- // ── Policy ask — user approves for session ───────────────────────────────
221
-
222
- it("records session rule when user approves for session", async () => {
223
- const events = makeEvents();
224
- const sessionRules = {
225
- approve: vi.fn(),
226
- getRuleset: vi.fn().mockReturnValue([]),
227
- clear: vi.fn(),
228
- };
229
- const deps = makeDeps({
230
- runtime: {
231
- permissionManager: {
232
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
233
- },
234
- sessionRules,
235
- },
236
- events,
237
- promptPermission: vi
238
- .fn()
239
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
240
- });
241
- const tcc = makeTcc();
242
- const result = await evaluateToolGate(tcc, deps);
108
+ const result = await evaluateToolGate(makeTcc(), deps);
243
109
  expect(result).toEqual({ action: "allow" });
244
- expect(sessionRules.approve).toHaveBeenCalledWith("read", "*");
245
- expect(events.emit).toHaveBeenCalledWith(
246
- "permissions:decision",
247
- expect.objectContaining({
248
- resolution: "user_approved_for_session",
249
- }),
250
- );
251
110
  });
252
111
 
253
- // ── Policy ask user denies ─────────────────────────────────────────────
254
-
255
- it("blocks and emits user_denied", async () => {
256
- const events = makeEvents();
257
- const deps = makeDeps({
258
- runtime: {
259
- permissionManager: {
260
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
261
- },
262
- },
263
- events,
112
+ it("blocks when state is ask and user denies", async () => {
113
+ const deps = makeToolGateDeps({
114
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
264
115
  promptPermission: vi
265
116
  .fn()
266
117
  .mockResolvedValue({ approved: false, state: "denied" }),
267
118
  });
268
- const tcc = makeTcc();
269
- const result = await evaluateToolGate(tcc, deps);
119
+ const result = await evaluateToolGate(makeTcc(), deps);
270
120
  expect(result).toMatchObject({ action: "block" });
271
- expect(events.emit).toHaveBeenCalledWith(
272
- "permissions:decision",
273
- expect.objectContaining({
274
- result: "deny",
275
- resolution: "user_denied",
276
- }),
277
- );
278
121
  });
279
122
 
280
- // ── Policy ask no UI ───────────────────────────────────────────────────
281
-
282
- it("blocks and emits confirmation_unavailable when no UI", async () => {
283
- const events = makeEvents();
284
- const deps = makeDeps({
285
- runtime: {
286
- permissionManager: {
287
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
288
- },
289
- },
290
- events,
291
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
123
+ it("approves session rule when user approves for session", async () => {
124
+ const deps = makeToolGateDeps({
125
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
126
+ promptPermission: vi
127
+ .fn()
128
+ .mockResolvedValue({ approved: true, state: "approved_for_session" }),
292
129
  });
293
- const tcc = makeTcc();
294
- const result = await evaluateToolGate(tcc, deps);
295
- expect(result).toMatchObject({ action: "block" });
296
- expect(events.emit).toHaveBeenCalledWith(
297
- "permissions:decision",
298
- expect.objectContaining({
299
- result: "deny",
300
- resolution: "confirmation_unavailable",
301
- }),
302
- );
130
+ await evaluateToolGate(makeTcc(), deps);
131
+ expect(deps.approveSessionRule).toHaveBeenCalled();
303
132
  });
304
133
 
305
- // ── Auto-approved ────────────────────────────────────────────────────────
306
-
307
- it("emits auto_approved resolution when decision is auto-approved", async () => {
308
- const events = makeEvents();
309
- const deps = makeDeps({
310
- runtime: {
311
- permissionManager: {
312
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
313
- },
314
- },
315
- events,
316
- promptPermission: vi.fn().mockResolvedValue({
317
- approved: true,
318
- state: "approved",
319
- autoApproved: true,
320
- }),
134
+ it("emits decision event with correct surface and result", async () => {
135
+ const deps = makeToolGateDeps({
136
+ checkPermission: vi
137
+ .fn()
138
+ .mockReturnValue(
139
+ makeCheckResult("allow", { origin: "global", matchedPattern: "*" }),
140
+ ),
321
141
  });
322
- const tcc = makeTcc();
323
- const result = await evaluateToolGate(tcc, deps);
324
- expect(result).toEqual({ action: "allow" });
325
- expect(events.emit).toHaveBeenCalledWith(
326
- "permissions:decision",
142
+ await evaluateToolGate(makeTcc({ toolName: "write" }), deps);
143
+ expect(deps.emitDecision).toHaveBeenCalledWith(
327
144
  expect.objectContaining({
328
- resolution: "auto_approved",
145
+ surface: "write",
146
+ result: "allow",
147
+ resolution: "policy_allow",
148
+ origin: "global",
329
149
  }),
330
150
  );
331
151
  });
332
152
 
333
- // ── Bash-specific value ──────────────────────────────────────────────────
334
-
335
- it("uses command as decision value for bash tool", async () => {
336
- const events = makeEvents();
337
- const deps = makeDeps({
338
- runtime: {
339
- permissionManager: {
340
- checkPermission: vi.fn().mockReturnValue(
341
- makeCheckResult("allow", {
342
- toolName: "bash",
343
- command: "git status",
344
- }),
345
- ),
346
- },
347
- },
348
- events,
349
- });
350
- const tcc = makeTcc({
351
- toolName: "bash",
352
- input: { command: "git status" },
353
- });
354
- await evaluateToolGate(tcc, deps);
355
- expect(events.emit).toHaveBeenCalledWith(
356
- "permissions:decision",
357
- expect.objectContaining({
153
+ it("passes session ruleset to checkPermission", async () => {
154
+ const sessionRules = [
155
+ {
358
156
  surface: "bash",
359
- value: "git status",
360
- }),
361
- );
362
- });
363
-
364
- // ── MCP-specific value ───────────────────────────────────────────────────
365
-
366
- it("uses target as decision value for mcp tool", async () => {
367
- const events = makeEvents();
368
- const deps = makeDeps({
369
- runtime: {
370
- permissionManager: {
371
- checkPermission: vi.fn().mockReturnValue(
372
- makeCheckResult("allow", {
373
- toolName: "mcp",
374
- target: "exa:search",
375
- }),
376
- ),
377
- },
157
+ pattern: "git *",
158
+ action: "allow" as const,
159
+ origin: "session" as const,
378
160
  },
379
- events,
161
+ ];
162
+ const deps = makeToolGateDeps({
163
+ getSessionRuleset: vi.fn().mockReturnValue(sessionRules),
380
164
  });
381
- const tcc = makeTcc({ toolName: "mcp", input: { tool: "exa:search" } });
382
- await evaluateToolGate(tcc, deps);
383
- expect(events.emit).toHaveBeenCalledWith(
384
- "permissions:decision",
385
- expect.objectContaining({
386
- surface: "mcp",
387
- value: "exa:search",
388
- }),
165
+ await evaluateToolGate(makeTcc({ toolName: "bash" }), deps);
166
+ expect(deps.checkPermission).toHaveBeenCalledWith(
167
+ "bash",
168
+ expect.anything(),
169
+ undefined,
170
+ sessionRules,
389
171
  );
390
172
  });
391
-
392
- // ── Bash unavailable message ─────────────────────────────────────────────
393
-
394
- it("includes command in unavailable message for bash", async () => {
395
- const deps = makeDeps({
396
- runtime: {
397
- permissionManager: {
398
- checkPermission: vi
399
- .fn()
400
- .mockReturnValue(
401
- makeCheckResult("ask", { toolName: "bash", command: "rm -rf /" }),
402
- ),
403
- },
404
- },
405
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
406
- });
407
- const tcc = makeTcc({
408
- toolName: "bash",
409
- input: { command: "rm -rf /" },
410
- });
411
- const result = await evaluateToolGate(tcc, deps);
412
- expect(result.action).toBe("block");
413
- if (result.action === "block") {
414
- expect(result.reason).toContain("rm -rf /");
415
- }
416
- });
417
173
  });
@@ -8,7 +8,7 @@ import { handleInput } from "../../src/handlers/input";
8
8
  import type { HandlerDeps } from "../../src/handlers/types";
9
9
  import type { PermissionDecisionEvent } from "../../src/permission-events";
10
10
  import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
11
- import type { ExtensionRuntime } from "../../src/runtime";
11
+ import type { SessionState } from "../../src/runtime";
12
12
 
13
13
  // ── helpers ────────────────────────────────────────────────────────────────
14
14
 
@@ -38,17 +38,8 @@ function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
38
38
  } as unknown as ExtensionContext;
39
39
  }
40
40
 
41
- function makeRuntime(
42
- state: "allow" | "deny" | "ask" = "allow",
43
- ): ExtensionRuntime {
41
+ function makeSession(state: "allow" | "deny" | "ask" = "allow"): SessionState {
44
42
  return {
45
- agentDir: "/test/agent",
46
- sessionsDir: "/test/agent/sessions",
47
- subagentSessionsDir: "/test/agent/subagent-sessions",
48
- forwardingDir: "/test/agent/sessions/permission-forwarding",
49
- globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
50
- piInfrastructureDirs: ["/test/agent"],
51
- config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
52
43
  runtimeContext: null,
53
44
  permissionManager: {
54
45
  checkPermission: vi.fn().mockReturnValue({
@@ -58,23 +49,17 @@ function makeRuntime(
58
49
  origin: "global",
59
50
  matchedPattern: "*",
60
51
  }),
61
- } as unknown as ExtensionRuntime["permissionManager"],
52
+ } as unknown as SessionState["permissionManager"],
62
53
  activeSkillEntries: [],
63
54
  lastKnownActiveAgentName: null,
64
55
  lastActiveToolsCacheKey: null,
65
56
  lastPromptStateCacheKey: null,
66
- lastConfigWarning: null,
67
57
  sessionRules: {
68
58
  approve: vi.fn(),
69
59
  getRuleset: vi.fn().mockReturnValue([]),
70
60
  clear: vi.fn(),
71
- } as unknown as ExtensionRuntime["sessionRules"],
72
- permissionForwardingContext: null,
73
- permissionForwardingTimer: null,
74
- isProcessingForwardedRequests: false,
75
- writeDebugLog: vi.fn(),
76
- writeReviewLog: vi.fn(),
77
- } as ExtensionRuntime;
61
+ } as unknown as SessionState["sessionRules"],
62
+ };
78
63
  }
79
64
 
80
65
  function makeDeps(
@@ -82,7 +67,11 @@ function makeDeps(
82
67
  overrides: Partial<HandlerDeps> = {},
83
68
  ): HandlerDeps {
84
69
  return {
85
- runtime: makeRuntime(state),
70
+ session: makeSession(state),
71
+ writeDebugLog: vi.fn(),
72
+ writeReviewLog: vi.fn(),
73
+ piInfrastructureDirs: ["/test/agent"],
74
+ getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
86
75
  events: makeEvents(),
87
76
  createPermissionManagerForCwd: vi.fn(),
88
77
  refreshExtensionConfig: vi.fn(),