@gotgenes/pi-permission-system 10.0.0 → 10.2.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 +33 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/agent-prep-session.ts +28 -0
- package/src/decision-reporter.ts +41 -0
- package/src/denial-messages.ts +11 -0
- package/src/forwarded-permissions/permission-forwarder.ts +549 -0
- package/src/forwarding-manager.ts +3 -7
- package/src/gate-handler-session.ts +13 -0
- package/src/gate-prompter.ts +14 -0
- package/src/handlers/before-agent-start.ts +2 -3
- package/src/handlers/gates/bash-command.ts +4 -18
- package/src/handlers/gates/bash-external-directory.ts +3 -15
- package/src/handlers/gates/bash-path.ts +3 -16
- package/src/handlers/gates/descriptor.ts +0 -28
- package/src/handlers/gates/path.ts +3 -15
- package/src/handlers/gates/runner.ts +142 -105
- package/src/handlers/gates/skill-input-gate-pipeline.ts +104 -0
- package/src/handlers/gates/skill-input.ts +44 -0
- package/src/handlers/gates/tool-call-gate-pipeline.ts +120 -0
- package/src/handlers/lifecycle.ts +9 -9
- package/src/handlers/permission-gate-handler.ts +34 -238
- package/src/index.ts +53 -69
- package/src/mcp-targets.ts +56 -46
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +7 -58
- package/src/permission-resolver.ts +17 -0
- package/src/permission-session.ts +83 -27
- package/src/permissions-service.ts +53 -0
- package/src/runtime.ts +1 -37
- package/src/service-lifecycle.ts +49 -0
- package/src/session-approval-recorder.ts +6 -0
- package/src/session-lifecycle-session.ts +24 -0
- package/src/tool-input-preview.ts +0 -62
- package/src/tool-input-prompt-formatters.ts +63 -0
- package/src/tool-preview-formatter.ts +6 -4
- package/test/decision-reporter.test.ts +112 -0
- package/test/denial-messages.test.ts +62 -0
- package/test/forwarding-manager.test.ts +26 -44
- package/test/handlers/before-agent-start.test.ts +45 -21
- package/test/handlers/external-directory-integration.test.ts +83 -114
- package/test/handlers/external-directory-session-dedup.test.ts +102 -55
- package/test/handlers/gates/bash-command.test.ts +49 -90
- package/test/handlers/gates/bash-external-directory.test.ts +54 -95
- package/test/handlers/gates/bash-path.test.ts +54 -157
- package/test/handlers/gates/path.test.ts +38 -105
- package/test/handlers/gates/runner.test.ts +151 -186
- package/test/handlers/gates/skill-input-gate-pipeline.test.ts +176 -0
- package/test/handlers/gates/skill-input.test.ts +128 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +180 -0
- package/test/handlers/input.test.ts +1 -2
- package/test/handlers/lifecycle.test.ts +49 -33
- package/test/handlers/tool-call-events.test.ts +1 -1
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +212 -17
- package/test/helpers/handler-fixtures.ts +226 -29
- package/test/mcp-targets.test.ts +55 -0
- package/test/permission-forwarder.test.ts +295 -0
- package/test/permission-forwarding.test.ts +0 -282
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +33 -44
- package/test/permission-session.test.ts +211 -105
- package/test/permissions-service.test.ts +151 -0
- package/test/runtime.test.ts +2 -86
- package/test/service-lifecycle.test.ts +162 -0
- package/test/tool-input-preview.test.ts +0 -111
- package/test/tool-input-prompt-formatters.test.ts +115 -0
- package/src/forwarded-permissions/polling.ts +0 -411
|
@@ -3,42 +3,32 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
|
|
4
4
|
// ── Module mocks (hoisted) ─────────────────────────────────────────────────
|
|
5
5
|
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
mockGetActiveAgentNameFromSystemPrompt:
|
|
13
|
-
vi.fn<(systemPrompt?: string) => string | null>(),
|
|
14
|
-
mockCreatePermissionManagerForCwd: vi.fn(),
|
|
15
|
-
}));
|
|
6
|
+
const { mockGetActiveAgentName, mockGetActiveAgentNameFromSystemPrompt } =
|
|
7
|
+
vi.hoisted(() => ({
|
|
8
|
+
mockGetActiveAgentName: vi.fn<(ctx: ExtensionContext) => string | null>(),
|
|
9
|
+
mockGetActiveAgentNameFromSystemPrompt:
|
|
10
|
+
vi.fn<(systemPrompt?: string) => string | null>(),
|
|
11
|
+
}));
|
|
16
12
|
|
|
17
13
|
vi.mock("../src/active-agent", () => ({
|
|
18
14
|
getActiveAgentName: mockGetActiveAgentName,
|
|
19
15
|
getActiveAgentNameFromSystemPrompt: mockGetActiveAgentNameFromSystemPrompt,
|
|
20
16
|
}));
|
|
21
17
|
|
|
22
|
-
vi.mock("../src/runtime", async (importOriginal) => {
|
|
23
|
-
const original = await importOriginal<typeof import("../src/runtime")>();
|
|
24
|
-
return {
|
|
25
|
-
...original,
|
|
26
|
-
createPermissionManagerForCwd: mockCreatePermissionManagerForCwd,
|
|
27
|
-
};
|
|
28
|
-
});
|
|
29
|
-
|
|
30
18
|
// ── Test helpers ───────────────────────────────────────────────────────────
|
|
31
19
|
|
|
32
20
|
import type { ExtensionPaths } from "#src/extension-paths";
|
|
33
21
|
import type { ForwardingController } from "#src/forwarding-manager";
|
|
34
|
-
import type {
|
|
22
|
+
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
35
23
|
import {
|
|
36
24
|
PermissionSession,
|
|
37
25
|
type PermissionSessionRuntimeDeps,
|
|
38
26
|
} from "#src/permission-session";
|
|
27
|
+
import type { Ruleset } from "#src/rule";
|
|
39
28
|
import { SessionApproval } from "#src/session-approval";
|
|
40
29
|
import type { SessionLogger } from "#src/session-logger";
|
|
41
30
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
31
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
42
32
|
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
43
33
|
|
|
44
34
|
function makeSkillEntry(
|
|
@@ -95,29 +85,37 @@ function makeForwarding(): ForwardingController {
|
|
|
95
85
|
};
|
|
96
86
|
}
|
|
97
87
|
|
|
98
|
-
function makePermissionManager(
|
|
99
|
-
overrides: Partial<PermissionManager> = {},
|
|
100
|
-
): PermissionManager {
|
|
88
|
+
function makePermissionManager() {
|
|
101
89
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
90
|
+
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
91
|
+
checkPermission: vi
|
|
92
|
+
.fn<
|
|
93
|
+
(
|
|
94
|
+
toolName: string,
|
|
95
|
+
input: unknown,
|
|
96
|
+
agentName?: string,
|
|
97
|
+
sessionRules?: Ruleset,
|
|
98
|
+
) => PermissionCheckResult
|
|
99
|
+
>()
|
|
100
|
+
.mockReturnValue({
|
|
101
|
+
state: "allow",
|
|
102
|
+
toolName: "read",
|
|
103
|
+
source: "tool",
|
|
104
|
+
origin: "builtin",
|
|
105
|
+
}),
|
|
106
|
+
getToolPermission: vi
|
|
107
|
+
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
108
|
+
.mockReturnValue("allow"),
|
|
109
|
+
getConfigIssues: vi.fn((): string[] => []),
|
|
110
|
+
getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
|
|
111
|
+
};
|
|
115
112
|
}
|
|
116
113
|
|
|
117
114
|
function createSession(overrides?: {
|
|
118
115
|
paths?: Partial<ExtensionPaths>;
|
|
119
116
|
logger?: SessionLogger;
|
|
120
117
|
forwarding?: ForwardingController;
|
|
118
|
+
permissionManager?: ScopedPermissionManager;
|
|
121
119
|
runtimeDeps?: PermissionSessionRuntimeDeps;
|
|
122
120
|
}): {
|
|
123
121
|
session: PermissionSession;
|
|
@@ -129,8 +127,16 @@ function createSession(overrides?: {
|
|
|
129
127
|
const paths = makePaths(overrides?.paths);
|
|
130
128
|
const logger = overrides?.logger ?? makeLogger();
|
|
131
129
|
const forwarding = overrides?.forwarding ?? makeForwarding();
|
|
130
|
+
const permissionManager =
|
|
131
|
+
overrides?.permissionManager ?? makePermissionManager();
|
|
132
132
|
const runtimeDeps = overrides?.runtimeDeps ?? makeRuntimeDeps();
|
|
133
|
-
const session = new PermissionSession(
|
|
133
|
+
const session = new PermissionSession(
|
|
134
|
+
paths,
|
|
135
|
+
logger,
|
|
136
|
+
forwarding,
|
|
137
|
+
permissionManager,
|
|
138
|
+
runtimeDeps,
|
|
139
|
+
);
|
|
134
140
|
return { session, paths, logger, forwarding, runtimeDeps };
|
|
135
141
|
}
|
|
136
142
|
|
|
@@ -139,10 +145,6 @@ function createSession(overrides?: {
|
|
|
139
145
|
beforeEach(() => {
|
|
140
146
|
mockGetActiveAgentName.mockReset();
|
|
141
147
|
mockGetActiveAgentNameFromSystemPrompt.mockReset();
|
|
142
|
-
mockCreatePermissionManagerForCwd.mockReset();
|
|
143
|
-
|
|
144
|
-
// Default: createPermissionManagerForCwd returns a fresh mock PM
|
|
145
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(makePermissionManager());
|
|
146
148
|
mockGetActiveAgentName.mockReturnValue(null);
|
|
147
149
|
mockGetActiveAgentNameFromSystemPrompt.mockReturnValue(null);
|
|
148
150
|
});
|
|
@@ -151,8 +153,7 @@ describe("PermissionSession", () => {
|
|
|
151
153
|
describe("constructor and delegation", () => {
|
|
152
154
|
it("delegates checkPermission to internal PermissionManager", () => {
|
|
153
155
|
const pm = makePermissionManager();
|
|
154
|
-
|
|
155
|
-
const { session } = createSession();
|
|
156
|
+
const { session } = createSession({ permissionManager: pm });
|
|
156
157
|
|
|
157
158
|
const result = session.checkPermission("bash", { command: "ls" });
|
|
158
159
|
|
|
@@ -167,8 +168,7 @@ describe("PermissionSession", () => {
|
|
|
167
168
|
|
|
168
169
|
it("delegates getToolPermission to internal PermissionManager", () => {
|
|
169
170
|
const pm = makePermissionManager();
|
|
170
|
-
|
|
171
|
-
const { session } = createSession();
|
|
171
|
+
const { session } = createSession({ permissionManager: pm });
|
|
172
172
|
|
|
173
173
|
const result = session.getToolPermission("read");
|
|
174
174
|
|
|
@@ -177,11 +177,9 @@ describe("PermissionSession", () => {
|
|
|
177
177
|
});
|
|
178
178
|
|
|
179
179
|
it("delegates getConfigIssues to internal PermissionManager", () => {
|
|
180
|
-
const pm = makePermissionManager(
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm);
|
|
184
|
-
const { session } = createSession();
|
|
180
|
+
const pm = makePermissionManager();
|
|
181
|
+
vi.mocked(pm.getConfigIssues).mockReturnValue(["issue1"]);
|
|
182
|
+
const { session } = createSession({ permissionManager: pm });
|
|
185
183
|
|
|
186
184
|
expect(session.getConfigIssues("agent1")).toEqual(["issue1"]);
|
|
187
185
|
expect(pm.getConfigIssues).toHaveBeenCalledWith("agent1");
|
|
@@ -189,8 +187,7 @@ describe("PermissionSession", () => {
|
|
|
189
187
|
|
|
190
188
|
it("delegates getPolicyCacheStamp to internal PermissionManager", () => {
|
|
191
189
|
const pm = makePermissionManager();
|
|
192
|
-
|
|
193
|
-
const { session } = createSession();
|
|
190
|
+
const { session } = createSession({ permissionManager: pm });
|
|
194
191
|
|
|
195
192
|
expect(session.getPolicyCacheStamp("agent1")).toBe("stamp-1");
|
|
196
193
|
expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent1");
|
|
@@ -217,6 +214,74 @@ describe("PermissionSession", () => {
|
|
|
217
214
|
});
|
|
218
215
|
});
|
|
219
216
|
|
|
217
|
+
describe("resolve", () => {
|
|
218
|
+
it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
|
|
219
|
+
const pm = makePermissionManager();
|
|
220
|
+
const { session } = createSession({ permissionManager: pm });
|
|
221
|
+
|
|
222
|
+
session.resolve("bash", { command: "ls" }, "agent-x");
|
|
223
|
+
|
|
224
|
+
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
225
|
+
"bash",
|
|
226
|
+
{ command: "ls" },
|
|
227
|
+
"agent-x",
|
|
228
|
+
[],
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("defaults agentName to undefined when omitted", () => {
|
|
233
|
+
const pm = makePermissionManager();
|
|
234
|
+
const { session } = createSession({ permissionManager: pm });
|
|
235
|
+
|
|
236
|
+
session.resolve("read", { path: ".env" });
|
|
237
|
+
|
|
238
|
+
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
239
|
+
"read",
|
|
240
|
+
{ path: ".env" },
|
|
241
|
+
undefined,
|
|
242
|
+
[],
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it("applies a recorded session approval on the next resolve", () => {
|
|
247
|
+
const pm = makePermissionManager();
|
|
248
|
+
const { session } = createSession({ permissionManager: pm });
|
|
249
|
+
|
|
250
|
+
session.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
251
|
+
session.resolve("bash", { command: "git status" });
|
|
252
|
+
|
|
253
|
+
const sessionRules = vi.mocked(pm.checkPermission).mock.calls[0][3];
|
|
254
|
+
expect(sessionRules).toHaveLength(1);
|
|
255
|
+
expect(sessionRules?.[0]).toMatchObject({
|
|
256
|
+
surface: "bash",
|
|
257
|
+
pattern: "git *",
|
|
258
|
+
action: "allow",
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("returns the PermissionManager's check result", () => {
|
|
263
|
+
const pm = makePermissionManager();
|
|
264
|
+
vi.mocked(pm.checkPermission).mockReturnValue({
|
|
265
|
+
state: "deny",
|
|
266
|
+
toolName: "bash",
|
|
267
|
+
source: "bash",
|
|
268
|
+
origin: "global",
|
|
269
|
+
matchedPattern: "rm *",
|
|
270
|
+
});
|
|
271
|
+
const { session } = createSession({ permissionManager: pm });
|
|
272
|
+
|
|
273
|
+
const result = session.resolve("bash", { command: "rm -rf /" });
|
|
274
|
+
|
|
275
|
+
expect(result).toEqual({
|
|
276
|
+
state: "deny",
|
|
277
|
+
toolName: "bash",
|
|
278
|
+
source: "bash",
|
|
279
|
+
origin: "global",
|
|
280
|
+
matchedPattern: "rm *",
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
220
285
|
describe("activate and deactivate", () => {
|
|
221
286
|
it("stores the context on activate", () => {
|
|
222
287
|
const { session, forwarding } = createSession();
|
|
@@ -237,28 +302,14 @@ describe("PermissionSession", () => {
|
|
|
237
302
|
});
|
|
238
303
|
|
|
239
304
|
describe("resetForNewSession", () => {
|
|
240
|
-
it("
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
state: "deny",
|
|
244
|
-
toolName: "bash",
|
|
245
|
-
source: "bash",
|
|
246
|
-
origin: "global",
|
|
247
|
-
}),
|
|
248
|
-
});
|
|
249
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
|
|
250
|
-
const { session } = createSession();
|
|
305
|
+
it("configures the injected PermissionManager for the context cwd", () => {
|
|
306
|
+
const pm = makePermissionManager();
|
|
307
|
+
const { session } = createSession({ permissionManager: pm });
|
|
251
308
|
const ctx = makeCtx({ cwd: "/new/project" });
|
|
252
309
|
|
|
253
310
|
session.resetForNewSession(ctx);
|
|
254
311
|
|
|
255
|
-
expect(
|
|
256
|
-
"/test/agent",
|
|
257
|
-
"/new/project",
|
|
258
|
-
);
|
|
259
|
-
// Verify the new PM is used for subsequent calls
|
|
260
|
-
const result = session.checkPermission("bash", { command: "rm" });
|
|
261
|
-
expect(result.state).toBe("deny");
|
|
312
|
+
expect(pm.configureForCwd).toHaveBeenCalledWith("/new/project");
|
|
262
313
|
});
|
|
263
314
|
|
|
264
315
|
it("clears cache keys", () => {
|
|
@@ -432,26 +483,25 @@ describe("PermissionSession", () => {
|
|
|
432
483
|
});
|
|
433
484
|
|
|
434
485
|
describe("infrastructure paths", () => {
|
|
435
|
-
it("
|
|
436
|
-
const { session } = createSession();
|
|
437
|
-
expect(session.getInfrastructureDirs()).toEqual([
|
|
438
|
-
"/test/agent",
|
|
439
|
-
"/test/agent/git",
|
|
440
|
-
]);
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
it("getInfrastructureReadPaths returns config paths", () => {
|
|
486
|
+
it("getInfrastructureReadDirs combines piInfrastructureDirs and piInfrastructureReadPaths", () => {
|
|
444
487
|
const runtimeDeps = makeRuntimeDeps();
|
|
445
488
|
(runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
446
489
|
piInfrastructureReadPaths: ["/extra/path"],
|
|
447
490
|
});
|
|
448
491
|
const { session } = createSession({ runtimeDeps });
|
|
449
|
-
expect(session.
|
|
492
|
+
expect(session.getInfrastructureReadDirs()).toEqual([
|
|
493
|
+
"/test/agent",
|
|
494
|
+
"/test/agent/git",
|
|
495
|
+
"/extra/path",
|
|
496
|
+
]);
|
|
450
497
|
});
|
|
451
498
|
|
|
452
|
-
it("
|
|
499
|
+
it("getInfrastructureReadDirs returns only piInfrastructureDirs when config omits the field", () => {
|
|
453
500
|
const { session } = createSession();
|
|
454
|
-
expect(session.
|
|
501
|
+
expect(session.getInfrastructureReadDirs()).toEqual([
|
|
502
|
+
"/test/agent",
|
|
503
|
+
"/test/agent/git",
|
|
504
|
+
]);
|
|
455
505
|
});
|
|
456
506
|
});
|
|
457
507
|
|
|
@@ -478,23 +528,38 @@ describe("PermissionSession", () => {
|
|
|
478
528
|
const { session } = createSession({ runtimeDeps });
|
|
479
529
|
expect(session.config).toBe(fakeConfig);
|
|
480
530
|
});
|
|
531
|
+
|
|
532
|
+
it("getToolPreviewLimits returns resolved preview limits from config", () => {
|
|
533
|
+
const runtimeDeps = makeRuntimeDeps();
|
|
534
|
+
(runtimeDeps.getConfig as ReturnType<typeof vi.fn>).mockReturnValue({
|
|
535
|
+
toolInputPreviewMaxLength: 400,
|
|
536
|
+
toolTextSummaryMaxLength: 120,
|
|
537
|
+
});
|
|
538
|
+
const { session } = createSession({ runtimeDeps });
|
|
539
|
+
const limits = session.getToolPreviewLimits();
|
|
540
|
+
expect(limits.toolInputPreviewMaxLength).toBe(400);
|
|
541
|
+
expect(limits.toolTextSummaryMaxLength).toBe(120);
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("getToolPreviewLimits falls back to built-in defaults when config omits fields", () => {
|
|
545
|
+
const { session } = createSession();
|
|
546
|
+
const limits = session.getToolPreviewLimits();
|
|
547
|
+
expect(limits.toolInputPreviewMaxLength).toBeGreaterThan(0);
|
|
548
|
+
expect(limits.toolTextSummaryMaxLength).toBeGreaterThan(0);
|
|
549
|
+
expect(limits.toolInputLogPreviewMaxLength).toBeGreaterThan(0);
|
|
550
|
+
});
|
|
481
551
|
});
|
|
482
552
|
|
|
483
553
|
describe("reload", () => {
|
|
484
|
-
it("
|
|
485
|
-
const
|
|
554
|
+
it("configures PermissionManager for current context cwd", () => {
|
|
555
|
+
const pm = makePermissionManager();
|
|
556
|
+
const { session } = createSession({ permissionManager: pm });
|
|
486
557
|
const ctx = makeCtx({ cwd: "/project" });
|
|
487
558
|
session.activate(ctx);
|
|
488
559
|
|
|
489
|
-
const pm2 = makePermissionManager();
|
|
490
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
|
|
491
|
-
|
|
492
560
|
session.reload();
|
|
493
561
|
|
|
494
|
-
expect(
|
|
495
|
-
"/test/agent",
|
|
496
|
-
"/project",
|
|
497
|
-
);
|
|
562
|
+
expect(pm.configureForCwd).toHaveBeenCalledWith("/project");
|
|
498
563
|
});
|
|
499
564
|
|
|
500
565
|
it("clears caches and skill entries", () => {
|
|
@@ -532,6 +597,62 @@ describe("PermissionSession", () => {
|
|
|
532
597
|
});
|
|
533
598
|
});
|
|
534
599
|
|
|
600
|
+
describe("canConfirm", () => {
|
|
601
|
+
it("returns true when context is active and canPrompt returns true", () => {
|
|
602
|
+
const { session } = createSession();
|
|
603
|
+
session.activate(makeCtx());
|
|
604
|
+
expect(session.canConfirm()).toBe(true);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("returns false when no context is active", () => {
|
|
608
|
+
const { session } = createSession();
|
|
609
|
+
expect(session.canConfirm()).toBe(false);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
it("returns false when canPrompt returns false", () => {
|
|
613
|
+
const runtimeDeps = makeRuntimeDeps();
|
|
614
|
+
(
|
|
615
|
+
runtimeDeps.canRequestPermissionConfirmation as ReturnType<typeof vi.fn>
|
|
616
|
+
).mockReturnValue(false);
|
|
617
|
+
const { session } = createSession({ runtimeDeps });
|
|
618
|
+
session.activate(makeCtx());
|
|
619
|
+
expect(session.canConfirm()).toBe(false);
|
|
620
|
+
});
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
describe("promptPermission", () => {
|
|
624
|
+
it("delegates to prompt with stored context", async () => {
|
|
625
|
+
const { session, runtimeDeps } = createSession();
|
|
626
|
+
const ctx = makeCtx();
|
|
627
|
+
session.activate(ctx);
|
|
628
|
+
const details = {
|
|
629
|
+
requestId: "req-1",
|
|
630
|
+
source: "tool_call" as const,
|
|
631
|
+
agentName: null,
|
|
632
|
+
message: "Allow?",
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const result = await session.promptPermission(details);
|
|
636
|
+
|
|
637
|
+
expect(runtimeDeps.promptPermission).toHaveBeenCalledWith(ctx, details);
|
|
638
|
+
expect(result).toEqual({ approved: true, state: "approved" });
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
it("throws when no context is active", async () => {
|
|
642
|
+
const { session } = createSession();
|
|
643
|
+
const details = {
|
|
644
|
+
requestId: "req-1",
|
|
645
|
+
source: "tool_call" as const,
|
|
646
|
+
agentName: null,
|
|
647
|
+
message: "Allow?",
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
await expect(session.promptPermission(details)).rejects.toThrow(
|
|
651
|
+
"promptPermission called before the session was activated",
|
|
652
|
+
);
|
|
653
|
+
});
|
|
654
|
+
});
|
|
655
|
+
|
|
535
656
|
describe("canPrompt", () => {
|
|
536
657
|
it("delegates to runtimeDeps.canRequestPermissionConfirmation", () => {
|
|
537
658
|
const { session, runtimeDeps } = createSession();
|
|
@@ -573,19 +694,4 @@ describe("PermissionSession", () => {
|
|
|
573
694
|
expect(result).toEqual({ approved: true, state: "approved" });
|
|
574
695
|
});
|
|
575
696
|
});
|
|
576
|
-
|
|
577
|
-
describe("createPermissionRequestId", () => {
|
|
578
|
-
it("starts with the given prefix", () => {
|
|
579
|
-
const { session } = createSession();
|
|
580
|
-
const id = session.createPermissionRequestId("skill-input");
|
|
581
|
-
expect(id.startsWith("skill-input-")).toBe(true);
|
|
582
|
-
});
|
|
583
|
-
|
|
584
|
-
it("generates unique IDs on repeated calls", () => {
|
|
585
|
-
const { session } = createSession();
|
|
586
|
-
const id1 = session.createPermissionRequestId("test");
|
|
587
|
-
const id2 = session.createPermissionRequestId("test");
|
|
588
|
-
expect(id1).not.toBe(id2);
|
|
589
|
-
});
|
|
590
|
-
});
|
|
591
697
|
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PermissionManager } from "#src/permission-manager";
|
|
3
|
+
import { LocalPermissionsService } from "#src/permissions-service";
|
|
4
|
+
import type { Ruleset } from "#src/rule";
|
|
5
|
+
import type { SessionRules } from "#src/session-rules";
|
|
6
|
+
import type {
|
|
7
|
+
ToolInputFormatter,
|
|
8
|
+
ToolInputFormatterRegistry,
|
|
9
|
+
} from "#src/tool-input-formatter-registry";
|
|
10
|
+
|
|
11
|
+
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
12
|
+
|
|
13
|
+
// ── input-normalizer stub ──────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const mockBuildInputForSurface = vi.hoisted(() =>
|
|
16
|
+
vi.fn<(surface: string, value?: string) => unknown>(),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
vi.mock("#src/input-normalizer", () => ({
|
|
20
|
+
buildInputForSurface: mockBuildInputForSurface,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function makePermissionManager(): PermissionManager {
|
|
26
|
+
return {
|
|
27
|
+
checkPermission: vi
|
|
28
|
+
.fn<PermissionManager["checkPermission"]>()
|
|
29
|
+
.mockReturnValue(makeCheckResult()),
|
|
30
|
+
getToolPermission: vi
|
|
31
|
+
.fn<PermissionManager["getToolPermission"]>()
|
|
32
|
+
.mockReturnValue("allow"),
|
|
33
|
+
} as unknown as PermissionManager;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function makeSessionRules(rules: Ruleset = []): SessionRules {
|
|
37
|
+
return {
|
|
38
|
+
getRuleset: vi.fn<SessionRules["getRuleset"]>().mockReturnValue(rules),
|
|
39
|
+
} as unknown as SessionRules;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeFormatterRegistry(): ToolInputFormatterRegistry {
|
|
43
|
+
return {
|
|
44
|
+
register: vi
|
|
45
|
+
.fn<ToolInputFormatterRegistry["register"]>()
|
|
46
|
+
.mockReturnValue(vi.fn()),
|
|
47
|
+
} as unknown as ToolInputFormatterRegistry;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function makeService(overrides?: {
|
|
51
|
+
permissionManager?: PermissionManager;
|
|
52
|
+
sessionRules?: SessionRules;
|
|
53
|
+
formatterRegistry?: ToolInputFormatterRegistry;
|
|
54
|
+
}) {
|
|
55
|
+
const permissionManager =
|
|
56
|
+
overrides?.permissionManager ?? makePermissionManager();
|
|
57
|
+
const sessionRules = overrides?.sessionRules ?? makeSessionRules();
|
|
58
|
+
const formatterRegistry =
|
|
59
|
+
overrides?.formatterRegistry ?? makeFormatterRegistry();
|
|
60
|
+
const service = new LocalPermissionsService(
|
|
61
|
+
permissionManager,
|
|
62
|
+
sessionRules,
|
|
63
|
+
formatterRegistry,
|
|
64
|
+
);
|
|
65
|
+
return { service, permissionManager, sessionRules, formatterRegistry };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── tests ──────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
beforeEach(() => {
|
|
71
|
+
mockBuildInputForSurface.mockReset();
|
|
72
|
+
mockBuildInputForSurface.mockReturnValue({ type: "tool-input" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("checkPermission", () => {
|
|
76
|
+
it("builds the surface input from surface and value", () => {
|
|
77
|
+
const { service } = makeService();
|
|
78
|
+
service.checkPermission("bash", "echo hi");
|
|
79
|
+
expect(mockBuildInputForSurface).toHaveBeenCalledWith("bash", "echo hi");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("builds the surface input with undefined value when value is omitted", () => {
|
|
83
|
+
const { service } = makeService();
|
|
84
|
+
service.checkPermission("read");
|
|
85
|
+
expect(mockBuildInputForSurface).toHaveBeenCalledWith("read", undefined);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("calls permissionManager.checkPermission with surface, built input, agentName, and current ruleset", () => {
|
|
89
|
+
const ruleset: Ruleset = [
|
|
90
|
+
{ surface: "bash", pattern: "*", action: "allow", origin: "global" },
|
|
91
|
+
];
|
|
92
|
+
const builtInput = { type: "bash-input" };
|
|
93
|
+
mockBuildInputForSurface.mockReturnValue(builtInput);
|
|
94
|
+
const { service, permissionManager, sessionRules } = makeService({
|
|
95
|
+
sessionRules: makeSessionRules(ruleset),
|
|
96
|
+
});
|
|
97
|
+
service.checkPermission("bash", "echo hi", "my-agent");
|
|
98
|
+
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
99
|
+
"bash",
|
|
100
|
+
builtInput,
|
|
101
|
+
"my-agent",
|
|
102
|
+
ruleset,
|
|
103
|
+
);
|
|
104
|
+
void sessionRules; // used indirectly
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("returns the result from permissionManager.checkPermission", () => {
|
|
108
|
+
const expected = makeCheckResult({ state: "deny", toolName: "bash" });
|
|
109
|
+
const { service, permissionManager } = makeService();
|
|
110
|
+
vi.mocked(permissionManager.checkPermission).mockReturnValue(expected);
|
|
111
|
+
const result = service.checkPermission("bash", "rm -rf /");
|
|
112
|
+
expect(result).toBe(expected);
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("getToolPermission", () => {
|
|
117
|
+
it("delegates to permissionManager.getToolPermission", () => {
|
|
118
|
+
const { service, permissionManager } = makeService();
|
|
119
|
+
vi.mocked(permissionManager.getToolPermission).mockReturnValue("deny");
|
|
120
|
+
const result = service.getToolPermission("write", "my-agent");
|
|
121
|
+
expect(permissionManager.getToolPermission).toHaveBeenCalledWith(
|
|
122
|
+
"write",
|
|
123
|
+
"my-agent",
|
|
124
|
+
);
|
|
125
|
+
expect(result).toBe("deny");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("omits agentName when not provided", () => {
|
|
129
|
+
const { service, permissionManager } = makeService();
|
|
130
|
+
service.getToolPermission("read");
|
|
131
|
+
expect(permissionManager.getToolPermission).toHaveBeenCalledWith(
|
|
132
|
+
"read",
|
|
133
|
+
undefined,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe("registerToolInputFormatter", () => {
|
|
139
|
+
it("delegates to formatterRegistry.register and returns the unsubscribe function", () => {
|
|
140
|
+
const unsub = vi.fn();
|
|
141
|
+
const { service, formatterRegistry } = makeService();
|
|
142
|
+
vi.mocked(formatterRegistry.register).mockReturnValue(unsub);
|
|
143
|
+
const formatter: ToolInputFormatter = vi.fn();
|
|
144
|
+
const result = service.registerToolInputFormatter("my-tool", formatter);
|
|
145
|
+
expect(formatterRegistry.register).toHaveBeenCalledWith(
|
|
146
|
+
"my-tool",
|
|
147
|
+
formatter,
|
|
148
|
+
);
|
|
149
|
+
expect(result).toBe(unsub);
|
|
150
|
+
});
|
|
151
|
+
});
|