@gotgenes/pi-permission-system 5.8.0 → 5.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +76 -0
- package/src/handlers/before-agent-start.ts +19 -32
- package/src/handlers/input.ts +5 -5
- package/src/handlers/lifecycle.ts +17 -33
- package/src/handlers/tool-call.ts +11 -18
- package/src/handlers/types.ts +11 -38
- package/src/index.ts +15 -19
- package/src/permission-session.ts +252 -0
- package/src/runtime.ts +5 -96
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/tests/forwarding-manager.test.ts +211 -0
- package/tests/handlers/before-agent-start.test.ts +79 -111
- package/tests/handlers/input-events.test.ts +19 -32
- package/tests/handlers/input.test.ts +41 -74
- package/tests/handlers/lifecycle.test.ts +61 -180
- package/tests/handlers/tool-call-events.test.ts +66 -93
- package/tests/handlers/tool-call.test.ts +40 -62
- package/tests/permission-session.test.ts +546 -0
- package/tests/runtime.test.ts +2 -92
|
@@ -6,24 +6,9 @@ import {
|
|
|
6
6
|
handleSessionStart,
|
|
7
7
|
} from "../../src/handlers/lifecycle";
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
|
-
import type {
|
|
10
|
-
import type { SessionState } from "../../src/runtime";
|
|
11
|
-
import type { SessionRules } from "../../src/session-rules";
|
|
12
|
-
import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
|
|
9
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
13
10
|
|
|
14
|
-
// ──
|
|
15
|
-
const { mockGetActiveAgentName } = vi.hoisted(() => ({
|
|
16
|
-
mockGetActiveAgentName: vi.fn<(ctx: ExtensionContext) => string | null>(),
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
vi.mock("../../src/active-agent", () => ({
|
|
20
|
-
getActiveAgentName: mockGetActiveAgentName,
|
|
21
|
-
getActiveAgentNameFromSystemPrompt: vi.fn().mockReturnValue(null),
|
|
22
|
-
}));
|
|
23
|
-
|
|
24
|
-
// ── PERMISSION_SYSTEM_STATUS_KEY stub ──────────────────────────────────────
|
|
25
|
-
// status.ts is re-exported through the handler; the key value doesn't matter
|
|
26
|
-
// for these tests.
|
|
11
|
+
// ── status stub ────────────────────────────────────────────────────────────
|
|
27
12
|
vi.mock("../../src/status", () => ({
|
|
28
13
|
PERMISSION_SYSTEM_STATUS_KEY: "permission-system",
|
|
29
14
|
syncPermissionSystemStatus: vi.fn(),
|
|
@@ -51,55 +36,32 @@ function makeCtx(overrides: Partial<ExtensionContext> = {}): ExtensionContext {
|
|
|
51
36
|
} as unknown as ExtensionContext;
|
|
52
37
|
}
|
|
53
38
|
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
):
|
|
57
|
-
return {
|
|
58
|
-
getConfigIssues: vi.fn().mockReturnValue(issues),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function makeSessionRules(): SessionRules {
|
|
63
|
-
return {
|
|
64
|
-
approve: vi.fn(),
|
|
65
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
66
|
-
clear: vi.fn(),
|
|
67
|
-
} as unknown as SessionRules;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
39
|
+
function makeSession(
|
|
40
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
41
|
+
): PermissionSession {
|
|
71
42
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
43
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
44
|
+
refreshConfig: vi.fn(),
|
|
45
|
+
resetForNewSession: vi.fn(),
|
|
46
|
+
logResolvedConfigPaths: vi.fn(),
|
|
47
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
48
|
+
getConfigIssues: vi.fn().mockReturnValue([]),
|
|
49
|
+
reload: vi.fn(),
|
|
50
|
+
getRuntimeContext: vi.fn().mockReturnValue(null),
|
|
51
|
+
shutdown: vi.fn(),
|
|
79
52
|
...overrides,
|
|
80
|
-
};
|
|
53
|
+
} as unknown as PermissionSession;
|
|
81
54
|
}
|
|
82
55
|
|
|
83
56
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
84
57
|
return {
|
|
85
58
|
session: makeSession(),
|
|
86
|
-
|
|
87
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
88
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
89
|
-
createPermissionManagerForCwd: vi
|
|
90
|
-
.fn()
|
|
91
|
-
.mockReturnValue(makePermissionManager()),
|
|
92
|
-
refreshExtensionConfig: vi.fn(),
|
|
93
|
-
logResolvedConfigPaths: vi.fn(),
|
|
94
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
59
|
+
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
95
60
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
96
61
|
promptPermission: vi
|
|
97
62
|
.fn()
|
|
98
63
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
99
64
|
createPermissionRequestId: vi.fn().mockReturnValue("test-id"),
|
|
100
|
-
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
101
|
-
startForwardedPermissionPolling: vi.fn(),
|
|
102
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
103
65
|
stopPermissionRpcHandlers: vi.fn(),
|
|
104
66
|
getAllTools: vi.fn().mockReturnValue([]),
|
|
105
67
|
setActiveTools: vi.fn(),
|
|
@@ -110,98 +72,54 @@ function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
|
110
72
|
// ── handleSessionStart ─────────────────────────────────────────────────────
|
|
111
73
|
|
|
112
74
|
describe("handleSessionStart", () => {
|
|
113
|
-
|
|
114
|
-
mockGetActiveAgentName.mockReset();
|
|
115
|
-
mockGetActiveAgentName.mockReturnValue(null);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("sets the runtime context", async () => {
|
|
119
|
-
const ctx = makeCtx();
|
|
120
|
-
const deps = makeDeps();
|
|
121
|
-
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
122
|
-
expect(deps.session.runtimeContext).toBe(ctx);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it("refreshes extension config with ctx", async () => {
|
|
126
|
-
const ctx = makeCtx();
|
|
127
|
-
const deps = makeDeps();
|
|
128
|
-
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
129
|
-
expect(deps.refreshExtensionConfig).toHaveBeenCalledWith(ctx);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it("creates a new permission manager for ctx.cwd and stores it", async () => {
|
|
133
|
-
const ctx = makeCtx({ cwd: "/my/project" });
|
|
134
|
-
const newPm = makePermissionManager();
|
|
135
|
-
const deps = makeDeps({
|
|
136
|
-
createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
|
|
137
|
-
});
|
|
138
|
-
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
139
|
-
expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
|
|
140
|
-
"/my/project",
|
|
141
|
-
);
|
|
142
|
-
expect(deps.session.permissionManager).toBe(newPm);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it("clears the before_agent_start cache", async () => {
|
|
146
|
-
const ctx = makeCtx();
|
|
147
|
-
const deps = makeDeps();
|
|
148
|
-
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
149
|
-
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
150
|
-
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
151
|
-
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("sets lastKnownActiveAgentName from getActiveAgentName", async () => {
|
|
155
|
-
mockGetActiveAgentName.mockReturnValue("my-agent");
|
|
75
|
+
it("refreshes config with ctx", async () => {
|
|
156
76
|
const ctx = makeCtx();
|
|
157
77
|
const deps = makeDeps();
|
|
158
78
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
159
|
-
expect(deps.session.
|
|
79
|
+
expect(deps.session.refreshConfig).toHaveBeenCalledWith(ctx);
|
|
160
80
|
});
|
|
161
81
|
|
|
162
|
-
it("
|
|
163
|
-
mockGetActiveAgentName.mockReturnValue(null);
|
|
82
|
+
it("calls resetForNewSession with ctx", async () => {
|
|
164
83
|
const ctx = makeCtx();
|
|
165
84
|
const deps = makeDeps();
|
|
166
85
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
167
|
-
expect(deps.session.
|
|
86
|
+
expect(deps.session.resetForNewSession).toHaveBeenCalledWith(ctx);
|
|
168
87
|
});
|
|
169
88
|
|
|
170
|
-
it("
|
|
171
|
-
const ctx = makeCtx();
|
|
89
|
+
it("logs resolved config paths", async () => {
|
|
172
90
|
const deps = makeDeps();
|
|
173
|
-
await handleSessionStart(deps, { reason: "startup" },
|
|
174
|
-
expect(deps.
|
|
91
|
+
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
92
|
+
expect(deps.session.logResolvedConfigPaths).toHaveBeenCalledOnce();
|
|
175
93
|
});
|
|
176
94
|
|
|
177
|
-
it("
|
|
95
|
+
it("resolves agent name from ctx", async () => {
|
|
178
96
|
const ctx = makeCtx();
|
|
179
97
|
const deps = makeDeps();
|
|
180
98
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
181
|
-
expect(deps.
|
|
99
|
+
expect(deps.session.resolveAgentName).toHaveBeenCalledWith(ctx);
|
|
182
100
|
});
|
|
183
101
|
|
|
184
102
|
it("notifies each policy issue", async () => {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
createPermissionManagerForCwd: vi.fn().mockReturnValue(pm),
|
|
103
|
+
const session = makeSession({
|
|
104
|
+
getConfigIssues: vi.fn().mockReturnValue(["issue A", "issue B"]),
|
|
188
105
|
});
|
|
106
|
+
const deps = makeDeps({ session });
|
|
189
107
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
108
|
+
expect(session.logger.warn).toHaveBeenCalledWith("issue A");
|
|
109
|
+
expect(session.logger.warn).toHaveBeenCalledWith("issue B");
|
|
192
110
|
});
|
|
193
111
|
|
|
194
|
-
it("does not
|
|
112
|
+
it("does not warn when there are no policy issues", async () => {
|
|
195
113
|
const deps = makeDeps();
|
|
196
114
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
197
|
-
expect(deps.logger.warn).not.toHaveBeenCalled();
|
|
115
|
+
expect(deps.session.logger.warn).not.toHaveBeenCalled();
|
|
198
116
|
});
|
|
199
117
|
|
|
200
118
|
it("writes lifecycle.reload debug log when reason is reload", async () => {
|
|
201
119
|
const ctx = makeCtx({ cwd: "/proj" });
|
|
202
120
|
const deps = makeDeps();
|
|
203
121
|
await handleSessionStart(deps, { reason: "reload" }, ctx);
|
|
204
|
-
expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
122
|
+
expect(deps.session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
205
123
|
triggeredBy: "session_start",
|
|
206
124
|
reason: "reload",
|
|
207
125
|
cwd: "/proj",
|
|
@@ -211,7 +129,18 @@ describe("handleSessionStart", () => {
|
|
|
211
129
|
it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
|
|
212
130
|
const deps = makeDeps();
|
|
213
131
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
214
|
-
expect(deps.logger.debug).not.toHaveBeenCalled();
|
|
132
|
+
expect(deps.session.logger.debug).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("calls refreshConfig before resetForNewSession", async () => {
|
|
136
|
+
const callOrder: string[] = [];
|
|
137
|
+
const session = makeSession({
|
|
138
|
+
refreshConfig: vi.fn(() => callOrder.push("refreshConfig")),
|
|
139
|
+
resetForNewSession: vi.fn(() => callOrder.push("resetForNewSession")),
|
|
140
|
+
});
|
|
141
|
+
const deps = makeDeps({ session });
|
|
142
|
+
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
143
|
+
expect(callOrder).toEqual(["refreshConfig", "resetForNewSession"]);
|
|
215
144
|
});
|
|
216
145
|
});
|
|
217
146
|
|
|
@@ -221,43 +150,23 @@ describe("handleResourcesDiscover", () => {
|
|
|
221
150
|
it("does nothing when reason is not reload", async () => {
|
|
222
151
|
const deps = makeDeps();
|
|
223
152
|
await handleResourcesDiscover(deps, { reason: "startup" });
|
|
224
|
-
expect(deps.
|
|
225
|
-
expect(deps.logger.debug).not.toHaveBeenCalled();
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
|
|
229
|
-
const ctx = makeCtx({ cwd: "/runtime/cwd" });
|
|
230
|
-
const newPm = makePermissionManager();
|
|
231
|
-
const deps = makeDeps({
|
|
232
|
-
session: makeSession({ runtimeContext: ctx }),
|
|
233
|
-
createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
|
|
234
|
-
});
|
|
235
|
-
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
236
|
-
expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
|
|
237
|
-
"/runtime/cwd",
|
|
238
|
-
);
|
|
239
|
-
expect(deps.session.permissionManager).toBe(newPm);
|
|
153
|
+
expect(deps.session.reload).not.toHaveBeenCalled();
|
|
240
154
|
});
|
|
241
155
|
|
|
242
|
-
it("
|
|
156
|
+
it("calls reload on the session on reload", async () => {
|
|
243
157
|
const deps = makeDeps();
|
|
244
158
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
245
|
-
expect(deps.
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
it("clears the before_agent_start cache on reload", async () => {
|
|
249
|
-
const deps = makeDeps();
|
|
250
|
-
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
251
|
-
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
252
|
-
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
253
|
-
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
159
|
+
expect(deps.session.reload).toHaveBeenCalledOnce();
|
|
254
160
|
});
|
|
255
161
|
|
|
256
162
|
it("writes lifecycle.reload debug log on reload", async () => {
|
|
257
163
|
const ctx = makeCtx({ cwd: "/proj" });
|
|
258
|
-
const
|
|
164
|
+
const session = makeSession({
|
|
165
|
+
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
166
|
+
});
|
|
167
|
+
const deps = makeDeps({ session });
|
|
259
168
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
260
|
-
expect(
|
|
169
|
+
expect(session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
261
170
|
triggeredBy: "resources_discover",
|
|
262
171
|
reason: "reload",
|
|
263
172
|
cwd: "/proj",
|
|
@@ -267,7 +176,7 @@ describe("handleResourcesDiscover", () => {
|
|
|
267
176
|
it("logs cwd as null when runtimeContext is null on reload", async () => {
|
|
268
177
|
const deps = makeDeps();
|
|
269
178
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
270
|
-
expect(deps.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
179
|
+
expect(deps.session.logger.debug).toHaveBeenCalledWith("lifecycle.reload", {
|
|
271
180
|
triggeredBy: "resources_discover",
|
|
272
181
|
reason: "reload",
|
|
273
182
|
cwd: null,
|
|
@@ -278,11 +187,12 @@ describe("handleResourcesDiscover", () => {
|
|
|
278
187
|
// ── handleSessionShutdown ──────────────────────────────────────────────────
|
|
279
188
|
|
|
280
189
|
describe("handleSessionShutdown", () => {
|
|
281
|
-
it("clears
|
|
190
|
+
it("clears UI status when runtime context is present", async () => {
|
|
282
191
|
const ctx = makeCtx();
|
|
283
|
-
const
|
|
284
|
-
|
|
192
|
+
const session = makeSession({
|
|
193
|
+
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
285
194
|
});
|
|
195
|
+
const deps = makeDeps({ session });
|
|
286
196
|
await handleSessionShutdown(deps);
|
|
287
197
|
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
288
198
|
"permission-system",
|
|
@@ -295,44 +205,15 @@ describe("handleSessionShutdown", () => {
|
|
|
295
205
|
await expect(handleSessionShutdown(deps)).resolves.not.toThrow();
|
|
296
206
|
});
|
|
297
207
|
|
|
298
|
-
it("
|
|
299
|
-
const ctx = makeCtx();
|
|
300
|
-
const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
|
|
301
|
-
await handleSessionShutdown(deps);
|
|
302
|
-
expect(deps.session.runtimeContext).toBeNull();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it("clears the before_agent_start cache", async () => {
|
|
208
|
+
it("calls shutdown on the session", async () => {
|
|
306
209
|
const deps = makeDeps();
|
|
307
210
|
await handleSessionShutdown(deps);
|
|
308
|
-
expect(deps.session.
|
|
309
|
-
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
310
|
-
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
211
|
+
expect(deps.session.shutdown).toHaveBeenCalledOnce();
|
|
311
212
|
});
|
|
312
213
|
|
|
313
|
-
it("
|
|
314
|
-
const deps = makeDeps();
|
|
315
|
-
await handleSessionShutdown(deps);
|
|
316
|
-
expect(deps.session.sessionRules.clear).toHaveBeenCalledOnce();
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
it("stops forwarded permission polling", async () => {
|
|
320
|
-
const deps = makeDeps();
|
|
321
|
-
await handleSessionShutdown(deps);
|
|
322
|
-
expect(deps.stopForwardedPermissionPolling).toHaveBeenCalledOnce();
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("calls stopPermissionRpcHandlers on shutdown", async () => {
|
|
214
|
+
it("calls stopPermissionRpcHandlers", async () => {
|
|
326
215
|
const deps = makeDeps();
|
|
327
216
|
await handleSessionShutdown(deps);
|
|
328
217
|
expect(deps.stopPermissionRpcHandlers).toHaveBeenCalledOnce();
|
|
329
218
|
});
|
|
330
|
-
|
|
331
|
-
it("does not reset lastKnownActiveAgentName", async () => {
|
|
332
|
-
const deps = makeDeps({
|
|
333
|
-
session: makeSession({ lastKnownActiveAgentName: "remembered" }),
|
|
334
|
-
});
|
|
335
|
-
await handleSessionShutdown(deps);
|
|
336
|
-
expect(deps.session.lastKnownActiveAgentName).toBe("remembered");
|
|
337
|
-
});
|
|
338
219
|
});
|
|
@@ -9,8 +9,8 @@ import { handleToolCall } from "../../src/handlers/tool-call";
|
|
|
9
9
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
10
10
|
import type { PermissionDecisionEvent } from "../../src/permission-events";
|
|
11
11
|
import { PERMISSIONS_DECISION_CHANNEL } from "../../src/permission-events";
|
|
12
|
-
import type {
|
|
13
|
-
import type { PermissionCheckResult } from "../../src/types";
|
|
12
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
13
|
+
import type { PermissionCheckResult, PermissionState } from "../../src/types";
|
|
14
14
|
|
|
15
15
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
16
16
|
|
|
@@ -67,43 +67,35 @@ function makeCheckResult(
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function makeSession(
|
|
70
|
+
function makeSession(
|
|
71
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
72
|
+
): PermissionSession {
|
|
71
73
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
} as unknown as SessionState["sessionRules"],
|
|
74
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
75
|
+
activate: vi.fn(),
|
|
76
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
77
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
78
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
79
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
80
|
+
approveSessionRule: vi.fn(),
|
|
81
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
82
|
+
getInfrastructureDirs: vi
|
|
83
|
+
.fn()
|
|
84
|
+
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
85
|
+
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
85
86
|
...overrides,
|
|
86
|
-
};
|
|
87
|
+
} as unknown as PermissionSession;
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
90
91
|
return {
|
|
91
92
|
session: makeSession(),
|
|
92
|
-
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
93
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
94
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
95
93
|
events: makeEvents(),
|
|
96
|
-
createPermissionManagerForCwd: vi.fn(),
|
|
97
|
-
refreshExtensionConfig: vi.fn(),
|
|
98
|
-
logResolvedConfigPaths: vi.fn(),
|
|
99
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
100
94
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
101
95
|
promptPermission: vi
|
|
102
96
|
.fn()
|
|
103
97
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
104
98
|
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
105
|
-
startForwardedPermissionPolling: vi.fn(),
|
|
106
|
-
stopForwardedPermissionPolling: vi.fn(),
|
|
107
99
|
stopPermissionRpcHandlers: vi.fn(),
|
|
108
100
|
getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
109
101
|
setActiveTools: vi.fn(),
|
|
@@ -123,18 +115,15 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
|
|
|
123
115
|
|
|
124
116
|
describe("handleToolCall decision events — policy_allow", () => {
|
|
125
117
|
it("emits allow with policy_allow when checkPermission returns allow", async () => {
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}),
|
|
134
|
-
),
|
|
135
|
-
} as unknown as SessionState["permissionManager"],
|
|
136
|
-
}),
|
|
118
|
+
const session = makeSession({
|
|
119
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
120
|
+
makeCheckResult("allow", {
|
|
121
|
+
origin: "global",
|
|
122
|
+
matchedPattern: "*",
|
|
123
|
+
}),
|
|
124
|
+
),
|
|
137
125
|
});
|
|
126
|
+
const deps = makeDeps({ session });
|
|
138
127
|
|
|
139
128
|
await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
|
|
140
129
|
|
|
@@ -154,18 +143,15 @@ describe("handleToolCall decision events — policy_allow", () => {
|
|
|
154
143
|
|
|
155
144
|
describe("handleToolCall decision events — policy_deny", () => {
|
|
156
145
|
it("emits deny with policy_deny when checkPermission returns deny", async () => {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}),
|
|
165
|
-
),
|
|
166
|
-
} as unknown as SessionState["permissionManager"],
|
|
167
|
-
}),
|
|
146
|
+
const session = makeSession({
|
|
147
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
148
|
+
makeCheckResult("deny", {
|
|
149
|
+
origin: "project",
|
|
150
|
+
matchedPattern: "read",
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
168
153
|
});
|
|
154
|
+
const deps = makeDeps({ session });
|
|
169
155
|
|
|
170
156
|
await handleToolCall(deps, makeToolCallEvent("read"), makeCtx());
|
|
171
157
|
|
|
@@ -183,18 +169,15 @@ describe("handleToolCall decision events — policy_deny", () => {
|
|
|
183
169
|
|
|
184
170
|
describe("handleToolCall decision events — session_approved", () => {
|
|
185
171
|
it("emits allow with session_approved when checkPermission returns source:session", async () => {
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
}),
|
|
194
|
-
),
|
|
195
|
-
} as unknown as SessionState["permissionManager"],
|
|
196
|
-
}),
|
|
172
|
+
const session = makeSession({
|
|
173
|
+
checkPermission: vi.fn().mockReturnValue(
|
|
174
|
+
makeCheckResult("allow", {
|
|
175
|
+
source: "session",
|
|
176
|
+
matchedPattern: "git *",
|
|
177
|
+
}),
|
|
178
|
+
),
|
|
197
179
|
});
|
|
180
|
+
const deps = makeDeps({ session });
|
|
198
181
|
|
|
199
182
|
await handleToolCall(
|
|
200
183
|
deps,
|
|
@@ -216,12 +199,11 @@ describe("handleToolCall decision events — session_approved", () => {
|
|
|
216
199
|
|
|
217
200
|
describe("handleToolCall decision events — user_approved", () => {
|
|
218
201
|
it("emits allow with user_approved when state=ask and user approves once", async () => {
|
|
202
|
+
const session = makeSession({
|
|
203
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
204
|
+
});
|
|
219
205
|
const deps = makeDeps({
|
|
220
|
-
session
|
|
221
|
-
permissionManager: {
|
|
222
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
223
|
-
} as unknown as SessionState["permissionManager"],
|
|
224
|
-
}),
|
|
206
|
+
session,
|
|
225
207
|
promptPermission: vi
|
|
226
208
|
.fn()
|
|
227
209
|
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
@@ -238,12 +220,11 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
238
220
|
});
|
|
239
221
|
|
|
240
222
|
it("emits allow with user_approved_for_session when user approves for session", async () => {
|
|
223
|
+
const session = makeSession({
|
|
224
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
225
|
+
});
|
|
241
226
|
const deps = makeDeps({
|
|
242
|
-
session
|
|
243
|
-
permissionManager: {
|
|
244
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
245
|
-
} as unknown as SessionState["permissionManager"],
|
|
246
|
-
}),
|
|
227
|
+
session,
|
|
247
228
|
promptPermission: vi
|
|
248
229
|
.fn()
|
|
249
230
|
.mockResolvedValue({ approved: true, state: "approved_for_session" }),
|
|
@@ -264,12 +245,11 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
264
245
|
|
|
265
246
|
describe("handleToolCall decision events — user_denied", () => {
|
|
266
247
|
it("emits deny with user_denied when state=ask and user denies", async () => {
|
|
248
|
+
const session = makeSession({
|
|
249
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
250
|
+
});
|
|
267
251
|
const deps = makeDeps({
|
|
268
|
-
session
|
|
269
|
-
permissionManager: {
|
|
270
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
271
|
-
} as unknown as SessionState["permissionManager"],
|
|
272
|
-
}),
|
|
252
|
+
session,
|
|
273
253
|
promptPermission: vi
|
|
274
254
|
.fn()
|
|
275
255
|
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
@@ -290,12 +270,11 @@ describe("handleToolCall decision events — user_denied", () => {
|
|
|
290
270
|
|
|
291
271
|
describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
292
272
|
it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
|
|
273
|
+
const session = makeSession({
|
|
274
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
275
|
+
});
|
|
293
276
|
const deps = makeDeps({
|
|
294
|
-
session
|
|
295
|
-
permissionManager: {
|
|
296
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
297
|
-
} as unknown as SessionState["permissionManager"],
|
|
298
|
-
}),
|
|
277
|
+
session,
|
|
299
278
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
300
279
|
});
|
|
301
280
|
|
|
@@ -319,14 +298,11 @@ describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
|
319
298
|
describe("handleToolCall decision events — infrastructure_auto_allowed", () => {
|
|
320
299
|
it("emits allow with infrastructure_auto_allowed for Pi infra reads", async () => {
|
|
321
300
|
const infraDir = "/test/agent";
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
permissionManager: {
|
|
326
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
327
|
-
} as unknown as SessionState["permissionManager"],
|
|
328
|
-
}),
|
|
301
|
+
const session = makeSession({
|
|
302
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
303
|
+
getInfrastructureDirs: vi.fn().mockReturnValue([infraDir]),
|
|
329
304
|
});
|
|
305
|
+
const deps = makeDeps({ session });
|
|
330
306
|
|
|
331
307
|
const event = makeToolCallEvent("read", {
|
|
332
308
|
input: { path: `${infraDir}/some-file.json` },
|
|
@@ -334,7 +310,6 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
|
|
|
334
310
|
await handleToolCall(deps, event, makeCtx());
|
|
335
311
|
|
|
336
312
|
const events = getDecisionEvents(deps);
|
|
337
|
-
// One infrastructure_auto_allowed event + one policy_allow for the normal gate
|
|
338
313
|
const infraEvents = events.filter(
|
|
339
314
|
(e) => e.resolution === "infrastructure_auto_allowed",
|
|
340
315
|
);
|
|
@@ -350,13 +325,11 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
|
|
|
350
325
|
|
|
351
326
|
describe("handleToolCall decision events — auto_approved", () => {
|
|
352
327
|
it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
|
|
328
|
+
const session = makeSession({
|
|
329
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
330
|
+
});
|
|
353
331
|
const deps = makeDeps({
|
|
354
|
-
session
|
|
355
|
-
permissionManager: {
|
|
356
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
357
|
-
} as unknown as SessionState["permissionManager"],
|
|
358
|
-
}),
|
|
359
|
-
// Simulate what PermissionPrompter returns in yolo mode
|
|
332
|
+
session,
|
|
360
333
|
promptPermission: vi.fn().mockResolvedValue({
|
|
361
334
|
approved: true,
|
|
362
335
|
state: "approved",
|