@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.
@@ -1,10 +1,13 @@
1
1
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { describe, expect, it, vi } from "vitest";
3
3
 
4
- import { getEventInput, handleToolCall } from "../../src/handlers/tool-call";
5
- import type { HandlerDeps } from "../../src/handlers/types";
6
- import type { SessionState } from "../../src/runtime";
7
- import type { PermissionCheckResult } from "../../src/types";
4
+ import {
5
+ getEventInput,
6
+ PermissionGateHandler,
7
+ } from "../../src/handlers/permission-gate-handler";
8
+ import type { PermissionSession } from "../../src/permission-session";
9
+ import type { ToolRegistry } from "../../src/tool-registry";
10
+ import type { PermissionCheckResult, PermissionState } from "../../src/types";
8
11
 
9
12
  // ── SDK stubs ──────────────────────────────────────────────────────────────
10
13
  vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
@@ -55,49 +58,58 @@ function makePermissionResult(
55
58
  return { state, toolName: "read", source: "tool", origin: "builtin" };
56
59
  }
57
60
 
58
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
61
+ function makeSession(
62
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
63
+ ): PermissionSession {
59
64
  return {
60
- runtimeContext: null,
61
- permissionManager: {
62
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
63
- } as unknown as SessionState["permissionManager"],
64
- activeSkillEntries: [],
65
- lastKnownActiveAgentName: null,
66
- lastActiveToolsCacheKey: null,
67
- lastPromptStateCacheKey: null,
68
- sessionRules: {
69
- approve: vi.fn(),
70
- getRuleset: vi.fn().mockReturnValue([]),
71
- clear: vi.fn(),
72
- } as unknown as SessionState["sessionRules"],
65
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
66
+ activate: vi.fn(),
67
+ resolveAgentName: vi.fn().mockReturnValue(null),
68
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
69
+ getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
70
+ getSessionRuleset: vi.fn().mockReturnValue([]),
71
+ approveSessionRule: vi.fn(),
72
+ getActiveSkillEntries: vi.fn().mockReturnValue([]),
73
+ getInfrastructureDirs: vi
74
+ .fn()
75
+ .mockReturnValue(["/test/agent", "/test/agent/git"]),
76
+ getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
77
+ canPrompt: vi.fn().mockReturnValue(true),
78
+ prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
73
79
  ...overrides,
80
+ } as unknown as PermissionSession;
81
+ }
82
+
83
+ function makeEvents() {
84
+ return {
85
+ emit: vi.fn(),
86
+ on: vi.fn().mockReturnValue(() => undefined),
74
87
  };
75
88
  }
76
89
 
77
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
90
+ function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
78
91
  return {
79
- session: makeSession(),
80
- logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
81
- piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
82
- getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
83
- createPermissionManagerForCwd: vi.fn(),
84
- refreshExtensionConfig: vi.fn(),
85
- logResolvedConfigPaths: vi.fn(),
86
- resolveAgentName: vi.fn().mockReturnValue(null),
87
- canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
88
- promptPermission: vi
89
- .fn()
90
- .mockResolvedValue({ approved: true, state: "approved" }),
91
- createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
92
- events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
93
- forwarding: { start: vi.fn(), stop: vi.fn() },
94
- stopPermissionRpcHandlers: vi.fn(),
95
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
96
- setActiveTools: vi.fn(),
92
+ getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
93
+ setActive: vi.fn(),
97
94
  ...overrides,
98
95
  };
99
96
  }
100
97
 
98
+ function makeHandler(overrides?: {
99
+ session?: Partial<Record<keyof PermissionSession, unknown>>;
100
+ toolRegistry?: Partial<ToolRegistry>;
101
+ }): {
102
+ handler: PermissionGateHandler;
103
+ session: PermissionSession;
104
+ toolRegistry: ToolRegistry;
105
+ } {
106
+ const session = makeSession(overrides?.session);
107
+ const events = makeEvents();
108
+ const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
109
+ const handler = new PermissionGateHandler(session, events, toolRegistry);
110
+ return { handler, session, toolRegistry };
111
+ }
112
+
101
113
  // ── getEventInput ──────────────────────────────────────────────────────────
102
114
 
