@gotgenes/pi-permission-system 5.5.0 → 5.6.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.
@@ -1,25 +1,17 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { evaluateToolGate } from "../../../src/handlers/gates/tool";
3
+ import type { GateDescriptor } from "../../../src/handlers/gates/descriptor";
4
+ import { describeToolGate } from "../../../src/handlers/gates/tool";
4
5
  import type { ToolCallContext } from "../../../src/handlers/gates/types";
5
- import type { HandlerDeps } from "../../../src/handlers/types";
6
- import type { PermissionEventBus } from "../../../src/permission-events";
7
6
  import type { PermissionCheckResult } from "../../../src/types";
8
7
 
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
8
  // ── helpers ────────────────────────────────────────────────────────────────
17
9
 
18
10
  function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
19
11
  return {
20
12
  toolName: "read",
21
13
  agentName: null,
22
- input: { path: "/test/project/foo.ts" },
14
+ input: {},
23
15
  toolCallId: "tc-1",
24
16
  cwd: "/test/project",
25
17
  ...overrides,
@@ -35,383 +27,154 @@ function makeCheckResult(
35
27
  toolName: "read",
36
28
  source: "tool",
37
29
  origin: "builtin",
30
+ matchedPattern: "*",
38
31
  ...overrides,
39
32
  };
40
33
  }
41
34
 
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"] {
49
- 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
- },
60
- 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),
71
- promptPermission: vi
72
- .fn()
73
- .mockResolvedValue({ approved: true, state: "approved" }),
74
- ...rest,
75
- } as unknown as HandlerDeps;
76
- }
77
-
78
35
  // ── tests ──────────────────────────────────────────────────────────────────
79
36
 
80
- 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);
105
- 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
- }),
37
+ describe("describeToolGate", () => {
38
+ it("returns descriptor with tool name as surface for standard tools", () => {
39
+ const desc = describeToolGate(
40
+ makeTcc({ toolName: "read" }),
41
+ makeCheckResult("ask"),
115
42
  );
43
+ expect(desc.surface).toBe("read");
44
+ expect(desc.decision.surface).toBe("read");
116
45
  });
117
46
 
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" },
140
- });
141
- await evaluateToolGate(tcc, deps);
142
- expect(sessionRules.approve).not.toHaveBeenCalled();
143
- });
144
-
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);
152
- expect(result).toEqual({ action: "allow" });
153
- expect(events.emit).toHaveBeenCalledWith(
154
- "permissions:decision",
155
- expect.objectContaining({
156
- result: "allow",
157
- resolution: "policy_allow",
158
- }),
47
+ it("returns descriptor with tool name as decision value for standard tools", () => {
48
+ const desc = describeToolGate(
49
+ makeTcc({ toolName: "write" }),
50
+ makeCheckResult("ask"),
159
51
  );
52
+ expect(desc.decision.value).toBe("write");
160
53
  });
161
54
 
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,
55
+ it("returns bash surface with command in decision.value for bash tools", () => {
56
+ const check = makeCheckResult("ask", {
57
+ toolName: "bash",
58
+ command: "git status",
173
59
  });
174
- const tcc = makeTcc();
175
- const result = await evaluateToolGate(tcc, deps);
176
- expect(result).toMatchObject({ action: "block" });
177
- expect(events.emit).toHaveBeenCalledWith(
178
- "permissions:decision",
179
- expect.objectContaining({
180
- result: "deny",
181
- resolution: "policy_deny",
182
- }),
60
+ const desc = describeToolGate(
61
+ makeTcc({ toolName: "bash", input: { command: "git status" } }),
62
+ check,
183
63
  );
64
+ expect(desc.surface).toBe("bash");
65
+ expect(desc.decision.surface).toBe("bash");
66
+ expect(desc.decision.value).toBe("git status");
184
67
  });
185
68
 
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,
203
- promptPermission: vi
204
- .fn()
205
- .mockResolvedValue({ approved: true, state: "approved" }),
69
+ it("returns mcp surface with target in decision.value for MCP tools", () => {
70
+ const check = makeCheckResult("ask", {
71
+ toolName: "mcp",
72
+ target: "server:tool",
206
73
  });
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
- }),
74
+ const desc = describeToolGate(
75
+ makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
76
+ check,
217
77
  );
78
+ expect(desc.surface).toBe("mcp");
79
+ expect(desc.decision.surface).toBe("mcp");
80
+ expect(desc.decision.value).toBe("server:tool");
218
81
  });
219
82
 
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);
243
- 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
- );
83
+ it("populates messages.denyReason via formatDenyReason", () => {
84
+ const check = makeCheckResult("deny", { toolName: "read" });
85
+ const desc = describeToolGate(makeTcc(), check);
86
+ expect(desc.messages.denyReason).toContain("read");
87
+ expect(desc.messages.denyReason).toContain("not permitted");
251
88
  });
