@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.
- package/CHANGELOG.md +39 -0
- package/package.json +1 -1
- package/src/defaults.ts +60 -0
- package/src/forwarded-permissions/io.ts +47 -12
- package/src/forwarded-permissions/polling.ts +33 -11
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/input.ts +7 -5
- package/src/handlers/lifecycle.ts +25 -24
- package/src/handlers/tool-call.ts +32 -22
- package/src/handlers/types.ts +7 -30
- package/src/index.ts +47 -417
- package/src/normalize.ts +70 -0
- package/src/permission-manager.ts +127 -254
- package/src/rule.ts +7 -23
- package/src/runtime.ts +484 -0
- package/src/types.ts +13 -18
- package/tests/defaults.test.ts +105 -0
- package/tests/forwarded-permissions/io.test.ts +135 -0
- package/tests/handlers/before-agent-start.test.ts +47 -31
- package/tests/handlers/input.test.ts +69 -39
- package/tests/handlers/lifecycle.test.ts +86 -65
- package/tests/handlers/tool-call.test.ts +92 -69
- package/tests/normalize.test.ts +121 -0
- package/tests/permission-system.test.ts +11 -39
- package/tests/rule.test.ts +24 -42
- package/tests/runtime.test.ts +618 -0
- package/tests/session-start.test.ts +2 -2
- package/src/bash-filter.ts +0 -51
- package/tests/bash-filter.test.ts +0 -142
|
@@ -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
|
|
71
|
-
|
|
71
|
+
function makeRuntime(
|
|
72
|
+
overrides: Partial<ExtensionRuntime> = {},
|
|
73
|
+
): ExtensionRuntime {
|
|
72
74
|
return {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
148
|
-
expect(deps.
|
|
149
|
-
expect(deps.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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(
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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(
|
|
263
|
+
const deps = makeDeps();
|
|
250
264
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
251
|
-
expect(deps.
|
|
252
|
-
expect(deps.
|
|
253
|
-
expect(deps.
|
|
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({
|
|
272
|
+
const deps = makeDeps({ runtime: makeRuntime({ runtimeContext: ctx }) });
|
|
259
273
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
260
|
-
expect(deps.writeDebugLog).toHaveBeenCalledWith(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
319
|
+
const ctx = makeCtx();
|
|
320
|
+
const deps = makeDeps({ runtime: makeRuntime({ runtimeContext: ctx }) });
|
|
302
321
|
await handleSessionShutdown(deps);
|
|
303
|
-
expect(deps.
|
|
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.
|
|
310
|
-
expect(deps.
|
|
311
|
-
expect(deps.
|
|
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.
|
|
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
|
|
58
|
+
function makeRuntime(
|
|
59
|
+
overrides: Partial<ExtensionRuntime> = {},
|
|
60
|
+
): ExtensionRuntime {
|
|
58
61
|
return {
|
|
59
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
-
|
|
195
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
-
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
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
|
|
367
|
+
} as unknown as ExtensionRuntime["sessionApprovalCache"];
|
|
350
368
|
const deps = makeDeps({
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
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",
|