@gotgenes/pi-permission-system 5.9.0 → 5.10.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.
@@ -9,8 +9,8 @@ import { handleToolCall } from "../../src/handlers/tool-call";
9
9
  import type { HandlerDeps } from "../../src/handlers/types";
10
10
  import type { PermissionDecisionEvent } from "../../src/permission-events";
11
11
  import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
12
- import type { SessionState } from "../../src/runtime";
13
- import type { PermissionCheckResult } from "../../src/types";
12
+ import type { PermissionSession } from "../../src/permission-session";
13
+ import type { PermissionCheckResult, PermissionState } from "../../src/types";
14
14
 
15
15
  // ── helpers ────────────────────────────────────────────────────────────────
16
16
 
@@ -67,42 +67,35 @@ 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([]),
85
86
  ...overrides,
86
- };
87
+ } as unknown as PermissionSession;
87
88
  }
88
89
 
89
90
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
90
91
  return {
91
92
  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
93
  events: makeEvents(),
96
- createPermissionManagerForCwd: vi.fn(),
97
- refreshExtensionConfig: vi.fn(),
98
- logResolvedConfigPaths: vi.fn(),
99
- resolveAgentName: vi.fn().mockReturnValue(null),
100
94
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
101
95
  promptPermission: vi
102
96
  .fn()
103
97
  .mockResolvedValue({ approved: true, state: "approved" }),
104
98
  createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
105
- forwarding: { start: vi.fn(), stop: vi.fn() },
106
99
  stopPermissionRpcHandlers: vi.fn(),
107
100
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
108
101
  setActiveTools: vi.fn(),
@@ -122,18 +115,15 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
122
115
 
123
116
  describe("handleToolCall decision events — policy_allow", () => {
124
117
  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
- }),
118
+ const session = makeSession({
119
+ checkPermission: vi.fn().mockReturnValue(
120
+ makeCheckResult("allow", {
121
+ origin: "global",
122
+ matchedPattern: "*",
123
+ }),
124
+ ),
136
125
  });
126
+ const deps = makeDeps({ session });
137
127
 
138
128
  await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
139
129
 
@@ -153,18 +143,15 @@ describe("handleToolCall decision events — policy_allow", () => {
153
143
 
154
144
  describe("handleToolCall decision events — policy_deny", () => {
155
145
  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
- }),
146
+ const session = makeSession({
147
+ checkPermission: vi.fn().mockReturnValue(
148
+ makeCheckResult("deny", {
149
+ origin: "project",
150
+ matchedPattern: "read",
151
+ }),
152
+ ),
167
153
  });
154
+ const deps = makeDeps({ session });
168
155
 
169
156
  await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
170
157
 
@@ -182,18 +169,15 @@ describe("handleToolCall decision events — policy_deny", () => {
182
169
 
183
170
  describe("handleToolCall decision events — session_approved", () => {
184
171
  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
- }),
172
+ const session = makeSession({
173
+ checkPermission: vi.fn().mockReturnValue(
174
+ makeCheckResult("allow", {
175
+ source: "session",
176
+ matchedPattern: "git *",
177
+ }),
178
+ ),
196
179
  });
180
+ const deps = makeDeps({ session });
197
181
 
198
182
  await handleToolCall(
199
183
  deps,
@@ -215,12 +199,11 @@ describe("handleToolCall decision events — session_approved", () => {
215
199
 
216
200
  describe("handleToolCall decision events — user_approved", () => {
217
201
  it("emits allow with user_approved when state=ask and user approves once", async () => {
202
+ const session = makeSession({
203
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
204
+ });
218
205
  const deps = makeDeps({
219
- session: makeSession({
220
- permissionManager: {
221
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
222
- } as unknown as SessionState["permissionManager"],
223
- }),
206
+ session,
224
207
  promptPermission: vi
225
208
  .fn()
226
209
  .mockResolvedValue({ approved: true, state: "approved" }),
@@ -237,12 +220,11 @@ describe("handleToolCall decision events — user_approved", () => {
237
220
  });
238
221
 
239
222
  it("emits allow with user_approved_for_session when user approves for session", async () => {
223
+ const session = makeSession({
224
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
225
+ });
240
226
  const deps = makeDeps({
241
- session: makeSession({
242
- permissionManager: {
243
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
244
- } as unknown as SessionState["permissionManager"],
245
- }),
227
+ session,
246
228
  promptPermission: vi
247
229
  .fn()
248
230
  .mockResolvedValue({ approved: true, state: "approved_for_session" }),
@@ -263,12 +245,11 @@ describe("handleToolCall decision events — user_approved", () => {
263
245
 
264
246
  describe("handleToolCall decision events — user_denied", () => {
265
247
  it("emits deny with user_denied when state=ask and user denies", async () => {
248
+ const session = makeSession({
249
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
250
+ });
266
251
  const deps = makeDeps({
267
- session: makeSession({
268
- permissionManager: {
269
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
270
- } as unknown as SessionState["permissionManager"],
271
- }),
252
+ session,
272
253
  promptPermission: vi
273
254
  .fn()
274
255
  .mockResolvedValue({ approved: false, state: "denied" }),
@@ -289,12 +270,11 @@ describe("handleToolCall decision events — user_denied", () => {
289
270
 
290
271
  describe("handleToolCall decision events — confirmation_unavailable", () => {
291
272
  it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
273
+ const session = makeSession({
274
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
275
+ });
292
276
  const deps = makeDeps({
293
- session: makeSession({
294
- permissionManager: {
295
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
296
- } as unknown as SessionState["permissionManager"],
297
- }),
277
+ session,
298
278
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
299
279
  });
300
280
 
@@ -318,14 +298,11 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
318
298
  describe("handleToolCall decision events — infrastructure_auto_allowed", () => {
319
299
  it("emits allow with infrastructure_auto_allowed for Pi infra reads", async () => {
320
300
  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
- }),
301
+ const session = makeSession({
302
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
303
+ getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
328
304
  });
305
+ const deps = makeDeps({ session });
329
306
 
330
307
  const event = makeToolCallEvent("read", {
331
308
  input: { path: `${infraDir}/some-file.json` },
@@ -333,7 +310,6 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
333
310
  await handleToolCall(deps, event, makeCtx());
334
311
 
335
312
  const events = getDecisionEvents(deps);
336
- // One infrastructure_auto_allowed event + one policy_allow for the normal gate
337
313
  const infraEvents = events.filter(
338
314
  (e) => e.resolution === "infrastructure_auto_allowed",
339
315
  );
@@ -349,13 +325,11 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
349
325
 
350
326
  describe("handleToolCall decision events — auto_approved", () => {
351
327
  it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
328
+ const session = makeSession({
329
+ checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
330
+ });
352
331
  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
332
+ session,
359
333
  promptPermission: vi.fn().mockResolvedValue({
360
334
  approved: true,
361
335
  state: "approved",
@@ -3,8 +3,8 @@ import { describe, expect, it, vi } from "vitest";
3
3
 
4
4
  import { getEventInput, handleToolCall } from "../../src/handlers/tool-call";
5
5
  import type { HandlerDeps } from "../../src/handlers/types";
6
- import type { SessionState } from "../../src/runtime";
7
- import type { PermissionCheckResult } from "../../src/types";
6
+ import type { PermissionSession } from "../../src/permission-session";
7
+ import type { PermissionCheckResult, PermissionState } from "../../src/types";
8
8
 
9
9
  // ── SDK stubs ──────────────────────────────────────────────────────────────
10
10
  vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
@@ -55,42 +55,35 @@ function makePermissionResult(
55
55
  return { state, toolName: "read", source: "tool", origin: "builtin" };
56
56
  }
57
57
 
58
- function makeSession(overrides: Partial<SessionState> = {}): SessionState {
58
+ function makeSession(
59
+ overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
60
+ ): PermissionSession {
59
61
  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"],
62
+ logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
63
+ activate: vi.fn(),
64
+ resolveAgentName: vi.fn().mockReturnValue(null),
65
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
66
+ getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
67
+ getSessionRuleset: vi.fn().mockReturnValue([]),
68
+ approveSessionRule: vi.fn(),
69
+ getActiveSkillEntries: vi.fn().mockReturnValue([]),
70
+ getInfrastructureDirs: vi
71
+ .fn()
72
+ .mockReturnValue(["/test/agent", "/test/agent/git"]),
73
+ getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
73
74
  ...overrides,
74
- };
75
+ } as unknown as PermissionSession;
75
76
  }
76
77
 
77
78
  function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
78
79
  return {
79
80
  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),
81
+ events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
87
82
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
88
83
  promptPermission: vi
89
84
  .fn()
90
85
  .mockResolvedValue({ approved: true, state: "approved" }),
91
86
  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
87
  stopPermissionRpcHandlers: vi.fn(),
95
88
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
96
89
  setActiveTools: vi.fn(),
@@ -127,23 +120,15 @@ describe("getEventInput", () => {
127
120
  // ── handleToolCall ─────────────────────────────────────────────────────────
128
121
 
129
122
  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 () => {
123
+ it("activates session with ctx", async () => {
138
124
  const ctx = makeCtx();
139
125
  const deps = makeDeps();
140
126
  await handleToolCall(deps, makeToolCallEvent("read"), ctx);
141
- expect(deps.forwarding.start).toHaveBeenCalledWith(ctx);
127
+ expect(deps.session.activate).toHaveBeenCalledWith(ctx);
142
128
  });
143
129
 
144
130
  it("blocks when tool name cannot be resolved", async () => {
145
131
  const deps = makeDeps();
146
- // An event with no recognisable name field
147
132
  const result = await handleToolCall(deps, { type: "tool_call" }, makeCtx());
148
133
  expect(result).toEqual({
149
134
  block: true,
@@ -164,7 +149,6 @@ describe("handleToolCall", () => {
164
149
  });
165
150
 
166
151
  it("returns empty object when tool is allowed", async () => {
167
- // default makeRuntime() has checkPermission → "allow"
168
152
  const deps = makeDeps();
169
153
  const result = await handleToolCall(
170
154
  deps,
@@ -175,15 +159,10 @@ describe("handleToolCall", () => {
175
159
  });
176
160
 
177
161
  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
- }),
162
+ const session = makeSession({
163
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
186
164
  });
165
+ const deps = makeDeps({ session });
187
166
  const result = await handleToolCall(
188
167
  deps,
189
168
  makeToolCallEvent("read"),
@@ -205,8 +184,11 @@ describe("handleToolCall — skill-read gate", () => {
205
184
  normalizedLocation: "/skills/librarian/SKILL.md",
206
185
  normalizedBaseDir: "/skills/librarian",
207
186
  };
187
+ const session = makeSession({
188
+ getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
189
+ });
208
190
  const deps = makeDeps({
209
- session: makeSession({ activeSkillEntries: [skillEntry] }),
191
+ session,
210
192
  getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
211
193
  });
212
194
  const event = {
@@ -228,8 +210,11 @@ describe("handleToolCall — skill-read gate", () => {
228
210
  normalizedLocation: "/skills/librarian/SKILL.md",
229
211
  normalizedBaseDir: "/skills/librarian",
230
212
  };
213
+ const session = makeSession({
214
+ getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
215
+ });
231
216
  const deps = makeDeps({
232
- session: makeSession({ activeSkillEntries: [skillEntry] }),
217
+ session,
233
218
  getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
234
219
  });
235
220
  const event = {
@@ -247,14 +232,11 @@ describe("handleToolCall — skill-read gate", () => {
247
232
 
248
233
  describe("handleToolCall — external-directory gate", () => {
249
234
  it("blocks a read of a path outside cwd when policy is deny", async () => {
235
+ const session = makeSession({
236
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
237
+ });
250
238
  const deps = makeDeps({
251
- session: makeSession({
252
- permissionManager: {
253
- checkPermission: vi
254
- .fn()
255
- .mockReturnValue(makePermissionResult("deny")),
256
- } as unknown as SessionState["permissionManager"],
257
- }),
239
+ session,
258
240
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
259
241
  });
260
242
  const event = {
@@ -272,14 +254,11 @@ describe("handleToolCall — external-directory gate", () => {
272
254
 
273
255
  describe("handleToolCall — bash external-directory gate", () => {
274
256
  it("blocks a bash command referencing an external path when policy is deny", async () => {
257
+ const session = makeSession({
258
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
259
+ });
275
260
  const deps = makeDeps({
276
- session: makeSession({
277
- permissionManager: {
278
- checkPermission: vi
279
- .fn()
280
- .mockReturnValue(makePermissionResult("deny")),
281
- } as unknown as SessionState["permissionManager"],
282
- }),
261
+ session,
283
262
  getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
284
263
  });
285
264
  const event = {