252
89
 
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,
264
- promptPermission: vi
265
- .fn()
266
- .mockResolvedValue({ approved: false, state: "denied" }),
90
+ it("populates messages.unavailableReason with bash command when tool is bash", () => {
91
+ const check = makeCheckResult("ask", {
92
+ toolName: "bash",
93
+ command: "rm -rf /",
267
94
  });
268
- const tcc = makeTcc();
269
- const result = await evaluateToolGate(tcc, deps);
270
- expect(result).toMatchObject({ action: "block" });
271
- expect(events.emit).toHaveBeenCalledWith(
272
- "permissions:decision",
273
- expect.objectContaining({
274
- result: "deny",
275
- resolution: "user_denied",
276
- }),
95
+ const desc = describeToolGate(
96
+ makeTcc({ toolName: "bash", input: { command: "rm -rf /" } }),
97
+ check,
277
98
  );
99
+ expect(desc.messages.unavailableReason).toContain("rm -rf /");
100
+ expect(desc.messages.unavailableReason).toContain("no interactive UI");
278
101
  });
279
102
 
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),
292
- });
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
- }),
103
+ it("populates messages.unavailableReason with tool name for non-bash tools", () => {
104
+ const desc = describeToolGate(
105
+ makeTcc({ toolName: "write" }),
106
+ makeCheckResult("ask"),
302
107
  );
108
+ expect(desc.messages.unavailableReason).toContain("write");
109
+ expect(desc.messages.unavailableReason).toContain("no interactive UI");
303
110
  });
304
111
 
305
- // ── Auto-approved ────────────────────────────────────────────────────────
112
+ it("populates messages.unavailableReason with mcp for mcp tool", () => {
113
+ const check = makeCheckResult("ask", { toolName: "mcp", target: "s:t" });
114
+ const desc = describeToolGate(makeTcc({ toolName: "mcp" }), check);
115
+ expect(desc.messages.unavailableReason).toContain("mcp");
116
+ });
306
117
 
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
- }),
118
+ it("populates messages.userDeniedReason as a function", () => {
119
+ const check = makeCheckResult("ask", { toolName: "read" });
120
+ const desc = describeToolGate(makeTcc(), check);
121
+ const reason = desc.messages.userDeniedReason({
122
+ approved: false,
123
+ state: "denied",
124
+ denialReason: "too risky",
321
125
  });
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",
327
- expect.objectContaining({
328
- resolution: "auto_approved",
329
- }),
330
- );
126
+ expect(reason).toContain("too risky");
331
127
  });
332
128
 
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({
129
+ it("populates sessionApproval via suggestSessionPattern", () => {
130
+ const check = makeCheckResult("ask", {
351
131
  toolName: "bash",
352
- input: { command: "git status" },
132
+ command: "git status",
353
133
  });
354
- await evaluateToolGate(tcc, deps);
355
- expect(events.emit).toHaveBeenCalledWith(
356
- "permissions:decision",
357
- expect.objectContaining({
358
- surface: "bash",
359
- value: "git status",
360
- }),
134
+ const desc = describeToolGate(
135
+ makeTcc({ toolName: "bash", input: { command: "git status" } }),
136
+ check,
361
137
  );
138
+ expect(desc.sessionApproval).toBeDefined();
139
+ expect(desc.sessionApproval!).toHaveProperty("surface", "bash");
140
+ expect(desc.sessionApproval!).toHaveProperty("pattern");
362
141
  });
363
142
 
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
- },
378
- },
379
- events,
380
- });
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
- }),
143
+ it("populates promptDetails with correct fields", () => {
144
+ const check = makeCheckResult("ask");
145
+ const desc = describeToolGate(
146
+ makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
147
+ check,
389
148
  );
149
+ expect(desc.promptDetails).toMatchObject({
150
+ source: "tool_call",
151
+ agentName: "my-agent",
152
+ toolCallId: "tc-42",
153
+ toolName: "read",
154
+ });
155
+ expect(desc.promptDetails.message).toBeDefined();
156
+ expect(desc.promptDetails.sessionLabel).toBeDefined();
390
157
  });
391
158
 
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({
159
+ it("populates logContext with tool input preview fields", () => {
160
+ const check = makeCheckResult("ask", { toolName: "bash", command: "ls" });
161
+ const desc = describeToolGate(
162
+ makeTcc({ toolName: "bash", input: { command: "ls" } }),
163
+ check,
164
+ );
165
+ expect(desc.logContext).toMatchObject({
166
+ source: "tool_call",
408
167
  toolName: "bash",
409
- input: { command: "rm -rf /" },
410
168
  });
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
- }
169
+ expect(desc.logContext.command).toBe("ls");
170
+ });
171
+
172
+ it("uses toolName as input for checkPermission surface", () => {
173
+ const desc = describeToolGate(
174
+ makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
175
+ makeCheckResult("ask", { toolName: "edit" }),
176
+ );
177
+ expect(desc.surface).toBe("edit");
178
+ expect(desc.input).toEqual({ path: "/a.ts" });
416
179
  });
417
180
  });
@@ -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(),