103
115
  describe("getEventInput", () => {
@@ -127,24 +139,19 @@ describe("getEventInput", () => {
127
139
  // ── handleToolCall ─────────────────────────────────────────────────────────
128
140
 
129
141
  describe("handleToolCall", () => {
130
- it("sets runtime context", async () => {
131
- const ctx = makeCtx();
132
- const deps = makeDeps();
133
- await handleToolCall(deps, makeToolCallEvent("read"), ctx);
134
- expect(deps.session.runtimeContext).toBe(ctx);
135
- });
136
-
137
- it("starts forwarded permission polling", async () => {
142
+ it("activates session with ctx", async () => {
138
143
  const ctx = makeCtx();
139
- const deps = makeDeps();
140
- await handleToolCall(deps, makeToolCallEvent("read"), ctx);
141
- expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
144
+ const { handler, session } = makeHandler();
145
+ await handler.handleToolCall(makeToolCallEvent("read"), ctx);
146
+ expect(session.activate).toHaveBeenCalledWith(ctx);
142
147
  });
143
148
 
144
149
  it("blocks when tool name cannot be resolved", async () => {
145
- const deps = makeDeps();
146
- // An event with no recognisable name field
147
- const result = await handleToolCall(deps, { type: "tool_call" }, makeCtx());
150
+ const { handler } = makeHandler();
151
+ const result = await handler.handleToolCall(
152
+ { type: "tool_call" },
153
+ makeCtx(),
154
+ );
148
155
  expect(result).toEqual({
149
156
  block: true,
150
157
  reason: expect.stringContaining("tool"),
@@ -152,11 +159,12 @@ describe("handleToolCall", () => {
152
159
  });
153
160
 
154
161
  it("blocks when tool is not registered", async () => {
155
- const deps = makeDeps({
156
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
162
+ const { handler } = makeHandler({
163
+ toolRegistry: {
164
+ getAll: vi.fn().mockReturnValue([{ name: "read" }]),
165
+ },
157
166
  });
158
- const result = await handleToolCall(
159
- deps,
167
+ const result = await handler.handleToolCall(
160
168
  makeToolCallEvent("unknown-tool"),
161
169
  makeCtx(),
162
170
  );
@@ -164,10 +172,8 @@ describe("handleToolCall", () => {
164
172
  });
165
173
 
166
174
  it("returns empty object when tool is allowed", async () => {
167
- // default makeRuntime() has checkPermission → "allow"
168
- const deps = makeDeps();
169
- const result = await handleToolCall(
170
- deps,
175
+ const { handler } = makeHandler();
176
+ const result = await handler.handleToolCall(
171
177
  makeToolCallEvent("read"),
172
178
  makeCtx(),
173
179
  );
@@ -175,17 +181,12 @@ describe("handleToolCall", () => {
175
181
  });
176
182
 
177
183
  it("blocks when tool is denied by policy", async () => {
178
- const deps = makeDeps({
179
- session: makeSession({
180
- permissionManager: {
181
- checkPermission: vi
182
- .fn()
183
- .mockReturnValue(makePermissionResult("deny")),
184
- } as unknown as SessionState["permissionManager"],
185
- }),
184
+ const { handler } = makeHandler({
185
+ session: {
186
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
187
+ },
186
188
  });
187
- const result = await handleToolCall(
188
- deps,
189
+ const result = await handler.handleToolCall(
189
190
  makeToolCallEvent("read"),
190
191
  makeCtx(),
191
192
  );
@@ -205,9 +206,13 @@ describe("handleToolCall — skill-read gate", () => {
205
206
  normalizedLocation: "/skills/librarian/SKILL.md",
206
207
  normalizedBaseDir: "/skills/librarian",
207
208
  };
208
- const deps = makeDeps({
209
- session: makeSession({ activeSkillEntries: [skillEntry] }),
210
- getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
209
+ const { handler } = makeHandler({
210
+ session: {
211
+ getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
212
+ },
213
+ toolRegistry: {
214
+ getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
215
+ },
211
216
  });
212
217
  const event = {
213
218
  type: "tool_call",
@@ -215,7 +220,7 @@ describe("handleToolCall — skill-read gate", () => {
215
220
  toolName: "read",
216
221
  input: { path: "/skills/librarian/SKILL.md" },
217
222
  };
218
- const result = await handleToolCall(deps, event, makeCtx());
223
+ const result = await handler.handleToolCall(event, makeCtx());
219
224
  expect(result).toMatchObject({ block: true });
220
225
  });
221
226
 
@@ -228,9 +233,13 @@ describe("handleToolCall — skill-read gate", () => {
228
233
  normalizedLocation: "/skills/librarian/SKILL.md",
229
234
  normalizedBaseDir: "/skills/librarian",
230
235
  };
231
- const deps = makeDeps({
232
- session: makeSession({ activeSkillEntries: [skillEntry] }),
233
- getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
236
+ const { handler } = makeHandler({
237
+ session: {
238
+ getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
239
+ },
240
+ toolRegistry: {
241
+ getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
242
+ },
234
243
  });
235
244
  const event = {
236
245
  type: "tool_call",
@@ -238,7 +247,7 @@ describe("handleToolCall — skill-read gate", () => {
238
247
  toolName: "read",
239
248
  input: { path: "/test/project/src/index.ts" },
240
249
  };
241
- const result = await handleToolCall(deps, event, makeCtx());
250
+ const result = await handler.handleToolCall(event, makeCtx());
242
251
  expect(result).toEqual({});
243
252
  });
244
253
  });
@@ -247,15 +256,13 @@ describe("handleToolCall — skill-read gate", () => {
247
256
 
248
257
  describe("handleToolCall — external-directory gate", () => {
249
258
  it("blocks a read of a path outside cwd when policy is deny", async () => {
250
- const deps = makeDeps({
251
- session: makeSession({
252
- permissionManager: {
253
- checkPermission: vi
254
- .fn()
255
- .mockReturnValue(makePermissionResult("deny")),
256
- } as unknown as SessionState["permissionManager"],
257
- }),
258
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
259
+ const { handler } = makeHandler({
260
+ session: {
261
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
262
+ },
263
+ toolRegistry: {
264
+ getAll: vi.fn().mockReturnValue([{ name: "read" }]),
265
+ },
259
266
  });
260
267
  const event = {
261
268
  type: "tool_call",
@@ -263,7 +270,7 @@ describe("handleToolCall — external-directory gate", () => {
263
270
  name: "read",
264
271
  input: { path: "/outside/project/file.ts" },
265
272
  };
266
- const result = await handleToolCall(deps, event, makeCtx());
273
+ const result = await handler.handleToolCall(event, makeCtx());
267
274
  expect(result).toMatchObject({ block: true });
268
275
  });
269
276
  });
@@ -272,15 +279,13 @@ describe("handleToolCall — external-directory gate", () => {
272
279
 
273
280
  describe("handleToolCall — bash external-directory gate", () => {
274
281
  it("blocks a bash command referencing an external path when policy is deny", async () => {
275
- const deps = makeDeps({
276
- session: makeSession({
277
- permissionManager: {
278
- checkPermission: vi
279
- .fn()
280
- .mockReturnValue(makePermissionResult("deny")),
281
- } as unknown as SessionState["permissionManager"],
282
- }),
283
- getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
282
+ const { handler } = makeHandler({
283
+ session: {
284
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
285
+ },
286
+ toolRegistry: {
287
+ getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
288
+ },
284
289
  });
285
290
  const event = {
286
291
  type: "tool_call",
@@ -288,7 +293,7 @@ describe("handleToolCall — bash external-directory gate", () => {
288
293
  name: "bash",
289
294
  input: { command: "cat /outside/project/file.ts" },
290
295
  };
291
- const result = await handleToolCall(deps, event, makeCtx());
296
+ const result = await handler.handleToolCall(event, makeCtx());
292
297
  expect(result).toMatchObject({ block: true });
293
298
  });
294
299
  });
@@ -15,8 +15,8 @@ vi.mock("../src/forwarded-permissions/polling", () => ({
15
15
 
16
16
  import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
17
17
  import { DEFAULT_EXTENSION_CONFIG } from "../src/extension-config";
18
- import type { PromptPermissionDetails } from "../src/handlers/types";
19
18
  import type { PermissionPromptDecision } from "../src/permission-dialog";
19
+ import type { PromptPermissionDetails } from "../src/permission-prompter";
20
20
  import {
21
21
  PermissionPrompter,
22
22
  type PermissionPrompterDeps,