@gotgenes/pi-permission-system 5.5.0 → 5.6.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 +23 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/gates/bash-external-directory.ts +70 -67
- package/src/handlers/gates/descriptor.ts +115 -0
- package/src/handlers/gates/external-directory.ts +62 -130
- package/src/handlers/gates/index.ts +12 -4
- package/src/handlers/gates/runner.ts +144 -0
- package/src/handlers/gates/skill-read.ts +40 -59
- package/src/handlers/gates/tool.ts +35 -104
- package/src/handlers/gates/types.ts +0 -2
- package/src/handlers/input.ts +3 -3
- package/src/handlers/lifecycle.ts +21 -21
- package/src/handlers/tool-call.ts +121 -20
- package/src/handlers/types.ts +20 -7
- package/src/index.ts +6 -1
- package/src/runtime.ts +17 -9
- package/tests/handlers/before-agent-start.test.ts +17 -27
- package/tests/handlers/gates/bash-external-directory.test.ts +129 -184
- package/tests/handlers/gates/external-directory.test.ts +118 -264
- package/tests/handlers/gates/runner.test.ts +361 -0
- package/tests/handlers/gates/skill-read.test.ts +86 -137
- package/tests/handlers/gates/tool.test.ts +109 -346
- package/tests/handlers/input-events.test.ts +10 -21
- package/tests/handlers/input.test.ts +26 -43
- package/tests/handlers/lifecycle.test.ts +47 -66
- package/tests/handlers/tool-call-events.test.ts +29 -40
- package/tests/handlers/tool-call.test.ts +19 -30
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
handleInput,
|
|
7
7
|
} from "../../src/handlers/input";
|
|
8
8
|
import type { HandlerDeps } from "../../src/handlers/types";
|
|
9
|
-
import type {
|
|
9
|
+
import type { SessionState } from "../../src/runtime";
|
|
10
10
|
import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
|
|
11
11
|
|
|
12
12
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -34,42 +34,32 @@ function makeInputEvent(text: string) {
|
|
|
34
34
|
return { text };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function
|
|
38
|
-
overrides: Partial<ExtensionRuntime> = {},
|
|
39
|
-
): ExtensionRuntime {
|
|
37
|
+
function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
40
38
|
return {
|
|
41
|
-
agentDir: "/test/agent",
|
|
42
|
-
sessionsDir: "/test/agent/sessions",
|
|
43
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
44
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
45
|
-
globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
|
|
46
|
-
config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
|
|
47
39
|
runtimeContext: null,
|
|
48
40
|
permissionManager: {
|
|
49
41
|
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
50
|
-
} as unknown as
|
|
42
|
+
} as unknown as SessionState["permissionManager"],
|
|
51
43
|
activeSkillEntries: [] as SkillPromptEntry[],
|
|
52
44
|
lastKnownActiveAgentName: null,
|
|
53
45
|
lastActiveToolsCacheKey: null,
|
|
54
46
|
lastPromptStateCacheKey: null,
|
|
55
|
-
lastConfigWarning: null,
|
|
56
47
|
sessionRules: {
|
|
57
48
|
approve: vi.fn(),
|
|
58
49
|
getRuleset: vi.fn().mockReturnValue([]),
|
|
59
50
|
clear: vi.fn(),
|
|
60
|
-
} as unknown as
|
|
61
|
-
permissionForwardingContext: null,
|
|
62
|
-
permissionForwardingTimer: null,
|
|
63
|
-
isProcessingForwardedRequests: false,
|
|
64
|
-
writeDebugLog: vi.fn(),
|
|
65
|
-
writeReviewLog: vi.fn(),
|
|
51
|
+
} as unknown as SessionState["sessionRules"],
|
|
66
52
|
...overrides,
|
|
67
|
-
}
|
|
53
|
+
};
|
|
68
54
|
}
|
|
69
55
|
|
|
70
56
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
71
57
|
return {
|
|
72
|
-
|
|
58
|
+
session: makeSession(),
|
|
59
|
+
writeDebugLog: vi.fn(),
|
|
60
|
+
writeReviewLog: vi.fn(),
|
|
61
|
+
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
62
|
+
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
73
63
|
createPermissionManagerForCwd: vi.fn(),
|
|
74
64
|
refreshExtensionConfig: vi.fn(),
|
|
75
65
|
notifyWarning: vi.fn(),
|
|
@@ -131,7 +121,7 @@ describe("handleInput", () => {
|
|
|
131
121
|
const ctx = makeCtx();
|
|
132
122
|
const deps = makeDeps();
|
|
133
123
|
await handleInput(deps, makeInputEvent("hello"), ctx);
|
|
134
|
-
expect(deps.
|
|
124
|
+
expect(deps.session.runtimeContext).toBe(ctx);
|
|
135
125
|
});
|
|
136
126
|
|
|
137
127
|
it("starts forwarded permission polling", async () => {
|
|
@@ -155,7 +145,7 @@ describe("handleInput", () => {
|
|
|
155
145
|
const deps = makeDeps();
|
|
156
146
|
await handleInput(deps, makeInputEvent("just a message"), makeCtx());
|
|
157
147
|
expect(
|
|
158
|
-
deps.
|
|
148
|
+
deps.session.permissionManager.checkPermission,
|
|
159
149
|
).not.toHaveBeenCalled();
|
|
160
150
|
});
|
|
161
151
|
|
|
@@ -175,9 +165,8 @@ describe("handleInput", () => {
|
|
|
175
165
|
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
176
166
|
};
|
|
177
167
|
const deps = makeDeps({
|
|
178
|
-
|
|
179
|
-
permissionManager:
|
|
180
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
168
|
+
session: makeSession({
|
|
169
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
181
170
|
}),
|
|
182
171
|
});
|
|
183
172
|
const result = await handleInput(
|
|
@@ -194,9 +183,8 @@ describe("handleInput", () => {
|
|
|
194
183
|
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
195
184
|
};
|
|
196
185
|
const deps = makeDeps({
|
|
197
|
-
|
|
198
|
-
permissionManager:
|
|
199
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
186
|
+
session: makeSession({
|
|
187
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
200
188
|
}),
|
|
201
189
|
});
|
|
202
190
|
await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
|
|
@@ -210,9 +198,8 @@ describe("handleInput", () => {
|
|
|
210
198
|
const ctx = makeCtx({ hasUI: false });
|
|
211
199
|
const pm = { checkPermission: vi.fn().mockReturnValue({ state: "deny" }) };
|
|
212
200
|
const deps = makeDeps({
|
|
213
|
-
|
|
214
|
-
permissionManager:
|
|
215
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
201
|
+
session: makeSession({
|
|
202
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
216
203
|
}),
|
|
217
204
|
});
|
|
218
205
|
await handleInput(deps, makeInputEvent("/skill:librarian"), ctx);
|
|
@@ -222,9 +209,8 @@ describe("handleInput", () => {
|
|
|
222
209
|
it("returns handled when skill requires approval but no UI is available", async () => {
|
|
223
210
|
const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
|
|
224
211
|
const deps = makeDeps({
|
|
225
|
-
|
|
226
|
-
permissionManager:
|
|
227
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
212
|
+
session: makeSession({
|
|
213
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
228
214
|
}),
|
|
229
215
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
230
216
|
});
|
|
@@ -239,9 +225,8 @@ describe("handleInput", () => {
|
|
|
239
225
|
it("prompts and returns continue when skill ask is approved", async () => {
|
|
240
226
|
const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
|
|
241
227
|
const deps = makeDeps({
|
|
242
|
-
|
|
243
|
-
permissionManager:
|
|
244
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
228
|
+
session: makeSession({
|
|
229
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
245
230
|
}),
|
|
246
231
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
247
232
|
promptPermission: vi
|
|
@@ -260,9 +245,8 @@ describe("handleInput", () => {
|
|
|
260
245
|
it("returns handled when skill ask is denied by user", async () => {
|
|
261
246
|
const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
|
|
262
247
|
const deps = makeDeps({
|
|
263
|
-
|
|
264
|
-
permissionManager:
|
|
265
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
248
|
+
session: makeSession({
|
|
249
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
266
250
|
}),
|
|
267
251
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
268
252
|
promptPermission: vi
|
|
@@ -280,9 +264,8 @@ describe("handleInput", () => {
|
|
|
280
264
|
it("passes agentName in the prompt permission request", async () => {
|
|
281
265
|
const pm = { checkPermission: vi.fn().mockReturnValue({ state: "ask" }) };
|
|
282
266
|
const deps = makeDeps({
|
|
283
|
-
|
|
284
|
-
permissionManager:
|
|
285
|
-
pm as unknown as ExtensionRuntime["permissionManager"],
|
|
267
|
+
session: makeSession({
|
|
268
|
+
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
286
269
|
}),
|
|
287
270
|
resolveAgentName: vi.fn().mockReturnValue("code-agent"),
|
|
288
271
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
@@ -7,7 +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 {
|
|
10
|
+
import type { SessionState } from "../../src/runtime";
|
|
11
11
|
import type { SessionRules } from "../../src/session-rules";
|
|
12
12
|
import type { SkillPromptEntry } from "../../src/skill-prompt-sanitizer";
|
|
13
13
|
|
|
@@ -67,36 +67,26 @@ function makeSessionRules(): SessionRules {
|
|
|
67
67
|
} as unknown as SessionRules;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function
|
|
71
|
-
overrides: Partial<ExtensionRuntime> = {},
|
|
72
|
-
): ExtensionRuntime {
|
|
70
|
+
function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
73
71
|
return {
|
|
74
|
-
agentDir: "/test/agent",
|
|
75
|
-
sessionsDir: "/test/agent/sessions",
|
|
76
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
77
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
78
|
-
globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
|
|
79
|
-
config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
|
|
80
72
|
runtimeContext: null,
|
|
81
73
|
permissionManager: makePermissionManager() as unknown as PermissionManager,
|
|
82
74
|
activeSkillEntries: [] as SkillPromptEntry[],
|
|
83
75
|
lastKnownActiveAgentName: null,
|
|
84
76
|
lastActiveToolsCacheKey: null,
|
|
85
77
|
lastPromptStateCacheKey: null,
|
|
86
|
-
lastConfigWarning: null,
|
|
87
78
|
sessionRules: makeSessionRules(),
|
|
88
|
-
permissionForwardingContext: null,
|
|
89
|
-
permissionForwardingTimer: null,
|
|
90
|
-
isProcessingForwardedRequests: false,
|
|
91
|
-
writeDebugLog: vi.fn(),
|
|
92
|
-
writeReviewLog: vi.fn(),
|
|
93
79
|
...overrides,
|
|
94
|
-
}
|
|
80
|
+
};
|
|
95
81
|
}
|
|
96
82
|
|
|
97
83
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
98
84
|
return {
|
|
99
|
-
|
|
85
|
+
session: makeSession(),
|
|
86
|
+
writeDebugLog: vi.fn(),
|
|
87
|
+
writeReviewLog: vi.fn(),
|
|
88
|
+
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
89
|
+
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
100
90
|
createPermissionManagerForCwd: vi
|
|
101
91
|
.fn()
|
|
102
92
|
.mockReturnValue(makePermissionManager()),
|
|
@@ -131,7 +121,7 @@ describe("handleSessionStart", () => {
|
|
|
131
121
|
const ctx = makeCtx();
|
|
132
122
|
const deps = makeDeps();
|
|
133
123
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
134
|
-
expect(deps.
|
|
124
|
+
expect(deps.session.runtimeContext).toBe(ctx);
|
|
135
125
|
});
|
|
136
126
|
|
|
137
127
|
it("refreshes extension config with ctx", async () => {
|
|
@@ -151,16 +141,16 @@ describe("handleSessionStart", () => {
|
|
|
151
141
|
expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
|
|
152
142
|
"/my/project",
|
|
153
143
|
);
|
|
154
|
-
expect(deps.
|
|
144
|
+
expect(deps.session.permissionManager).toBe(newPm);
|
|
155
145
|
});
|
|
156
146
|
|
|
157
147
|
it("clears the before_agent_start cache", async () => {
|
|
158
148
|
const ctx = makeCtx();
|
|
159
149
|
const deps = makeDeps();
|
|
160
150
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
161
|
-
expect(deps.
|
|
162
|
-
expect(deps.
|
|
163
|
-
expect(deps.
|
|
151
|
+
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
152
|
+
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
153
|
+
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
164
154
|
});
|
|
165
155
|
|
|
166
156
|
it("sets lastKnownActiveAgentName from getActiveAgentName", async () => {
|
|
@@ -168,7 +158,7 @@ describe("handleSessionStart", () => {
|
|
|
168
158
|
const ctx = makeCtx();
|
|
169
159
|
const deps = makeDeps();
|
|
170
160
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
171
|
-
expect(deps.
|
|
161
|
+
expect(deps.session.lastKnownActiveAgentName).toBe("my-agent");
|
|
172
162
|
});
|
|
173
163
|
|
|
174
164
|
it("sets lastKnownActiveAgentName to null when no agent is active", async () => {
|
|
@@ -176,7 +166,7 @@ describe("handleSessionStart", () => {
|
|
|
176
166
|
const ctx = makeCtx();
|
|
177
167
|
const deps = makeDeps();
|
|
178
168
|
await handleSessionStart(deps, { reason: "startup" }, ctx);
|
|
179
|
-
expect(deps.
|
|
169
|
+
expect(deps.session.lastKnownActiveAgentName).toBeNull();
|
|
180
170
|
});
|
|
181
171
|
|
|
182
172
|
it("starts forwarded permission polling", async () => {
|
|
@@ -213,20 +203,17 @@ describe("handleSessionStart", () => {
|
|
|
213
203
|
const ctx = makeCtx({ cwd: "/proj" });
|
|
214
204
|
const deps = makeDeps();
|
|
215
205
|
await handleSessionStart(deps, { reason: "reload" }, ctx);
|
|
216
|
-
expect(deps.
|
|
217
|
-
"
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
cwd: "/proj",
|
|
222
|
-
},
|
|
223
|
-
);
|
|
206
|
+
expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
|
|
207
|
+
triggeredBy: "session_start",
|
|
208
|
+
reason: "reload",
|
|
209
|
+
cwd: "/proj",
|
|
210
|
+
});
|
|
224
211
|
});
|
|
225
212
|
|
|
226
213
|
it("does not write lifecycle.reload debug log for non-reload reasons", async () => {
|
|
227
214
|
const deps = makeDeps();
|
|
228
215
|
await handleSessionStart(deps, { reason: "startup" }, makeCtx());
|
|
229
|
-
expect(deps.
|
|
216
|
+
expect(deps.writeDebugLog).not.toHaveBeenCalled();
|
|
230
217
|
});
|
|
231
218
|
});
|
|
232
219
|
|
|
@@ -237,21 +224,21 @@ describe("handleResourcesDiscover", () => {
|
|
|
237
224
|
const deps = makeDeps();
|
|
238
225
|
await handleResourcesDiscover(deps, { reason: "startup" });
|
|
239
226
|
expect(deps.createPermissionManagerForCwd).not.toHaveBeenCalled();
|
|
240
|
-
expect(deps.
|
|
227
|
+
expect(deps.writeDebugLog).not.toHaveBeenCalled();
|
|
241
228
|
});
|
|
242
229
|
|
|
243
230
|
it("creates and stores a new PM using runtimeContext.cwd on reload", async () => {
|
|
244
231
|
const ctx = makeCtx({ cwd: "/runtime/cwd" });
|
|
245
232
|
const newPm = makePermissionManager();
|
|
246
233
|
const deps = makeDeps({
|
|
247
|
-
|
|
234
|
+
session: makeSession({ runtimeContext: ctx }),
|
|
248
235
|
createPermissionManagerForCwd: vi.fn().mockReturnValue(newPm),
|
|
249
236
|
});
|
|
250
237
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
251
238
|
expect(deps.createPermissionManagerForCwd).toHaveBeenCalledWith(
|
|
252
239
|
"/runtime/cwd",
|
|
253
240
|
);
|
|
254
|
-
expect(deps.
|
|
241
|
+
expect(deps.session.permissionManager).toBe(newPm);
|
|
255
242
|
});
|
|
256
243
|
|
|
257
244
|
it("uses undefined cwd when runtimeContext is null on reload", async () => {
|
|
@@ -263,36 +250,30 @@ describe("handleResourcesDiscover", () => {
|
|
|
263
250
|
it("clears the before_agent_start cache on reload", async () => {
|
|
264
251
|
const deps = makeDeps();
|
|
265
252
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
266
|
-
expect(deps.
|
|
267
|
-
expect(deps.
|
|
268
|
-
expect(deps.
|
|
253
|
+
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
254
|
+
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
255
|
+
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
269
256
|
});
|
|
270
257
|
|
|
271
258
|
it("writes lifecycle.reload debug log on reload", async () => {
|
|
272
259
|
const ctx = makeCtx({ cwd: "/proj" });
|
|
273
|
-
const deps = makeDeps({
|
|
260
|
+
const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
|
|
274
261
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
275
|
-
expect(deps.
|
|
276
|
-
"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
cwd: "/proj",
|
|
281
|
-
},
|
|
282
|
-
);
|
|
262
|
+
expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
|
|
263
|
+
triggeredBy: "resources_discover",
|
|
264
|
+
reason: "reload",
|
|
265
|
+
cwd: "/proj",
|
|
266
|
+
});
|
|
283
267
|
});
|
|
284
268
|
|
|
285
269
|
it("logs cwd as null when runtimeContext is null on reload", async () => {
|
|
286
270
|
const deps = makeDeps();
|
|
287
271
|
await handleResourcesDiscover(deps, { reason: "reload" });
|
|
288
|
-
expect(deps.
|
|
289
|
-
"
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
cwd: null,
|
|
294
|
-
},
|
|
295
|
-
);
|
|
272
|
+
expect(deps.writeDebugLog).toHaveBeenCalledWith("lifecycle.reload", {
|
|
273
|
+
triggeredBy: "resources_discover",
|
|
274
|
+
reason: "reload",
|
|
275
|
+
cwd: null,
|
|
276
|
+
});
|
|
296
277
|
});
|
|
297
278
|
});
|
|
298
279
|
|
|
@@ -302,7 +283,7 @@ describe("handleSessionShutdown", () => {
|
|
|
302
283
|
it("clears the UI status when a runtime context is present", async () => {
|
|
303
284
|
const ctx = makeCtx();
|
|
304
285
|
const deps = makeDeps({
|
|
305
|
-
|
|
286
|
+
session: makeSession({ runtimeContext: ctx }),
|
|
306
287
|
});
|
|
307
288
|
await handleSessionShutdown(deps);
|
|
308
289
|
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
@@ -318,23 +299,23 @@ describe("handleSessionShutdown", () => {
|
|
|
318
299
|
|
|
319
300
|
it("sets runtime context to null", async () => {
|
|
320
301
|
const ctx = makeCtx();
|
|
321
|
-
const deps = makeDeps({
|
|
302
|
+
const deps = makeDeps({ session: makeSession({ runtimeContext: ctx }) });
|
|
322
303
|
await handleSessionShutdown(deps);
|
|
323
|
-
expect(deps.
|
|
304
|
+
expect(deps.session.runtimeContext).toBeNull();
|
|
324
305
|
});
|
|
325
306
|
|
|
326
307
|
it("clears the before_agent_start cache", async () => {
|
|
327
308
|
const deps = makeDeps();
|
|
328
309
|
await handleSessionShutdown(deps);
|
|
329
|
-
expect(deps.
|
|
330
|
-
expect(deps.
|
|
331
|
-
expect(deps.
|
|
310
|
+
expect(deps.session.activeSkillEntries).toEqual([]);
|
|
311
|
+
expect(deps.session.lastActiveToolsCacheKey).toBeNull();
|
|
312
|
+
expect(deps.session.lastPromptStateCacheKey).toBeNull();
|
|
332
313
|
});
|
|
333
314
|
|
|
334
315
|
it("clears the session rules", async () => {
|
|
335
316
|
const deps = makeDeps();
|
|
336
317
|
await handleSessionShutdown(deps);
|
|
337
|
-
expect(deps.
|
|
318
|
+
expect(deps.session.sessionRules.clear).toHaveBeenCalledOnce();
|
|
338
319
|
});
|
|
339
320
|
|
|
340
321
|
it("stops forwarded permission polling", async () => {
|
|
@@ -351,9 +332,9 @@ describe("handleSessionShutdown", () => {
|
|
|
351
332
|
|
|
352
333
|
it("does not reset lastKnownActiveAgentName", async () => {
|
|
353
334
|
const deps = makeDeps({
|
|
354
|
-
|
|
335
|
+
session: makeSession({ lastKnownActiveAgentName: "remembered" }),
|
|
355
336
|
});
|
|
356
337
|
await handleSessionShutdown(deps);
|
|
357
|
-
expect(deps.
|
|
338
|
+
expect(deps.session.lastKnownActiveAgentName).toBe("remembered");
|
|
358
339
|
});
|
|
359
340
|
});
|
|
@@ -9,7 +9,7 @@ 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 {
|
|
12
|
+
import type { SessionState } from "../../src/runtime";
|
|
13
13
|
import type { PermissionCheckResult } from "../../src/types";
|
|
14
14
|
|
|
15
15
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -67,43 +67,32 @@ function makeCheckResult(
|
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
function
|
|
71
|
-
overrides: Partial<ExtensionRuntime> = {},
|
|
72
|
-
): ExtensionRuntime {
|
|
70
|
+
function makeSession(overrides: Partial<SessionState> = {}): SessionState {
|
|
73
71
|
return {
|
|
74
|
-
agentDir: "/test/agent",
|
|
75
|
-
sessionsDir: "/test/agent/sessions",
|
|
76
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
77
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
78
|
-
globalLogsDir: "/test/agent/extensions/pi-permission-system/logs",
|
|
79
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
80
|
-
config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
|
|
81
72
|
runtimeContext: null,
|
|
82
73
|
permissionManager: {
|
|
83
74
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
84
|
-
} as unknown as
|
|
75
|
+
} as unknown as SessionState["permissionManager"],
|
|
85
76
|
activeSkillEntries: [],
|
|
86
77
|
lastKnownActiveAgentName: null,
|
|
87
78
|
lastActiveToolsCacheKey: null,
|
|
88
79
|
lastPromptStateCacheKey: null,
|
|
89
|
-
lastConfigWarning: null,
|
|
90
80
|
sessionRules: {
|
|
91
81
|
approve: vi.fn(),
|
|
92
82
|
getRuleset: vi.fn().mockReturnValue([]),
|
|
93
83
|
clear: vi.fn(),
|
|
94
|
-
} as unknown as
|
|
95
|
-
permissionForwardingContext: null,
|
|
96
|
-
permissionForwardingTimer: null,
|
|
97
|
-
isProcessingForwardedRequests: false,
|
|
98
|
-
writeDebugLog: vi.fn(),
|
|
99
|
-
writeReviewLog: vi.fn(),
|
|
84
|
+
} as unknown as SessionState["sessionRules"],
|
|
100
85
|
...overrides,
|
|
101
|
-
}
|
|
86
|
+
};
|
|
102
87
|
}
|
|
103
88
|
|
|
104
89
|
function makeDeps(overrides: Partial<HandlerDeps> = {}): HandlerDeps {
|
|
105
90
|
return {
|
|
106
|
-
|
|
91
|
+
session: makeSession(),
|
|
92
|
+
writeDebugLog: vi.fn(),
|
|
93
|
+
writeReviewLog: vi.fn(),
|
|
94
|
+
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
95
|
+
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
107
96
|
events: makeEvents(),
|
|
108
97
|
createPermissionManagerForCwd: vi.fn(),
|
|
109
98
|
refreshExtensionConfig: vi.fn(),
|
|
@@ -137,7 +126,7 @@ function getDecisionEvents(deps: HandlerDeps): PermissionDecisionEvent[] {
|
|
|
137
126
|
describe("handleToolCall decision events — policy_allow", () => {
|
|
138
127
|
it("emits allow with policy_allow when checkPermission returns allow", async () => {
|
|
139
128
|
const deps = makeDeps({
|
|
140
|
-
|
|
129
|
+
session: makeSession({
|
|
141
130
|
permissionManager: {
|
|
142
131
|
checkPermission: vi.fn().mockReturnValue(
|
|
143
132
|
makeCheckResult("allow", {
|
|
@@ -145,7 +134,7 @@ describe("handleToolCall decision events — policy_allow", () => {
|
|
|
145
134
|
matchedPattern: "*",
|
|
146
135
|
}),
|
|
147
136
|
),
|
|
148
|
-
} as unknown as
|
|
137
|
+
} as unknown as SessionState["permissionManager"],
|
|
149
138
|
}),
|
|
150
139
|
});
|
|
151
140
|
|
|
@@ -168,7 +157,7 @@ describe("handleToolCall decision events — policy_allow", () => {
|
|
|
168
157
|
describe("handleToolCall decision events — policy_deny", () => {
|
|
169
158
|
it("emits deny with policy_deny when checkPermission returns deny", async () => {
|
|
170
159
|
const deps = makeDeps({
|
|
171
|
-
|
|
160
|
+
session: makeSession({
|
|
172
161
|
permissionManager: {
|
|
173
162
|
checkPermission: vi.fn().mockReturnValue(
|
|
174
163
|
makeCheckResult("deny", {
|
|
@@ -176,7 +165,7 @@ describe("handleToolCall decision events — policy_deny", () => {
|
|
|
176
165
|
matchedPattern: "read",
|
|
177
166
|
}),
|
|
178
167
|
),
|
|
179
|
-
} as unknown as
|
|
168
|
+
} as unknown as SessionState["permissionManager"],
|
|
180
169
|
}),
|
|
181
170
|
});
|
|
182
171
|
|
|
@@ -197,7 +186,7 @@ describe("handleToolCall decision events — policy_deny", () => {
|
|
|
197
186
|
describe("handleToolCall decision events — session_approved", () => {
|
|
198
187
|
it("emits allow with session_approved when checkPermission returns source:session", async () => {
|
|
199
188
|
const deps = makeDeps({
|
|
200
|
-
|
|
189
|
+
session: makeSession({
|
|
201
190
|
permissionManager: {
|
|
202
191
|
checkPermission: vi.fn().mockReturnValue(
|
|
203
192
|
makeCheckResult("allow", {
|
|
@@ -205,7 +194,7 @@ describe("handleToolCall decision events — session_approved", () => {
|
|
|
205
194
|
matchedPattern: "git *",
|
|
206
195
|
}),
|
|
207
196
|
),
|
|
208
|
-
} as unknown as
|
|
197
|
+
} as unknown as SessionState["permissionManager"],
|
|
209
198
|
}),
|
|
210
199
|
});
|
|
211
200
|
|
|
@@ -230,10 +219,10 @@ describe("handleToolCall decision events — session_approved", () => {
|
|
|
230
219
|
describe("handleToolCall decision events — user_approved", () => {
|
|
231
220
|
it("emits allow with user_approved when state=ask and user approves once", async () => {
|
|
232
221
|
const deps = makeDeps({
|
|
233
|
-
|
|
222
|
+
session: makeSession({
|
|
234
223
|
permissionManager: {
|
|
235
224
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
236
|
-
} as unknown as
|
|
225
|
+
} as unknown as SessionState["permissionManager"],
|
|
237
226
|
}),
|
|
238
227
|
promptPermission: vi
|
|
239
228
|
.fn()
|
|
@@ -252,10 +241,10 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
252
241
|
|
|
253
242
|
it("emits allow with user_approved_for_session when user approves for session", async () => {
|
|
254
243
|
const deps = makeDeps({
|
|
255
|
-
|
|
244
|
+
session: makeSession({
|
|
256
245
|
permissionManager: {
|
|
257
246
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
258
|
-
} as unknown as
|
|
247
|
+
} as unknown as SessionState["permissionManager"],
|
|
259
248
|
}),
|
|
260
249
|
promptPermission: vi
|
|
261
250
|
.fn()
|
|
@@ -278,10 +267,10 @@ describe("handleToolCall decision events — user_approved", () => {
|
|
|
278
267
|
describe("handleToolCall decision events — user_denied", () => {
|
|
279
268
|
it("emits deny with user_denied when state=ask and user denies", async () => {
|
|
280
269
|
const deps = makeDeps({
|
|
281
|
-
|
|
270
|
+
session: makeSession({
|
|
282
271
|
permissionManager: {
|
|
283
272
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
284
|
-
} as unknown as
|
|
273
|
+
} as unknown as SessionState["permissionManager"],
|
|
285
274
|
}),
|
|
286
275
|
promptPermission: vi
|
|
287
276
|
.fn()
|
|
@@ -304,10 +293,10 @@ describe("handleToolCall decision events — user_denied", () => {
|
|
|
304
293
|
describe("handleToolCall decision events — confirmation_unavailable", () => {
|
|
305
294
|
it("emits deny with confirmation_unavailable when state=ask but no UI", async () => {
|
|
306
295
|
const deps = makeDeps({
|
|
307
|
-
|
|
296
|
+
session: makeSession({
|
|
308
297
|
permissionManager: {
|
|
309
298
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
310
|
-
} as unknown as
|
|
299
|
+
} as unknown as SessionState["permissionManager"],
|
|
311
300
|
}),
|
|
312
301
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
313
302
|
});
|
|
@@ -333,11 +322,11 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
|
|
|
333
322
|
it("emits allow with infrastructure_auto_allowed for Pi infra reads", async () => {
|
|
334
323
|
const infraDir = "/test/agent";
|
|
335
324
|
const deps = makeDeps({
|
|
336
|
-
|
|
337
|
-
|
|
325
|
+
piInfrastructureDirs: [infraDir],
|
|
326
|
+
session: makeSession({
|
|
338
327
|
permissionManager: {
|
|
339
328
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
340
|
-
} as unknown as
|
|
329
|
+
} as unknown as SessionState["permissionManager"],
|
|
341
330
|
}),
|
|
342
331
|
});
|
|
343
332
|
|
|
@@ -364,10 +353,10 @@ describe("handleToolCall decision events — infrastructure_auto_allowed", () =>
|
|
|
364
353
|
describe("handleToolCall decision events — auto_approved", () => {
|
|
365
354
|
it("emits allow with auto_approved when promptPermission returns autoApproved:true", async () => {
|
|
366
355
|
const deps = makeDeps({
|
|
367
|
-
|
|
356
|
+
session: makeSession({
|
|
368
357
|
permissionManager: {
|
|
369
358
|
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
370
|
-
} as unknown as
|
|
359
|
+
} as unknown as SessionState["permissionManager"],
|
|
371
360
|
}),
|
|
372
361
|
// Simulate what PermissionPrompter returns in yolo mode
|
|
373
362
|
promptPermission: vi.fn().mockResolvedValue({
|