@gotgenes/pi-permission-system 3.7.0 → 3.9.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.
@@ -7,6 +7,7 @@ import {
7
7
  } from "../../src/handlers/lifecycle";
8
8
  import type { HandlerDeps } from "../../src/handlers/types";
9
9
  import type { PermissionManager } from "../../src/permission-manager";
10
+ import type { ExtensionRuntime } from "../../src/runtime";
10
11
  import type { SessionApprovalCache } from "../../src/session-approval-cache";
11
12
  import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
12
13
 
@@ -67,22 +68,36 @@ function makeSessionApprovalCache(): SessionApprovalCache {
67
68
  } as unknown as SessionApprovalCache;
68
69
  }
69
70
 
70
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
71
- const pm = makePermissionManager();
71
+ function makeRuntime(
72
+ overrides: Partial<ExtensionRuntime> = {},
73
+ ): ExtensionRuntime {
72
74
  return {
73
- getPermissionManager: vi.fn().mockReturnValue(pm),
74
- setPermissionManager: vi.fn(),
75
- getRuntimeContext: vi.fn().mockReturnValue(null),
76
- setRuntimeContext: vi.fn(),
77
- getActiveSkillEntries: vi.fn().mockReturnValue([] as SkillPromptEntry[]),
78
- setActiveSkillEntries: vi.fn(),
79
- getLastKnownActiveAgentName: vi.fn().mockReturnValue(null),
80
- setLastKnownActiveAgentName: vi.fn(),
81
- getLastActiveToolsCacheKey: vi.fn().mockReturnValue(null),
82
- setLastActiveToolsCacheKey: vi.fn(),
83
- getLastPromptStateCacheKey: vi.fn().mockReturnValue(null),
84
- setLastPromptStateCacheKey: vi.fn(),
75
+ agentDir: "/test/agent",
76
+ sessionsDir: "/test/agent/sessions",
77
+ subagentSessionsDir: "/test/agent/subagent-sessions",
78
+ forwardingDir: "/test/agent/sessions/permission-forwarding",
79
+ globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
80
+ config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
81
+ runtimeContext: null,
82
+ permissionManager: makePermissionManager() as unknown as PermissionManager,
83
+ activeSkillEntries: [] as SkillPromptEntry[],
84
+ lastKnownActiveAgentName: null,
85
+ lastActiveToolsCacheKey: null,
86
+ lastPromptStateCacheKey: null,
87
+ lastConfigWarning: null,
85
88
  sessionApprovalCache: makeSessionApprovalCache(),
89
+ permissionForwardingContext: null,
90
+ permissionForwardingTimer: null,
91
+ isProcessingForwardedRequests: false,
92
+ writeDebugLog: vi.fn(),
93
+ writeReviewLog: vi.fn(),
94
+ ...overrides,
95
+ } as ExtensionRuntime;
96
+ }
97
+
98
+ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
99
+ return {
100
+ runtime: makeRuntime(),
86
101
  createPermissionManagerForCwd: vi
87
102
  .fn()
88
103
  .mockReturnValue(makePermissionManager()),
@@ -97,8 +112,6 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
97
112
  createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
98
113
  startForwardedPermissionPolling: vi.fn(),
99
114
  stopForwardedPermissionPolling: vi.fn(),
100
- writeReviewLog: vi.fn(),
101
- writeDebugLog: vi.fn(),
102
115
  getAllTools: vi.fn().mockReturnValue([]),
103
116
  setActiveTools: vi.fn(),
104
117
  ...overrides,
@@ -117,7 +130,7 @@ describe("handleSessionStart", () => {
117
130
  const ctx = makeCtx();
118
131
  const deps = makeDeps();
119
132
  await handleSessionStart(deps, { reason: "startup" }, ctx);
120
- expect(deps.setRuntimeContext).toHaveBeenCalledWith(ctx);
133
+ expect(deps.runtime.runtimeContext).toBe(ctx);
121
134
  });
122
135
 
123
136
  it("refreshes extension config with ctx", async () => {
@@ -137,16 +150,16 @@ describe("handleSessionStart", () => {
137
150
  expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
138
151
  "/my/project",
139
152
  );
140
- expect(deps.setPermissionManager).toHaveBeenCalledWith(newPm);
153
+ expect(deps.runtime.permissionManager).toBe(newPm);
141
154
  });
142
155
 
143
156
  it("clears the before_agent_start cache", async () => {
144
157
  const ctx = makeCtx();
145
158
  const deps = makeDeps();
146
159
  await handleSessionStart(deps, { reason: "startup" }, ctx);
147
- expect(deps.setActiveSkillEntries).toHaveBeenCalledWith([]);
148
- expect(deps.setLastActiveToolsCacheKey).toHaveBeenCalledWith(null);
149
- expect(deps.setLastPromptStateCacheKey).toHaveBeenCalledWith(null);
160
+ expect(deps.runtime.activeSkillEntries).toEqual([]);
161
+ expect(deps.runtime.lastActiveToolsCacheKey).toBeNull();
162
+ expect(deps.runtime.lastPromptStateCacheKey).toBeNull();
150
163
  });
151
164
 
152
165
  it("sets lastKnownActiveAgentName from getActiveAgentName", async () => {
@@ -154,7 +167,7 @@ describe("handleSessionStart", () => {
154
167
  const ctx = makeCtx();
155
168
  const deps = makeDeps();
156
169
  await handleSessionStart(deps, { reason: "startup" }, ctx);
157
- expect(deps.setLastKnownActiveAgentName).toHaveBeenCalledWith("my-agent");
170
+ expect(deps.runtime.lastKnownActiveAgentName).toBe("my-agent");
158
171
  });
159
172
 
160
173
  it("sets lastKnownActiveAgentName to null when no agent is active", async () => {
@@ -162,7 +175,7 @@ describe("handleSessionStart", () => {
162
175
  const ctx = makeCtx();
163
176
  const deps = makeDeps();
164
177
  await handleSessionStart(deps, { reason: "startup" }, ctx);
165
- expect(deps.setLastKnownActiveAgentName).toHaveBeenCalledWith(null);
178
+ expect(deps.runtime.lastKnownActiveAgentName).toBeNull();
166
179
  });
167
180
 
168
181
  it("starts forwarded permission polling", async () => {
@@ -182,7 +195,7 @@ describe("handleSessionStart", () => {
182
195
  it("notifies each policy issue", async () => {
183
196
  const pm = makePermissionManager(["issue A", "issue B"]);
184
197
  const deps = makeDeps({
185
- getPermissionManager: vi.fn().mockReturnValue(pm),
198
+ createPermissionManagerForCwd: vi.fn().mockReturnValue(pm),
186
199
  });
187
200
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
188
201
  expect(deps.notifyWarning).toHaveBeenCalledWith("issue A");
@@ -199,17 +212,20 @@ describe("handleSessionStart", () => {
199
212
  const ctx = makeCtx({ cwd: "/proj" });
200
213
  const deps = makeDeps();
201
214
  await handleSessionStart(deps, { reason: "reload" }, ctx);
202
- expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
203
- triggeredBy: "session_start",
204
- reason: "reload",
205
- cwd: "/proj",
206
- });
215
+ expect(deps.runtime.writeDebugLog).toHaveBeenCalledWith(
216
+ "lifecycle.reload",
217
+ {
218
+ triggeredBy: "session_start",
219
+ reason: "reload",
220
+ cwd: "/proj",
221
+ },
222
+ );
207
223
  });
208
224
 
209
225
  it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
210
226
  const deps = makeDeps();
211
227
  await handleSessionStart(deps, { reason: "startup" }, makeCtx());
212
- expect(deps.writeDebugLog).not.toHaveBeenCalled();
228
+ expect(deps.runtime.writeDebugLog).not.toHaveBeenCalled();
213
229
  });
214
230
  });
215
231
 
@@ -219,61 +235,63 @@ describe("handleResourcesDiscover", () => {
219
235
  it("does nothing when reason is not reload", async () => {
220
236
  const deps = makeDeps();
221
237
  await handleResourcesDiscover(deps, { reason: "startup" });
222
- expect(deps.setPermissionManager).not.toHaveBeenCalled();
223
- expect(deps.writeDebugLog).not.toHaveBeenCalled();
238
+ expect(deps.createPermissionManagerForCwd).not.toHaveBeenCalled();
239
+ expect(deps.runtime.writeDebugLog).not.toHaveBeenCalled();
224
240
  });
225
241
 
226
242
  it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
227
243
  const ctx = makeCtx({ cwd: "/runtime/cwd" });
228
244
  const newPm = makePermissionManager();
229
245
  const deps = makeDeps({
230
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
246
+ runtime: makeRuntime({ runtimeContext: ctx }),
231
247
  createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
232
248
  });
233
249
  await handleResourcesDiscover(deps, { reason: "reload" });
234
250
  expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
235
251
  "/runtime/cwd",
236
252
  );
237
- expect(deps.setPermissionManager).toHaveBeenCalledWith(newPm);
253
+ expect(deps.runtime.permissionManager).toBe(newPm);
238
254
  });
239
255
 
240
256
  it("uses undefined cwd when runtimeContext is null on reload", async () => {
241
- const deps = makeDeps({
242
- getRuntimeContext: vi.fn().mockReturnValue(null),
243
- });
257
+ const deps = makeDeps();
244
258
  await handleResourcesDiscover(deps, { reason: "reload" });
245
259
  expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(undefined);
246
260
  });
247
261
 
248
262
  it("clears the before_agent_start cache on reload", async () => {
249
- const deps = makeDeps({ getRuntimeContext: vi.fn().mockReturnValue(null) });
263
+ const deps = makeDeps();
250
264
  await handleResourcesDiscover(deps, { reason: "reload" });
251
- expect(deps.setActiveSkillEntries).toHaveBeenCalledWith([]);
252
- expect(deps.setLastActiveToolsCacheKey).toHaveBeenCalledWith(null);
253
- expect(deps.setLastPromptStateCacheKey).toHaveBeenCalledWith(null);
265
+ expect(deps.runtime.activeSkillEntries).toEqual([]);
266
+ expect(deps.runtime.lastActiveToolsCacheKey).toBeNull();
267
+ expect(deps.runtime.lastPromptStateCacheKey).toBeNull();
254
268
  });
255
269
 
256
270
  it("writes lifecycle.reload debug log on reload", async () => {
257
271
  const ctx = makeCtx({ cwd: "/proj" });
258
- const deps = makeDeps({ getRuntimeContext: vi.fn().mockReturnValue(ctx) });
272
+ const deps = makeDeps({ runtime: makeRuntime({ runtimeContext: ctx }) });
259
273
  await handleResourcesDiscover(deps, { reason: "reload" });
260
- expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
261
- triggeredBy: "resources_discover",
262
- reason: "reload",
263
- cwd: "/proj",
264
- });
274
+ expect(deps.runtime.writeDebugLog).toHaveBeenCalledWith(
275
+ "lifecycle.reload",
276
+ {
277
+ triggeredBy: "resources_discover",
278
+ reason: "reload",
279
+ cwd: "/proj",
280
+ },
281
+ );
265
282
  });
266
283
 
267
284
  it("logs cwd as null when runtimeContext is null on reload", async () => {
268
- const deps = makeDeps({
269
- getRuntimeContext: vi.fn().mockReturnValue(null),
270
- });
285
+ const deps = makeDeps();
271
286
  await handleResourcesDiscover(deps, { reason: "reload" });
272
- expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
273
- triggeredBy: "resources_discover",
274
- reason: "reload",
275
- cwd: null,
276
- });
287
+ expect(deps.runtime.writeDebugLog).toHaveBeenCalledWith(
288
+ "lifecycle.reload",
289
+ {
290
+ triggeredBy: "resources_discover",
291
+ reason: "reload",
292
+ cwd: null,
293
+ },
294
+ );
277
295
  });
278
296
  });
