@gotgenes/pi-permission-system 5.10.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.
- package/CHANGELOG.md +15 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +1 -1
- package/src/handlers/before-agent-start.ts +73 -60
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/handlers/index.ts +6 -15
- package/src/handlers/lifecycle.ts +54 -42
- package/src/handlers/permission-gate-handler.ts +346 -0
- package/src/index.ts +33 -38
- package/src/permission-prompter.ts +23 -6
- package/src/permission-session.ts +29 -0
- package/src/session-logger.ts +1 -1
- package/src/tool-registry.ts +6 -0
- package/tests/handlers/before-agent-start.test.ts +70 -90
- package/tests/handlers/input-events.test.ts +70 -63
- package/tests/handlers/input.test.ts +85 -83
- package/tests/handlers/lifecycle.test.ts +60 -72
- package/tests/handlers/tool-call-events.test.ts +128 -122
- package/tests/handlers/tool-call.test.ts +84 -58
- package/tests/permission-prompter.test.ts +1 -1
- package/tests/permission-session.test.ts +61 -0
- package/src/handlers/input.ts +0 -126
- package/src/handlers/tool-call.ts +0 -203
- package/src/handlers/types.ts +0 -63
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
6
6
|
import { describe, expect, it, vi } from "vitest";
|
|
7
7
|
|
|
8
|
-
import {
|
|
9
|
-
import type { HandlerDeps } from "../../src/handlers/types";
|
|
8
|
+
import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
|
|
10
9
|
import type { PermissionDecisionEvent } from "../../src/permission-events";
|
|
11
10
|
import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
|
|
12
11
|
import type { PermissionSession } from "../../src/permission-session";
|
|
12
|
+
import type { ToolRegistry } from "../../src/tool-registry";
|
|
13
13
|
import type { PermissionCheckResult, PermissionState } from "../../src/types";
|
|
14
14
|
|
|
15
15
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -83,30 +83,40 @@ function makeSession(
|
|
|
83
83
|
.fn()
|
|
84
84
|
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
85
85
|
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
86
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
87
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
86
88
|
...overrides,
|
|
87
89
|
} as unknown as PermissionSession;
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
function
|
|
92
|
+
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
91
93
|
return {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
95
|
-
promptPermission: vi
|
|
96
|
-
.fn()
|
|
97
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
98
|
-
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
99
|
-
stopPermissionRpcHandlers: vi.fn(),
|
|
100
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
101
|
-
setActiveTools: vi.fn(),
|
|
94
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
95
|
+
setActive: vi.fn(),
|
|
102
96
|
...overrides,
|
|
103
97
|
};
|
|
104
98
|
}
|
|
105
99
|
|
|
100
|
+
function makeHandler(overrides?: {
|
|
101
|
+
session?: Partial<Record<keyof PermissionSession, unknown>>;
|
|
102
|
+
toolRegistry?: Partial<ToolRegistry>;
|
|
103
|
+
}): {
|
|
104
|
+
handler: PermissionGateHandler;
|
|
105
|
+
events: ReturnType<typeof makeEvents>;
|
|
106
|
+
session: PermissionSession;
|
|
107
|
+
} {
|
|
108
|
+
const session = makeSession(overrides?.session);
|
|
109
|
+
const events = makeEvents();
|
|
110
|
+
const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
|
|
111
|
+
const handler = new PermissionGateHandler(session, events, toolRegistry);
|
|
112
|
+
return { handler, events, session };
|
|
113
|
+
}
|
|
114
|
+
|
|
106
115
|
/** Extract all permissions:decision payloads from the events.emit mock. */
|
|
107
|
-
function getDecisionEvents(
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
function getDecisionEvents(
|
|
117
|
+
events: ReturnType<typeof makeEvents>,
|
|
118
|
+
): PermissionDecisionEvent[] {
|
|
119
|
+
return events.emit.mock.calls
|
|
110
120
|
.filter(([channel]) => channel === PERMISSIONS_DECISION_CHANNEL)
|
|
111
121
|
.map(([, payload]) => payload as PermissionDecisionEvent);
|
|
112
122
|
}
|
|
@@ -115,21 +125,22 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
|
|
|
115
125
|
|
|
116
126
|
describe("handleToolCall decision events — policy_allow", () => {
|
|
117
127
|
it("emits allow with policy_allow when checkPermission returns allow", async () => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
128
|
+
const { handler, events } = makeHandler({
|
|
129
|
+
session: {
|
|
130
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
131
|
+
makeCheckResult("allow", {
|
|
132
|
+
origin: "global",
|
|
133
|
+
matchedPattern: "*",
|
|
134
|
+
}),
|
|
135
|
+
),
|
|
136
|
+
},
|
|
125
137
|
});
|
|
126
|
-
const deps = makeDeps({ session });
|
|
127
138
|
|
|
128
|
-
await handleToolCall(
|
|
139
|
+
await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
|
|
129
140
|
|
|
130
|
-
const
|
|
131
|
-
expect(
|
|
132
|
-
expect(
|
|
141
|
+
const decisions = getDecisionEvents(events);
|
|
142
|
+
expect(decisions).toHaveLength(1);
|
|
143
|
+
expect(decisions[0]).toMatchObject({
|
|
133
144
|
surface: "read",
|
|
134
145
|
result: "allow",
|
|
135
146
|
resolution: "policy_allow",
|
|
@@ -143,21 +154,22 @@ describe("handleToolCall decision events — policy_allow", () => {
|
|
|
143
154
|
|
|
144
155
|
describe("handleToolCall decision events — policy_deny", () => {
|
|
145
156
|
it("emits deny with policy_deny when checkPermission returns deny", async () => {
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
157
|
+
const { handler, events } = makeHandler({
|
|
158
|
+
session: {
|
|
159
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
160
|
+
makeCheckResult("deny", {
|
|
161
|
+
origin: "project",
|
|
162
|
+
matchedPattern: "read",
|
|
163
|
+
}),
|
|
164
|
+
),
|
|
165
|
+
},
|
|
153
166
|
});
|
|
154
|
-
const deps = makeDeps({ session });
|
|
155
167
|
|
|
156
|
-
await handleToolCall(
|
|
168
|
+
await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
|
|
157
169
|
|
|
158
|
-
const
|
|
159
|
-
expect(
|
|
160
|
-
expect(
|
|
170
|
+
const decisions = getDecisionEvents(events);
|
|
171
|
+
expect(decisions).toHaveLength(1);
|
|
172
|
+
expect(decisions[0]).toMatchObject({
|
|
161
173
|
surface: "read",
|
|
162
174
|
result: "deny",
|
|
163
175
|
resolution: "policy_deny",
|
|
@@ -169,25 +181,25 @@ describe("handleToolCall decision events — policy_deny", () => {
|
|
|
169
181
|
|
|
170
182
|
describe("handleToolCall decision events — session_approved", () => {
|
|
171
183
|
it("emits allow with session_approved when checkPermission returns source:session", async () => {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
184
|
+
const { handler, events } = makeHandler({
|
|
185
|
+
session: {
|
|
186
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
187
|
+
makeCheckResult("allow", {
|
|
188
|
+
source: "session",
|
|
189
|
+
matchedPattern: "git *",
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
},
|
|
179
193
|
});
|
|
180
|
-
const deps = makeDeps({ session });
|
|
181
194
|
|
|
182
|
-
await handleToolCall(
|
|
183
|
-
deps,
|
|
195
|
+
await handler.handleToolCall(
|
|
184
196
|
makeToolCallEvent("bash", { input: { command: "git status" } }),
|
|
185
197
|
makeCtx(),
|
|
186
198
|
);
|
|
187
199
|
|
|
188
|
-
const
|
|
189
|
-
expect(
|
|
190
|
-
expect(
|
|
200
|
+
const decisions = getDecisionEvents(events);
|
|
201
|
+
expect(decisions).toHaveLength(1);
|
|
202
|
+
expect(decisions[0]).toMatchObject({
|
|
191
203
|
surface: "bash",
|
|
192
204
|
result: "allow",
|
|
193
205
|
resolution: "session_approved",
|
|
@@ -199,42 +211,41 @@ describe("handleToolCall decision events — session_approved", () => {
|
|
|
199
211
|
|
|
200
212
|
describe("handleToolCall decision events — user_approved", () => {
|
|
201
213
|
it("emits allow with user_approved when state=ask and user approves once", async () => {
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
214
|
+
const { handler, events } = makeHandler({
|
|
215
|
+
session: {
|
|
216
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
217
|
+
prompt: vi
|
|
218
|
+
.fn()
|
|
219
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
220
|
+
},
|
|
210
221
|
});
|
|
211
222
|
|
|
212
|
-
await handleToolCall(
|
|
223
|
+
await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
|
|
213
224
|
|
|
214
|
-
const
|
|
215
|
-
expect(
|
|
216
|
-
expect(
|
|
225
|
+
const decisions = getDecisionEvents(events);
|
|
226
|
+
expect(decisions).toHaveLength(1);
|
|
227
|
+
expect(decisions[0]).toMatchObject({
|
|
217
228
|
result: "allow",
|
|
218
229
|
resolution: "user_approved",
|
|
219
230
|
});
|
|
220
231
|
});
|
|
221
232
|
|
|
222
233
|
it("emits allow with user_approved_for_session when user approves for session", async () => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
234
|
+
const { handler, events } = makeHandler({
|
|
235
|
+
session: {
|
|
236
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
237
|
+
prompt: vi.fn().mockResolvedValue({
|
|
238
|
+
approved: true,
|
|
239
|
+
state: "approved_for_session",
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
231
242
|
});
|
|
232
243
|
|
|
233
|
-
await handleToolCall(
|
|
244
|
+
await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
|
|
234
245
|
|
|
235
|
-
const
|
|
236
|
-
expect(
|
|
237
|
-
expect(
|
|
246
|
+
const decisions = getDecisionEvents(events);
|
|
247
|
+
expect(decisions).toHaveLength(1);
|
|
248
|
+
expect(decisions[0]).toMatchObject({
|
|
238
249
|
result: "allow",
|
|
239
250
|
resolution: "user_approved_for_session",
|
|
240
251
|
});
|
|
@@ -245,21 +256,18 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
245
256
|
|
|
246
257
|
describe("handleToolCall decision events — user_denied", () => {
|
|
247
258
|
it("emits deny with user_denied when state=ask and user denies", async () => {
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
promptPermission: vi
|
|
254
|
-
.fn()
|
|
255
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
259
|
+
const { handler, events } = makeHandler({
|
|
260
|
+
session: {
|
|
261
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
262
|
+
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
263
|
+
},
|
|
256
264
|
});
|
|
257
265
|
|
|
258
|
-
await handleToolCall(
|
|
266
|
+
await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
|
|
259
267
|
|
|
260
|
-
const
|
|
261
|
-
expect(
|
|
262
|
-
expect(
|
|
268
|
+
const decisions = getDecisionEvents(events);
|
|
269
|
+
expect(decisions).toHaveLength(1);
|
|
270
|
+
expect(decisions[0]).toMatchObject({
|
|
263
271
|
result: "deny",
|
|
264
272
|
resolution: "user_denied",
|
|
265
273
|
});
|
|
@@ -270,23 +278,21 @@ describe("handleToolCall decision events — user_denied", () => {
|
|
|
270
278
|
|
|
271
279
|
describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
272
280
|
it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
281
|
+
const { handler, events } = makeHandler({
|
|
282
|
+
session: {
|
|
283
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
284
|
+
canPrompt: vi.fn().mockReturnValue(false),
|
|
285
|
+
},
|
|
279
286
|
});
|
|
280
287
|
|
|
281
|
-
await handleToolCall(
|
|
282
|
-
deps,
|
|
288
|
+
await handler.handleToolCall(
|
|
283
289
|
makeToolCallEvent("read"),
|
|
284
290
|
makeCtx({ hasUI: false }),
|
|
285
291
|
);
|
|
286
292
|
|
|
287
|
-
const
|
|
288
|
-
expect(
|
|
289
|
-
expect(
|
|
293
|
+
const decisions = getDecisionEvents(events);
|
|
294
|
+
expect(decisions).toHaveLength(1);
|
|
295
|
+
expect(decisions[0]).toMatchObject({
|
|
290
296
|
result: "deny",
|
|
291
297
|
resolution: "confirmation_unavailable",
|
|
292
298
|
});
|
|
@@ -298,19 +304,20 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
|
298
304
|
describe("handleToolCall decision events — infrastructure_auto_allowed", () => {
|
|
299
305
|
it("emits allow with infrastructure_auto_allowed for Pi infra reads", async () => {
|
|
300
306
|
const infraDir = "/test/agent";
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
const { handler, events } = makeHandler({
|
|
308
|
+
session: {
|
|
309
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
310
|
+
getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
|
|
311
|
+
},
|
|
304
312
|
});
|
|
305
|
-
const deps = makeDeps({ session });
|
|
306
313
|
|
|
307
314
|
const event = makeToolCallEvent("read", {
|
|
308
315
|
input: { path: `${infraDir}/some-file.json` },
|
|
309
316
|
});
|
|
310
|
-
await handleToolCall(
|
|
317
|
+
await handler.handleToolCall(event, makeCtx());
|
|
311
318
|
|
|
312
|
-
const
|
|
313
|
-
const infraEvents =
|
|
319
|
+
const decisions = getDecisionEvents(events);
|
|
320
|
+
const infraEvents = decisions.filter(
|
|
314
321
|
(e) => e.resolution === "infrastructure_auto_allowed",
|
|
315
322
|
);
|
|
316
323
|
expect(infraEvents).toHaveLength(1);
|
|
@@ -324,24 +331,23 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
|
|
|
324
331
|
// ── auto_approved path (yolo mode) ───────────────────────────────────
|
|
325
332
|
|
|
326
333
|
describe("handleToolCall decision events — auto_approved", () => {
|
|
327
|
-
it("emits allow with auto_approved when
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}),
|
|
334
|
+
it("emits allow with auto_approved when prompt returns autoApproved:true", async () => {
|
|
335
|
+
const { handler, events } = makeHandler({
|
|
336
|
+
session: {
|
|
337
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
338
|
+
prompt: vi.fn().mockResolvedValue({
|
|
339
|
+
approved: true,
|
|
340
|
+
state: "approved",
|
|
341
|
+
autoApproved: true,
|
|
342
|
+
}),
|
|
343
|
+
},
|
|
338
344
|
});
|
|
339
345
|
|
|
340
|
-
await handleToolCall(
|
|
346
|
+
await handler.handleToolCall(makeToolCallEvent("read"), makeCtx());
|
|
341
347
|
|
|
342
|
-
const
|
|
343
|
-
expect(
|
|
344
|
-
expect(
|
|
348
|
+
const decisions = getDecisionEvents(events);
|
|
349
|
+
expect(decisions).toHaveLength(1);
|
|
350
|
+
expect(decisions[0]).toMatchObject({
|
|
345
351
|
result: "allow",
|
|
346
352
|
resolution: "auto_approved",
|
|
347
353
|
});
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
getEventInput,
|
|
6
|
+
PermissionGateHandler,
|
|
7
|
+
} from "../../src/handlers/permission-gate-handler";
|
|
6
8
|
import type { PermissionSession } from "../../src/permission-session";
|
|
9
|
+
import type { ToolRegistry } from "../../src/tool-registry";
|
|
7
10
|
import type { PermissionCheckResult, PermissionState } from "../../src/types";
|
|
8
11
|
|
|
9
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
@@ -71,26 +74,42 @@ function makeSession(
|
|
|
71
74
|
.fn()
|
|
72
75
|
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
73
76
|
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
77
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
78
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
74
79
|
...overrides,
|
|
75
80
|
} as unknown as PermissionSession;
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
function
|
|
83
|
+
function makeEvents() {
|
|
79
84
|
return {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
setActiveTools: vi.fn(),
|
|
85
|
+
emit: vi.fn(),
|
|
86
|
+
on: vi.fn().mockReturnValue(() => undefined),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
91
|
+
return {
|
|
92
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
93
|
+
setActive: vi.fn(),
|
|
90
94
|
...overrides,
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
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
|
+
|
|
94
113
|
// ── getEventInput ──────────────────────────────────────────────────────────
|
|
95
114
|
|
|
96
115
|
describe("getEventInput", () => {
|
|
@@ -122,14 +141,17 @@ describe("getEventInput", () => {
|
|
|
122
141
|
describe("handleToolCall", () => {
|
|
123
142
|
it("activates session with ctx", async () => {
|
|
124
143
|
const ctx = makeCtx();
|
|
125
|
-
const
|
|
126
|
-
await handleToolCall(
|
|
127
|
-
expect(
|
|
144
|
+
const { handler, session } = makeHandler();
|
|
145
|
+
await handler.handleToolCall(makeToolCallEvent("read"), ctx);
|
|
146
|
+
expect(session.activate).toHaveBeenCalledWith(ctx);
|
|
128
147
|
});
|
|
129
148
|
|
|
130
149
|
it("blocks when tool name cannot be resolved", async () => {
|
|
131
|
-
const
|
|
132
|
-
const result = await handleToolCall(
|
|
150
|
+
const { handler } = makeHandler();
|
|
151
|
+
const result = await handler.handleToolCall(
|
|
152
|
+
{ type: "tool_call" },
|
|
153
|
+
makeCtx(),
|
|
154
|
+
);
|
|
133
155
|
expect(result).toEqual({
|
|
134
156
|
block: true,
|
|
135
157
|
reason: expect.stringContaining("tool"),
|
|
@@ -137,11 +159,12 @@ describe("handleToolCall", () => {
|
|
|
137
159
|
});
|
|
138
160
|
|
|
139
161
|
it("blocks when tool is not registered", async () => {
|
|
140
|
-
const
|
|
141
|
-
|
|
162
|
+
const { handler } = makeHandler({
|
|
163
|
+
toolRegistry: {
|
|
164
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
165
|
+
},
|
|
142
166
|
});
|
|
143
|
-
const result = await handleToolCall(
|
|
144
|
-
deps,
|
|
167
|
+
const result = await handler.handleToolCall(
|
|
145
168
|
makeToolCallEvent("unknown-tool"),
|
|
146
169
|
makeCtx(),
|
|
147
170
|
);
|
|
@@ -149,9 +172,8 @@ describe("handleToolCall", () => {
|
|
|
149
172
|
});
|
|
150
173
|
|
|
151
174
|
it("returns empty object when tool is allowed", async () => {
|
|
152
|
-
const
|
|
153
|
-
const result = await handleToolCall(
|
|
154
|
-
deps,
|
|
175
|
+
const { handler } = makeHandler();
|
|
176
|
+
const result = await handler.handleToolCall(
|
|
155
177
|
makeToolCallEvent("read"),
|
|
156
178
|
makeCtx(),
|
|
157
179
|
);
|
|
@@ -159,12 +181,12 @@ describe("handleToolCall", () => {
|
|
|
159
181
|
});
|
|
160
182
|
|
|
161
183
|
it("blocks when tool is denied by policy", async () => {
|
|
162
|
-
const
|
|
163
|
-
|
|
184
|
+
const { handler } = makeHandler({
|
|
185
|
+
session: {
|
|
186
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
|
|
187
|
+
},
|
|
164
188
|
});
|
|
165
|
-
const
|
|
166
|
-
const result = await handleToolCall(
|
|
167
|
-
deps,
|
|
189
|
+
const result = await handler.handleToolCall(
|
|
168
190
|
makeToolCallEvent("read"),
|
|
169
191
|
makeCtx(),
|
|
170
192
|
);
|
|
@@ -184,12 +206,13 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
184
206
|
normalizedLocation: "/skills/librarian/SKILL.md",
|
|
185
207
|
normalizedBaseDir: "/skills/librarian",
|
|
186
208
|
};
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
const { handler } = makeHandler({
|
|
210
|
+
session: {
|
|
211
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
|
|
212
|
+
},
|
|
213
|
+
toolRegistry: {
|
|
214
|
+
getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
215
|
+
},
|
|
193
216
|
});
|
|
194
217
|
const event = {
|
|
195
218
|
type: "tool_call",
|
|
@@ -197,7 +220,7 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
197
220
|
toolName: "read",
|
|
198
221
|
input: { path: "/skills/librarian/SKILL.md" },
|
|
199
222
|
};
|
|
200
|
-
const result = await handleToolCall(
|
|
223
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
201
224
|
expect(result).toMatchObject({ block: true });
|
|
202
225
|
});
|
|
203
226
|
|
|
@@ -210,12 +233,13 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
210
233
|
normalizedLocation: "/skills/librarian/SKILL.md",
|
|
211
234
|
normalizedBaseDir: "/skills/librarian",
|
|
212
235
|
};
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
const { handler } = makeHandler({
|
|
237
|
+
session: {
|
|
238
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
|
|
239
|
+
},
|
|
240
|
+
toolRegistry: {
|
|
241
|
+
getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
242
|
+
},
|
|
219
243
|
});
|
|
220
244
|
const event = {
|
|
221
245
|
type: "tool_call",
|
|
@@ -223,7 +247,7 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
223
247
|
toolName: "read",
|
|
224
248
|
input: { path: "/test/project/src/index.ts" },
|
|
225
249
|
};
|
|
226
|
-
const result = await handleToolCall(
|
|
250
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
227
251
|
expect(result).toEqual({});
|
|
228
252
|
});
|
|
229
253
|
});
|
|
@@ -232,12 +256,13 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
232
256
|
|
|
233
257
|
describe("handleToolCall — external-directory gate", () => {
|
|
234
258
|
it("blocks a read of a path outside cwd when policy is deny", async () => {
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
259
|
+
const { handler } = makeHandler({
|
|
260
|
+
session: {
|
|
261
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
|
|
262
|
+
},
|
|
263
|
+
toolRegistry: {
|
|
264
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
265
|
+
},
|
|
241
266
|
});
|
|
242
267
|
const event = {
|
|
243
268
|
type: "tool_call",
|
|
@@ -245,7 +270,7 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
245
270
|
name: "read",
|
|
246
271
|
input: { path: "/outside/project/file.ts" },
|
|
247
272
|
};
|
|
248
|
-
const result = await handleToolCall(
|
|
273
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
249
274
|
expect(result).toMatchObject({ block: true });
|
|
250
275
|
});
|
|
251
276
|
});
|
|
@@ -254,12 +279,13 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
254
279
|
|
|
255
280
|
describe("handleToolCall — bash external-directory gate", () => {
|
|
256
281
|
it("blocks a bash command referencing an external path when policy is deny", async () => {
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
282
|
+
const { handler } = makeHandler({
|
|
283
|
+
session: {
|
|
284
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
|
|
285
|
+
},
|
|
286
|
+
toolRegistry: {
|
|
287
|
+
getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
288
|
+
},
|
|
263
289
|
});
|
|
264
290
|
const event = {
|
|
265
291
|
type: "tool_call",
|
|
@@ -267,7 +293,7 @@ describe("handleToolCall — bash external-directory gate", () => {
|
|
|
267
293
|
name: "bash",
|
|
268
294
|
input: { command: "cat /outside/project/file.ts" },
|
|
269
295
|
};
|
|
270
|
-
const result = await handleToolCall(
|
|
296
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
271
297
|
expect(result).toMatchObject({ block: true });
|
|
272
298
|
});
|
|
273
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,
|