@gotgenes/pi-permission-system 3.6.0 → 3.8.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 +36 -0
- package/package.json +1 -1
- package/src/forwarded-permissions/io.ts +47 -12
- package/src/forwarded-permissions/polling.ts +33 -11
- package/src/handlers/before-agent-start.ts +112 -0
- package/src/handlers/index.ts +16 -0
- package/src/handlers/input.ts +99 -0
- package/src/handlers/lifecycle.ts +81 -0
- package/src/handlers/tool-call.ts +410 -0
- package/src/handlers/types.ts +72 -0
- package/src/index.ts +73 -1040
- package/src/runtime.ts +484 -0
- package/tests/forwarded-permissions/io.test.ts +135 -0
- package/tests/handlers/before-agent-start.test.ts +290 -0
- package/tests/handlers/input.test.ts +301 -0
- package/tests/handlers/lifecycle.test.ts +352 -0
- package/tests/handlers/tool-call.test.ts +441 -0
- package/tests/runtime.test.ts +618 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { getEventInput, handleToolCall } from "../../src/handlers/tool-call";
|
|
5
|
+
import type { HandlerDeps } from "../../src/handlers/types";
|
|
6
|
+
import type { ExtensionRuntime } from "../../src/runtime";
|
|
7
|
+
import type { PermissionCheckResult } from "../../src/types";
|
|
8
|
+
|
|
9
|
+
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
10
|
+
vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
|
|
11
|
+
const original =
|
|
12
|
+
await importOriginal<typeof import("@mariozechner/pi-coding-agent")>();
|
|
13
|
+
return { ...original };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function makeCtx(
|
|
19
|
+
overrides: Partial<ExtensionContext> & { cwd?: string } = {},
|
|
20
|
+
): ExtensionContext {
|
|
21
|
+
return {
|
|
22
|
+
cwd: "/test/project",
|
|
23
|
+
hasUI: true,
|
|
24
|
+
ui: {
|
|
25
|
+
setStatus: vi.fn(),
|
|
26
|
+
notify: vi.fn(),
|
|
27
|
+
select: vi.fn(),
|
|
28
|
+
input: vi.fn(),
|
|
29
|
+
},
|
|
30
|
+
sessionManager: {
|
|
31
|
+
getEntries: vi.fn().mockReturnValue([]),
|
|
32
|
+
getSessionDir: vi.fn().mockReturnValue("/sessions/test"),
|
|
33
|
+
addEntry: vi.fn(),
|
|
34
|
+
},
|
|
35
|
+
...overrides,
|
|
36
|
+
} as unknown as ExtensionContext;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function makeToolCallEvent(
|
|
40
|
+
toolName: string,
|
|
41
|
+
extraFields: Record<string, unknown> = {},
|
|
42
|
+
) {
|
|
43
|
+
return {
|
|
44
|
+
type: "tool_call",
|
|
45
|
+
toolCallId: "tc-1",
|
|
46
|
+
name: toolName,
|
|
47
|
+
input: {},
|
|
48
|
+
...extraFields,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makePermissionResult(
|
|
53
|
+
state: "allow" | "deny" | "ask",
|
|
54
|
+
): PermissionCheckResult {
|
|
55
|
+
return { state, toolName: "read", source: "tool" };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function makeRuntime(
|
|
59
|
+
overrides: Partial<ExtensionRuntime> = {},
|
|
60
|
+
): ExtensionRuntime {
|
|
61
|
+
return {
|
|
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: {
|
|
70
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
|
|
71
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
72
|
+
activeSkillEntries: [],
|
|
73
|
+
lastKnownActiveAgentName: null,
|
|
74
|
+
lastActiveToolsCacheKey: null,
|
|
75
|
+
lastPromptStateCacheKey: null,
|
|
76
|
+
lastConfigWarning: null,
|
|
77
|
+
sessionApprovalCache: {
|
|
78
|
+
approve: vi.fn(),
|
|
79
|
+
has: vi.fn().mockReturnValue(false),
|
|
80
|
+
findMatchingPrefix: vi.fn().mockReturnValue(null),
|
|
81
|
+
clear: vi.fn(),
|
|
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(),
|
|
95
|
+
createPermissionManagerForCwd: vi.fn(),
|
|
96
|
+
refreshExtensionConfig: vi.fn(),
|
|
97
|
+
notifyWarning: vi.fn(),
|
|
98
|
+
logResolvedConfigPaths: vi.fn(),
|
|
99
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
100
|
+
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
101
|
+
promptPermission: vi
|
|
102
|
+
.fn()
|
|
103
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
104
|
+
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
105
|
+
startForwardedPermissionPolling: vi.fn(),
|
|
106
|
+
stopForwardedPermissionPolling: vi.fn(),
|
|
107
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
108
|
+
setActiveTools: vi.fn(),
|
|
109
|
+
...overrides,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── getEventInput ──────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
describe("getEventInput", () => {
|
|
116
|
+
it("returns the input field when present", () => {
|
|
117
|
+
expect(getEventInput({ input: { path: "/foo" } })).toEqual({
|
|
118
|
+
path: "/foo",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("returns the arguments field when input is absent", () => {
|
|
123
|
+
expect(getEventInput({ arguments: { command: "ls" } })).toEqual({
|
|
124
|
+
command: "ls",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("returns empty object when neither field is present", () => {
|
|
129
|
+
expect(getEventInput({ type: "tool_call" })).toEqual({});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("prefers input over arguments when both are present", () => {
|
|
133
|
+
expect(getEventInput({ input: { a: 1 }, arguments: { b: 2 } })).toEqual({
|
|
134
|
+
a: 1,
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── handleToolCall ─────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
describe("handleToolCall", () => {
|
|
142
|
+
it("sets runtime context", async () => {
|
|
143
|
+
const ctx = makeCtx();
|
|
144
|
+
const deps = makeDeps();
|
|
145
|
+
await handleToolCall(deps, makeToolCallEvent("read"), ctx);
|
|
146
|
+
expect(deps.runtime.runtimeContext).toBe(ctx);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("starts forwarded permission polling", async () => {
|
|
150
|
+
const ctx = makeCtx();
|
|
151
|
+
const deps = makeDeps();
|
|
152
|
+
await handleToolCall(deps, makeToolCallEvent("read"), ctx);
|
|
153
|
+
expect(deps.startForwardedPermissionPolling).toHaveBeenCalledWith(ctx);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("blocks when tool name cannot be resolved", async () => {
|
|
157
|
+
const deps = makeDeps();
|
|
158
|
+
// An event with no recognisable name field
|
|
159
|
+
const result = await handleToolCall(deps, { type: "tool_call" }, makeCtx());
|
|
160
|
+
expect(result).toEqual({
|
|
161
|
+
block: true,
|
|
162
|
+
reason: expect.stringContaining("tool"),
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("blocks when tool is not registered", async () => {
|
|
167
|
+
const deps = makeDeps({
|
|
168
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
169
|
+
});
|
|
170
|
+
const result = await handleToolCall(
|
|
171
|
+
deps,
|
|
172
|
+
makeToolCallEvent("unknown-tool"),
|
|
173
|
+
makeCtx(),
|
|
174
|
+
);
|
|
175
|
+
expect(result).toMatchObject({ block: true });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("returns empty object when tool is allowed", async () => {
|
|
179
|
+
// default makeRuntime() has checkPermission → "allow"
|
|
180
|
+
const deps = makeDeps();
|
|
181
|
+
const result = await handleToolCall(
|
|
182
|
+
deps,
|
|
183
|
+
makeToolCallEvent("read"),
|
|
184
|
+
makeCtx(),
|
|
185
|
+
);
|
|
186
|
+
expect(result).toEqual({});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("blocks when tool is denied by policy", async () => {
|
|
190
|
+
const deps = makeDeps({
|
|
191
|
+
runtime: makeRuntime({
|
|
192
|
+
permissionManager: {
|
|
193
|
+
checkPermission: vi
|
|
194
|
+
.fn()
|
|
195
|
+
.mockReturnValue(makePermissionResult("deny")),
|
|
196
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
197
|
+
}),
|
|
198
|
+
});
|
|
199
|
+
const result = await handleToolCall(
|
|
200
|
+
deps,
|
|
201
|
+
makeToolCallEvent("read"),
|
|
202
|
+
makeCtx(),
|
|
203
|
+
);
|
|
204
|
+
expect(result).toMatchObject({ block: true });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("blocks when tool ask has no UI available", async () => {
|
|
208
|
+
const deps = makeDeps({
|
|
209
|
+
runtime: makeRuntime({
|
|
210
|
+
permissionManager: {
|
|
211
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
|
|
212
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
213
|
+
}),
|
|
214
|
+
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
215
|
+
});
|
|
216
|
+
const result = await handleToolCall(
|
|
217
|
+
deps,
|
|
218
|
+
makeToolCallEvent("read"),
|
|
219
|
+
makeCtx(),
|
|
220
|
+
);
|
|
221
|
+
expect(result).toMatchObject({ block: true });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it("allows when user approves the ask prompt", async () => {
|
|
225
|
+
const deps = makeDeps({
|
|
226
|
+
runtime: makeRuntime({
|
|
227
|
+
permissionManager: {
|
|
228
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
|
|
229
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
230
|
+
}),
|
|
231
|
+
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
232
|
+
promptPermission: vi
|
|
233
|
+
.fn()
|
|
234
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
235
|
+
});
|
|
236
|
+
const result = await handleToolCall(
|
|
237
|
+
deps,
|
|
238
|
+
makeToolCallEvent("read"),
|
|
239
|
+
makeCtx(),
|
|
240
|
+
);
|
|
241
|
+
expect(result).toEqual({});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("blocks when user denies the ask prompt", async () => {
|
|
245
|
+
const deps = makeDeps({
|
|
246
|
+
runtime: makeRuntime({
|
|
247
|
+
permissionManager: {
|
|
248
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
|
|
249
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
250
|
+
}),
|
|
251
|
+
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
252
|
+
promptPermission: vi
|
|
253
|
+
.fn()
|
|
254
|
+
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
255
|
+
});
|
|
256
|
+
const result = await handleToolCall(
|
|
257
|
+
deps,
|
|
258
|
+
makeToolCallEvent("read"),
|
|
259
|
+
makeCtx(),
|
|
260
|
+
);
|
|
261
|
+
expect(result).toMatchObject({ block: true });
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ── skill-read gate ────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe("handleToolCall — skill-read gate", () => {
|
|
268
|
+
it("blocks a read of a denied skill path", async () => {
|
|
269
|
+
const skillEntry = {
|
|
270
|
+
name: "librarian",
|
|
271
|
+
description: "Research skills",
|
|
272
|
+
location: "/skills/librarian/SKILL.md",
|
|
273
|
+
state: "deny" as const,
|
|
274
|
+
normalizedLocation: "/skills/librarian/SKILL.md",
|
|
275
|
+
normalizedBaseDir: "/skills/librarian",
|
|
276
|
+
};
|
|
277
|
+
const deps = makeDeps({
|
|
278
|
+
runtime: makeRuntime({ activeSkillEntries: [skillEntry] }),
|
|
279
|
+
getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
280
|
+
});
|
|
281
|
+
const event = {
|
|
282
|
+
type: "tool_call",
|
|
283
|
+
toolCallId: "tc-skill",
|
|
284
|
+
toolName: "read",
|
|
285
|
+
input: { path: "/skills/librarian/SKILL.md" },
|
|
286
|
+
};
|
|
287
|
+
const result = await handleToolCall(deps, event, makeCtx());
|
|
288
|
+
expect(result).toMatchObject({ block: true });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("allows a read of a non-skill path even when skill entries are present", async () => {
|
|
292
|
+
const skillEntry = {
|
|
293
|
+
name: "librarian",
|
|
294
|
+
description: "Research skills",
|
|
295
|
+
location: "/skills/librarian/SKILL.md",
|
|
296
|
+
state: "deny" as const,
|
|
297
|
+
normalizedLocation: "/skills/librarian/SKILL.md",
|
|
298
|
+
normalizedBaseDir: "/skills/librarian",
|
|
299
|
+
};
|
|
300
|
+
const deps = makeDeps({
|
|
301
|
+
runtime: makeRuntime({ activeSkillEntries: [skillEntry] }),
|
|
302
|
+
getAllTools: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
303
|
+
});
|
|
304
|
+
const event = {
|
|
305
|
+
type: "tool_call",
|
|
306
|
+
toolCallId: "tc-ok",
|
|
307
|
+
toolName: "read",
|
|
308
|
+
input: { path: "/test/project/src/index.ts" },
|
|
309
|
+
};
|
|
310
|
+
const result = await handleToolCall(deps, event, makeCtx());
|
|
311
|
+
expect(result).toEqual({});
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ── external-directory gate ────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
describe("handleToolCall — external-directory gate", () => {
|
|
318
|
+
it("blocks a read of a path outside cwd when policy is deny", async () => {
|
|
319
|
+
const deps = makeDeps({
|
|
320
|
+
runtime: makeRuntime({
|
|
321
|
+
permissionManager: {
|
|
322
|
+
checkPermission: vi
|
|
323
|
+
.fn()
|
|
324
|
+
.mockReturnValue(makePermissionResult("deny")),
|
|
325
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
326
|
+
}),
|
|
327
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
328
|
+
});
|
|
329
|
+
const event = {
|
|
330
|
+
type: "tool_call",
|
|
331
|
+
toolCallId: "tc-ext",
|
|
332
|
+
name: "read",
|
|
333
|
+
input: { path: "/outside/project/file.ts" },
|
|
334
|
+
};
|
|
335
|
+
const result = await handleToolCall(deps, event, makeCtx());
|
|
336
|
+
expect(result).toMatchObject({ block: true });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it("allows when session has an existing approval for the external path", async () => {
|
|
340
|
+
const deps = makeDeps({
|
|
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"],
|
|
348
|
+
}),
|
|
349
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
350
|
+
});
|
|
351
|
+
const event = {
|
|
352
|
+
type: "tool_call",
|
|
353
|
+
toolCallId: "tc-session",
|
|
354
|
+
name: "read",
|
|
355
|
+
input: { path: "/outside/project/file.ts" },
|
|
356
|
+
};
|
|
357
|
+
const result = await handleToolCall(deps, event, makeCtx());
|
|
358
|
+
expect(result).toEqual({});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("approves session when user selects approved_for_session", async () => {
|
|
362
|
+
const approveCache = {
|
|
363
|
+
approve: vi.fn(),
|
|
364
|
+
has: vi.fn().mockReturnValue(false),
|
|
365
|
+
findMatchingPrefix: vi.fn().mockReturnValue(null),
|
|
366
|
+
clear: vi.fn(),
|
|
367
|
+
} as unknown as ExtensionRuntime["sessionApprovalCache"];
|
|
368
|
+
const deps = makeDeps({
|
|
369
|
+
runtime: makeRuntime({
|
|
370
|
+
permissionManager: {
|
|
371
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("ask")),
|
|
372
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
373
|
+
sessionApprovalCache: approveCache,
|
|
374
|
+
}),
|
|
375
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
376
|
+
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
377
|
+
promptPermission: vi
|
|
378
|
+
.fn()
|
|
379
|
+
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
380
|
+
});
|
|
381
|
+
const event = {
|
|
382
|
+
type: "tool_call",
|
|
383
|
+
toolCallId: "tc-sess-approve",
|
|
384
|
+
name: "read",
|
|
385
|
+
input: { path: "/outside/project/file.ts" },
|
|
386
|
+
};
|
|
387
|
+
await handleToolCall(deps, event, makeCtx());
|
|
388
|
+
expect(approveCache.approve).toHaveBeenCalledWith(
|
|
389
|
+
"external_directory",
|
|
390
|
+
expect.any(String),
|
|
391
|
+
);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ── bash external-directory gate ──────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
describe("handleToolCall — bash external-directory gate", () => {
|
|
398
|
+
it("blocks a bash command referencing an external path when policy is deny", async () => {
|
|
399
|
+
const deps = makeDeps({
|
|
400
|
+
runtime: makeRuntime({
|
|
401
|
+
permissionManager: {
|
|
402
|
+
checkPermission: vi
|
|
403
|
+
.fn()
|
|
404
|
+
.mockReturnValue(makePermissionResult("deny")),
|
|
405
|
+
} as unknown as ExtensionRuntime["permissionManager"],
|
|
406
|
+
}),
|
|
407
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
408
|
+
});
|
|
409
|
+
const event = {
|
|
410
|
+
type: "tool_call",
|
|
411
|
+
toolCallId: "tc-bash-ext",
|
|
412
|
+
name: "bash",
|
|
413
|
+
input: { command: "cat /outside/project/file.ts" },
|
|
414
|
+
};
|
|
415
|
+
const result = await handleToolCall(deps, event, makeCtx());
|
|
416
|
+
expect(result).toMatchObject({ block: true });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("skips bash external gate when all referenced paths are session-approved", async () => {
|
|
420
|
+
const deps = makeDeps({
|
|
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"],
|
|
429
|
+
}),
|
|
430
|
+
getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
431
|
+
});
|
|
432
|
+
const event = {
|
|
433
|
+
type: "tool_call",
|
|
434
|
+
toolCallId: "tc-bash-sess",
|
|
435
|
+
name: "bash",
|
|
436
|
+
input: { command: "cat /outside/project/file.ts" },
|
|
437
|
+
};
|
|
438
|
+
const result = await handleToolCall(deps, event, makeCtx());
|
|
439
|
+
expect(result).toEqual({});
|
|
440
|
+
});
|
|
441
|
+
});
|