279
297
 
@@ -283,7 +301,7 @@ describe("handleSessionShutdown", () => {
283
301
  it("clears the UI status when a runtime context is present", async () => {
284
302
  const ctx = makeCtx();
285
303
  const deps = makeDeps({
286
- getRuntimeContext: vi.fn().mockReturnValue(ctx),
304
+ runtime: makeRuntime({ runtimeContext: ctx }),
287
305
  });
288
306
  await handleSessionShutdown(deps);
289
307
  expect(ctx.ui.setStatus).toHaveBeenCalledWith(
@@ -293,28 +311,29 @@ describe("handleSessionShutdown", () => {
293
311
  });
294
312
 
295
313
  it("does not throw when runtime context is null", async () => {
296
- const deps = makeDeps({ getRuntimeContext: vi.fn().mockReturnValue(null) });
314
+ const deps = makeDeps();
297
315
  await expect(handleSessionShutdown(deps)).resolves.not.toThrow();
298
316
  });
299
317
 
300
318
  it("sets runtime context to null", async () => {
301
- const deps = makeDeps();
319
+ const ctx = makeCtx();
320
+ const deps = makeDeps({ runtime: makeRuntime({ runtimeContext: ctx }) });
302
321
  await handleSessionShutdown(deps);
303
- expect(deps.setRuntimeContext).toHaveBeenCalledWith(null);
322
+ expect(deps.runtime.runtimeContext).toBeNull();
304
323
  });
305
324
 
306
325
  it("clears the before_agent_start cache", async () => {
307
326
  const deps = makeDeps();
308
327
  await handleSessionShutdown(deps);
309
- expect(deps.setActiveSkillEntries).toHaveBeenCalledWith([]);
310
- expect(deps.setLastActiveToolsCacheKey).toHaveBeenCalledWith(null);
311
- expect(deps.setLastPromptStateCacheKey).toHaveBeenCalledWith(null);
328
+ expect(deps.runtime.activeSkillEntries).toEqual([]);
329
+ expect(deps.runtime.lastActiveToolsCacheKey).toBeNull();
330
+ expect(deps.runtime.lastPromptStateCacheKey).toBeNull();
312
331
  });
313
332
 
314
333
  it("clears the session approval cache", async () => {
315
334
  const deps = makeDeps();
316
335
  await handleSessionShutdown(deps);
317
- expect(deps.sessionApprovalCache.clear).toHaveBeenCalledOnce();
336
+ expect(deps.runtime.sessionApprovalCache.clear).toHaveBeenCalledOnce();
318
337
  });
319
338
 
320
339
  it("stops forwarded permission polling", async () => {
@@ -324,8 +343,10 @@ describe("handleSessionShutdown", () => {
324
343
  });
325
344
 
326
345
  it("does not reset lastKnownActiveAgentName", async () => {
327
- const deps = makeDeps();
346
+ const deps = makeDeps({
347
+ runtime: makeRuntime({ lastKnownActiveAgentName: "remembered" }),
348
+ });
328
349
  await handleSessionShutdown(deps);
329
- expect(deps.setLastKnownActiveAgentName).not.toHaveBeenCalled();
350
+ expect(deps.runtime.lastKnownActiveAgentName).toBe("remembered");
330
351
  });
331
352
  });
@@ -3,6 +3,7 @@ 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 { ExtensionRuntime } from "../../src/runtime";
6
7
  import type { PermissionCheckResult } from "../../src/types";
7
8
 
8
9
  // ── SDK stubs ──────────────────────────────────────────────────────────────
@@ -54,28 +55,43 @@ function makePermissionResult(
54
55
  return { state, toolName: "read", source: "tool" };
55
56
  }
56
57
 
57
- function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
58
+ function makeRuntime(
59
+ overrides: Partial<ExtensionRuntime> = {},
60
+ ): ExtensionRuntime {
58
61
  return {
59
- getPermissionManager: vi.fn().mockReturnValue({
62
+ agentDir: "/test/agent",
63
+ sessionsDir: "/test/agent/sessions",
64
+ subagentSessionsDir: "/test/agent/subagent-sessions",
65
+ forwardingDir: "/test/agent/sessions/permission-forwarding",
66
+ globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
67
+ config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
68
+ runtimeContext: null,
69
+ permissionManager: {
60
70
  checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
61
- }),
62
- setPermissionManager: vi.fn(),
63
- getRuntimeContext: vi.fn().mockReturnValue(null),
64
- setRuntimeContext: vi.fn(),
65
- getActiveSkillEntries: vi.fn().mockReturnValue([]),
66
- setActiveSkillEntries: vi.fn(),
67
- getLastKnownActiveAgentName: vi.fn().mockReturnValue(null),
68
- setLastKnownActiveAgentName: vi.fn(),
69
- getLastActiveToolsCacheKey: vi.fn().mockReturnValue(null),
70
- setLastActiveToolsCacheKey: vi.fn(),
71
- getLastPromptStateCacheKey: vi.fn().mockReturnValue(null),
72
- setLastPromptStateCacheKey: vi.fn(),
71
+ } as unknown as ExtensionRuntime["permissionManager"],
72
+ activeSkillEntries: [],
73
+ lastKnownActiveAgentName: null,
74
+ lastActiveToolsCacheKey: null,
75
+ lastPromptStateCacheKey: null,
76
+ lastConfigWarning: null,
73
77
  sessionApprovalCache: {
74
78
  approve: vi.fn(),
75
79
  has: vi.fn().mockReturnValue(false),
76
80
  findMatchingPrefix: vi.fn().mockReturnValue(null),
77
81
  clear: vi.fn(),
78
- } as unknown as HandlerDeps["sessionApprovalCache"],
82
+ } as unknown as ExtensionRuntime["sessionApprovalCache"],
83
+ permissionForwardingContext: null,
84
+ permissionForwardingTimer: null,
85
+ isProcessingForwardedRequests: false,
86
+ writeDebugLog: vi.fn(),
87
+ writeReviewLog: vi.fn(),
88
+ ...overrides,
89
+ } as ExtensionRuntime;
90
+ }
91
+
92
+ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
93
+ return {
94
+ runtime: makeRuntime(),
79
95
  createPermissionManagerForCwd: vi.fn(),
80
96
  refreshExtensionConfig: vi.fn(),
81
97
  notifyWarning: vi.fn(),
@@ -88,8 +104,6 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
88
104
  createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
89
105
  startForwardedPermissionPolling: vi.fn(),
90
106
  stopForwardedPermissionPolling: vi.fn(),
91
- writeReviewLog: vi.fn(),
92
- writeDebugLog: vi.fn(),
93
107
  getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
94
108
  setActiveTools: vi.fn(),
95
109
  ...overrides,
@@ -129,7 +143,7 @@ describe("handleToolCall", () => {
129
143
  const ctx = makeCtx();
130
144
  const deps = makeDeps();
131
145
  await handleToolCall(deps, makeToolCallEvent("read"), ctx);
132
- expect(deps.setRuntimeContext).toHaveBeenCalledWith(ctx);
146
+ expect(deps.runtime.runtimeContext).toBe(ctx);
133
147
  });
134
148
 
135
149
  it("starts forwarded permission polling", async () => {
@@ -162,11 +176,8 @@ describe("handleToolCall", () => {
162
176
  });
163
177
 
164
178
  it("returns empty object when tool is allowed", async () => {
165
- const deps = makeDeps({
166
- getPermissionManager: vi.fn().mockReturnValue({
167
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
168
- }),
169
- });
179
+ // default makeRuntime() has checkPermission → "allow"
180
+ const deps = makeDeps();
170
181
  const result = await handleToolCall(
171
182
  deps,
172
183
  makeToolCallEvent("read"),
@@ -177,8 +188,12 @@ describe("handleToolCall", () => {
177
188
 
178
189
  it("blocks when tool is denied by policy", async () => {
179
190
  const deps = makeDeps({
180
- getPermissionManager: vi.fn().mockReturnValue({
181
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
191
+ runtime: makeRuntime({
192
+ permissionManager: {
193
+ checkPermission: vi
194
+ .fn()
195
+ .mockReturnValue(makePermissionResult("deny")),
196
+ } as unknown as ExtensionRuntime["permissionManager"],
182
197
  }),
183
198
  });
184
199
  const result = await handleToolCall(
@@ -191,8 +206,10 @@ describe("handleToolCall", () => {
191
206
 
192
207
  it("blocks when tool ask has no UI available", async () => {
193
208
  const deps = makeDeps({
194
- getPermissionManager: vi.fn().mockReturnValue({
195
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
209
+ runtime: makeRuntime({
210
+ permissionManager: {
211
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
212
+ } as unknown as ExtensionRuntime["permissionManager"],
196
213
  }),
197
214
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
198
215
  });
@@ -206,8 +223,10 @@ describe("handleToolCall", () => {
206
223
 
207
224
  it("allows when user approves the ask prompt", async () => {
208
225
  const deps = makeDeps({
209
- getPermissionManager: vi.fn().mockReturnValue({
210
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
226
+ runtime: makeRuntime({
227
+ permissionManager: {
228
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
229
+ } as unknown as ExtensionRuntime["permissionManager"],
211
230
  }),
212
231
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
213
232
  promptPermission: vi
@@ -224,8 +243,10 @@ describe("handleToolCall", () => {
224
243
 
225
244
  it("blocks when user denies the ask prompt", async () => {
226
245
  const deps = makeDeps({
227
- getPermissionManager: vi.fn().mockReturnValue({
228
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
246
+ runtime: makeRuntime({
247
+ permissionManager: {
248
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
249
+ } as unknown as ExtensionRuntime["permissionManager"],
229
250
  }),
230
251
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
231
252
  promptPermission: vi
@@ -254,11 +275,8 @@ describe("handleToolCall — skill-read gate", () => {
254
275
  normalizedBaseDir: "/skills/librarian",
255
276
  };
256
277
  const deps = makeDeps({
257
- getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
278
+ runtime: makeRuntime({ activeSkillEntries: [skillEntry] }),
258
279
  getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
259
- getPermissionManager: vi.fn().mockReturnValue({
260
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
261
- }),
262
280
  });
263
281
  const event = {
264
282
  type: "tool_call",
@@ -280,11 +298,8 @@ describe("handleToolCall — skill-read gate", () => {
280
298
  normalizedBaseDir: "/skills/librarian",
281
299
  };
282
300
  const deps = makeDeps({
283
- getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
301
+ runtime: makeRuntime({ activeSkillEntries: [skillEntry] }),
284
302
  getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
285
- getPermissionManager: vi.fn().mockReturnValue({
286
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
287
- }),
288
303
  });
289
304
  const event = {
290
305
  type: "tool_call",
@@ -302,10 +317,14 @@ describe("handleToolCall — skill-read gate", () => {
302
317
  describe("handleToolCall — external-directory gate", () => {
303
318
  it("blocks a read of a path outside cwd when policy is deny", async () => {
304
319
  const deps = makeDeps({
305
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
306
- getPermissionManager: vi.fn().mockReturnValue({
307
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
320
+ runtime: makeRuntime({
321
+ permissionManager: {
322
+ checkPermission: vi
323
+ .fn()
324
+ .mockReturnValue(makePermissionResult("deny")),
325
+ } as unknown as ExtensionRuntime["permissionManager"],
308
326
  }),
327
+ getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
309
328
  });
310
329
  const event = {
311
330
  type: "tool_call",
@@ -319,16 +338,15 @@ describe("handleToolCall — external-directory gate", () => {
319
338
 
320
339
  it("allows when session has an existing approval for the external path", async () => {
321
340
  const deps = makeDeps({
322
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
323
- getPermissionManager: vi.fn().mockReturnValue({
324
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
341
+ runtime: makeRuntime({
342
+ sessionApprovalCache: {
343
+ approve: vi.fn(),
344
+ has: vi.fn().mockReturnValue(false),
345
+ findMatchingPrefix: vi.fn().mockReturnValue("/outside/project/"),
346
+ clear: vi.fn(),
347
+ } as unknown as ExtensionRuntime["sessionApprovalCache"],
325
348
  }),
326
- sessionApprovalCache: {
327
- approve: vi.fn(),
328
- has: vi.fn().mockReturnValue(false),
329
- findMatchingPrefix: vi.fn().mockReturnValue("/outside/project/"),
330
- clear: vi.fn(),
331
- } as unknown as HandlerDeps["sessionApprovalCache"],
349
+ getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
332
350
  });
333
351
  const event = {
334
352
  type: "tool_call",
@@ -346,17 +364,19 @@ describe("handleToolCall — external-directory gate", () => {
346
364
  has: vi.fn().mockReturnValue(false),
347
365
  findMatchingPrefix: vi.fn().mockReturnValue(null),
348
366
  clear: vi.fn(),
349
- } as unknown as HandlerDeps["sessionApprovalCache"];
367
+ } as unknown as ExtensionRuntime["sessionApprovalCache"];
350
368
  const deps = makeDeps({
351
- getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
352
- getPermissionManager: vi.fn().mockReturnValue({
353
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
369
+ runtime: makeRuntime({
370
+ permissionManager: {
371
+ checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
372
+ } as unknown as ExtensionRuntime["permissionManager"],
373
+ sessionApprovalCache: approveCache,
354
374
  }),
375
+ getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
355
376
  canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
356
377
  promptPermission: vi
357
378
  .fn()
358
379
  .mockResolvedValue({ approved: true, state: "approved_for_session" }),
359
- sessionApprovalCache: approveCache,
360
380
  });
361
381
  const event = {
362
382
  type: "tool_call",
@@ -377,10 +397,14 @@ describe("handleToolCall — external-directory gate", () => {
377
397
  describe("handleToolCall — bash external-directory gate", () => {
378
398
  it("blocks a bash command referencing an external path when policy is deny", async () => {
379
399
  const deps = makeDeps({
380
- getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
381
- getPermissionManager: vi.fn().mockReturnValue({
382
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
400
+ runtime: makeRuntime({
401
+ permissionManager: {
402
+ checkPermission: vi
403
+ .fn()
404
+ .mockReturnValue(makePermissionResult("deny")),
405
+ } as unknown as ExtensionRuntime["permissionManager"],
383
406
  }),
407
+ getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
384
408
  });
385
409
  const event = {
386
410
  type: "tool_call",
@@ -394,17 +418,16 @@ describe("handleToolCall — bash external-directory gate", () => {
394
418
 
395
419
  it("skips bash external gate when all referenced paths are session-approved", async () => {
396
420
  const deps = makeDeps({
397
- getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
398
- getPermissionManager: vi.fn().mockReturnValue({
399
- checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
421
+ runtime: makeRuntime({
422
+ sessionApprovalCache: {
423
+ approve: vi.fn(),
424
+ // All paths are covered
425
+ has: vi.fn().mockReturnValue(true),
426
+ findMatchingPrefix: vi.fn().mockReturnValue(null),
427
+ clear: vi.fn(),
428
+ } as unknown as ExtensionRuntime["sessionApprovalCache"],
400
429
  }),
401
- sessionApprovalCache: {
402
- approve: vi.fn(),
403
- // All paths are covered
404
- has: vi.fn().mockReturnValue(true),
405
- findMatchingPrefix: vi.fn().mockReturnValue(null),
406
- clear: vi.fn(),
407
- } as unknown as HandlerDeps["sessionApprovalCache"],
430
+ getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
408
431
  });
409
432
  const event = {
410
433
  type: "tool_call",