@gotgenes/pi-permission-system 10.1.0 → 10.3.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 +14 -0
- package/package.json +1 -1
- package/src/config-modal.ts +7 -10
- package/src/config-store.ts +243 -0
- package/src/index.ts +11 -15
- package/src/permission-manager.ts +69 -3
- package/src/permission-prompter.ts +3 -3
- package/src/permission-session.ts +13 -28
- package/src/runtime.ts +34 -203
- package/test/config-modal.test.ts +15 -10
- package/test/config-store.test.ts +452 -0
- package/test/handlers/external-directory-integration.test.ts +81 -176
- package/test/handlers/gates/bash-path.test.ts +26 -44
- package/test/handlers/gates/runner.test.ts +27 -119
- package/test/handlers/tool-call.test.ts +44 -153
- package/test/helpers/gate-fixtures.ts +66 -2
- package/test/helpers/handler-fixtures.ts +83 -2
- package/test/permission-manager-unified.test.ts +159 -1
- package/test/permission-prompter.test.ts +12 -5
- package/test/permission-session.test.ts +111 -120
- package/test/runtime.test.ts +11 -275
|
@@ -3,42 +3,34 @@ 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
|
|
|
20
|
+
import type { SessionConfigStore } from "#src/config-store";
|
|
21
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
32
22
|
import type { ExtensionPaths } from "#src/extension-paths";
|
|
33
23
|
import type { ForwardingController } from "#src/forwarding-manager";
|
|
34
|
-
import type {
|
|
24
|
+
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
35
25
|
import {
|
|
36
26
|
PermissionSession,
|
|
37
27
|
type PermissionSessionRuntimeDeps,
|
|
38
28
|
} from "#src/permission-session";
|
|
29
|
+
import type { Ruleset } from "#src/rule";
|
|
39
30
|
import { SessionApproval } from "#src/session-approval";
|
|
40
31
|
import type { SessionLogger } from "#src/session-logger";
|
|
41
32
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
33
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
42
34
|
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
43
35
|
|
|
44
36
|
function makeSkillEntry(
|
|
@@ -76,11 +68,22 @@ function makeLogger(): SessionLogger {
|
|
|
76
68
|
};
|
|
77
69
|
}
|
|
78
70
|
|
|
71
|
+
function makeConfigStore(
|
|
72
|
+
overrides: Partial<SessionConfigStore> = {},
|
|
73
|
+
): SessionConfigStore {
|
|
74
|
+
return {
|
|
75
|
+
current:
|
|
76
|
+
overrides.current ??
|
|
77
|
+
vi
|
|
78
|
+
.fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
|
|
79
|
+
.mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
|
|
80
|
+
refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
|
|
81
|
+
logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
function makeRuntimeDeps(): PermissionSessionRuntimeDeps {
|
|
80
86
|
return {
|
|
81
|
-
refreshExtensionConfig: vi.fn(),
|
|
82
|
-
logResolvedConfigPaths: vi.fn(),
|
|
83
|
-
getConfig: vi.fn().mockReturnValue({}),
|
|
84
87
|
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
85
88
|
promptPermission: vi
|
|
86
89
|
.fn()
|
|
@@ -95,43 +98,63 @@ function makeForwarding(): ForwardingController {
|
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
100
|
|
|
98
|
-
function makePermissionManager(
|
|
99
|
-
overrides: Partial<PermissionManager> = {},
|
|
100
|
-
): PermissionManager {
|
|
101
|
+
function makePermissionManager() {
|
|
101
102
|
return {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
103
|
+
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
104
|
+
checkPermission: vi
|
|
105
|
+
.fn<
|
|
106
|
+
(
|
|
107
|
+
toolName: string,
|
|
108
|
+
input: unknown,
|
|
109
|
+
agentName?: string,
|
|
110
|
+
sessionRules?: Ruleset,
|
|
111
|
+
) => PermissionCheckResult
|
|
112
|
+
>()
|
|
113
|
+
.mockReturnValue({
|
|
114
|
+
state: "allow",
|
|
115
|
+
toolName: "read",
|
|
116
|
+
source: "tool",
|
|
117
|
+
origin: "builtin",
|
|
118
|
+
}),
|
|
119
|
+
getToolPermission: vi
|
|
120
|
+
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
121
|
+
.mockReturnValue("allow"),
|
|
122
|
+
getConfigIssues: vi.fn((): string[] => []),
|
|
123
|
+
getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
|
|
124
|
+
};
|
|
115
125
|
}
|
|
116
126
|
|
|
117
127
|
function createSession(overrides?: {
|
|
118
128
|
paths?: Partial<ExtensionPaths>;
|
|
119
129
|
logger?: SessionLogger;
|
|
120
130
|
forwarding?: ForwardingController;
|
|
131
|
+
permissionManager?: ScopedPermissionManager;
|
|
132
|
+
configStore?: SessionConfigStore;
|
|
121
133
|
runtimeDeps?: PermissionSessionRuntimeDeps;
|
|
122
134
|
}): {
|
|
123
135
|
session: PermissionSession;
|
|
124
136
|
paths: ExtensionPaths;
|
|
125
137
|
logger: SessionLogger;
|
|
126
138
|
forwarding: ForwardingController;
|
|
139
|
+
configStore: SessionConfigStore;
|
|
127
140
|
runtimeDeps: PermissionSessionRuntimeDeps;
|
|
128
141
|
} {
|
|
129
142
|
const paths = makePaths(overrides?.paths);
|
|
130
143
|
const logger = overrides?.logger ?? makeLogger();
|
|
131
144
|
const forwarding = overrides?.forwarding ?? makeForwarding();
|
|
145
|
+
const permissionManager =
|
|
146
|
+
overrides?.permissionManager ?? makePermissionManager();
|
|
147
|
+
const configStore = overrides?.configStore ?? makeConfigStore();
|
|
132
148
|
const runtimeDeps = overrides?.runtimeDeps ?? makeRuntimeDeps();
|
|
133
|
-
const session = new PermissionSession(
|
|
134
|
-
|
|
149
|
+
const session = new PermissionSession(
|
|
150
|
+
paths,
|
|
151
|
+
logger,
|
|
152
|
+
forwarding,
|
|
153
|
+
permissionManager,
|
|
154
|
+
configStore,
|
|
155
|
+
runtimeDeps,
|
|
156
|
+
);
|
|
157
|
+
return { session, paths, logger, forwarding, configStore, runtimeDeps };
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
@@ -139,10 +162,6 @@ function createSession(overrides?: {
|
|
|
139
162
|
beforeEach(() => {
|
|
140
163
|
mockGetActiveAgentName.mockReset();
|
|
141
164
|
mockGetActiveAgentNameFromSystemPrompt.mockReset();
|
|
142
|
-
mockCreatePermissionManagerForCwd.mockReset();
|
|
143
|
-
|
|
144
|
-
// Default: createPermissionManagerForCwd returns a fresh mock PM
|
|
145
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(makePermissionManager());
|
|
146
165
|
mockGetActiveAgentName.mockReturnValue(null);
|
|
147
166
|
mockGetActiveAgentNameFromSystemPrompt.mockReturnValue(null);
|
|
148
167
|
});
|
|
@@ -151,8 +170,7 @@ describe("PermissionSession", () => {
|
|
|
151
170
|
describe("constructor and delegation", () => {
|
|
152
171
|
it("delegates checkPermission to internal PermissionManager", () => {
|
|
153
172
|
const pm = makePermissionManager();
|
|
154
|
-
|
|
155
|
-
const { session } = createSession();
|
|
173
|
+
const { session } = createSession({ permissionManager: pm });
|
|
156
174
|
|
|
157
175
|
const result = session.checkPermission("bash", { command: "ls" });
|
|
158
176
|
|
|
@@ -167,8 +185,7 @@ describe("PermissionSession", () => {
|
|
|
167
185
|
|
|
168
186
|
it("delegates getToolPermission to internal PermissionManager", () => {
|
|
169
187
|
const pm = makePermissionManager();
|
|
170
|
-
|
|
171
|
-
const { session } = createSession();
|
|
188
|
+
const { session } = createSession({ permissionManager: pm });
|
|
172
189
|
|
|
173
190
|
const result = session.getToolPermission("read");
|
|
174
191
|
|
|
@@ -177,11 +194,9 @@ describe("PermissionSession", () => {
|
|
|
177
194
|
});
|
|
178
195
|
|
|
179
196
|
it("delegates getConfigIssues to internal PermissionManager", () => {
|
|
180
|
-
const pm = makePermissionManager(
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm);
|
|
184
|
-
const { session } = createSession();
|
|
197
|
+
const pm = makePermissionManager();
|
|
198
|
+
vi.mocked(pm.getConfigIssues).mockReturnValue(["issue1"]);
|
|
199
|
+
const { session } = createSession({ permissionManager: pm });
|
|
185
200
|
|
|
186
201
|
expect(session.getConfigIssues("agent1")).toEqual(["issue1"]);
|
|
187
202
|
expect(pm.getConfigIssues).toHaveBeenCalledWith("agent1");
|
|
@@ -189,8 +204,7 @@ describe("PermissionSession", () => {
|
|
|
189
204
|
|
|
190
205
|
it("delegates getPolicyCacheStamp to internal PermissionManager", () => {
|
|
191
206
|
const pm = makePermissionManager();
|
|
192
|
-
|
|
193
|
-
const { session } = createSession();
|
|
207
|
+
const { session } = createSession({ permissionManager: pm });
|
|
194
208
|
|
|
195
209
|
expect(session.getPolicyCacheStamp("agent1")).toBe("stamp-1");
|
|
196
210
|
expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent1");
|
|
@@ -220,8 +234,7 @@ describe("PermissionSession", () => {
|
|
|
220
234
|
describe("resolve", () => {
|
|
221
235
|
it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
|
|
222
236
|
const pm = makePermissionManager();
|
|
223
|
-
|
|
224
|
-
const { session } = createSession();
|
|
237
|
+
const { session } = createSession({ permissionManager: pm });
|
|
225
238
|
|
|
226
239
|
session.resolve("bash", { command: "ls" }, "agent-x");
|
|
227
240
|
|
|
@@ -235,8 +248,7 @@ describe("PermissionSession", () => {
|
|
|
235
248
|
|
|
236
249
|
it("defaults agentName to undefined when omitted", () => {
|
|
237
250
|
const pm = makePermissionManager();
|
|
238
|
-
|
|
239
|
-
const { session } = createSession();
|
|
251
|
+
const { session } = createSession({ permissionManager: pm });
|
|
240
252
|
|
|
241
253
|
session.resolve("read", { path: ".env" });
|
|
242
254
|
|
|
@@ -250,8 +262,7 @@ describe("PermissionSession", () => {
|
|
|
250
262
|
|
|
251
263
|
it("applies a recorded session approval on the next resolve", () => {
|
|
252
264
|
const pm = makePermissionManager();
|
|
253
|
-
|
|
254
|
-
const { session } = createSession();
|
|
265
|
+
const { session } = createSession({ permissionManager: pm });
|
|
255
266
|
|
|
256
267
|
session.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
257
268
|
session.resolve("bash", { command: "git status" });
|
|
@@ -266,17 +277,15 @@ describe("PermissionSession", () => {
|
|
|
266
277
|
});
|
|
267
278
|
|
|
268
279
|
it("returns the PermissionManager's check result", () => {
|
|
269
|
-
const pm = makePermissionManager(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}),
|
|
280
|
+
const pm = makePermissionManager();
|
|
281
|
+
vi.mocked(pm.checkPermission).mockReturnValue({
|
|
282
|
+
state: "deny",
|
|
283
|
+
toolName: "bash",
|
|
284
|
+
source: "bash",
|
|
285
|
+
origin: "global",
|
|
286
|
+
matchedPattern: "rm *",
|
|
277
287
|
});
|
|
278
|
-
|
|
279
|
-
const { session } = createSession();
|
|
288
|
+
const { session } = createSession({ permissionManager: pm });
|
|
280
289
|
|
|
281
290
|
const result = session.resolve("bash", { command: "rm -rf /" });
|
|
282
291
|
|
|
@@ -310,28 +319,14 @@ describe("PermissionSession", () => {
|
|
|
310
319
|
});
|
|
311
320
|
|
|
312
321
|
describe("resetForNewSession", () => {
|
|
313
|
-
it("
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
state: "deny",
|
|
317
|
-
toolName: "bash",
|
|
318
|
-
source: "bash",
|
|
319
|
-
origin: "global",
|
|
320
|
-
}),
|
|
321
|
-
});
|
|
322
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
|
|
323
|
-
const { session } = createSession();
|
|
322
|
+
it("configures the injected PermissionManager for the context cwd", () => {
|
|
323
|
+
const pm = makePermissionManager();
|
|
324
|
+
const { session } = createSession({ permissionManager: pm });
|
|
324
325
|
const ctx = makeCtx({ cwd: "/new/project" });
|
|
325
326
|
|
|
326
327
|
session.resetForNewSession(ctx);
|
|
327
328
|
|
|
328
|
-
expect(
|
|
329
|
-
"/test/agent",
|
|
330
|
-
"/new/project",
|
|
331
|
-
);
|
|
332
|
-
// Verify the new PM is used for subsequent calls
|
|
333
|
-
const result = session.checkPermission("bash", { command: "rm" });
|
|
334
|
-
expect(result.state).toBe("deny");
|
|
329
|
+
expect(pm.configureForCwd).toHaveBeenCalledWith("/new/project");
|
|
335
330
|
});
|
|
336
331
|
|
|
337
332
|
it("clears cache keys", () => {
|
|
@@ -506,11 +501,12 @@ describe("PermissionSession", () => {
|
|
|
506
501
|
|
|
507
502
|
describe("infrastructure paths", () => {
|
|
508
503
|
it("getInfrastructureReadDirs combines piInfrastructureDirs and piInfrastructureReadPaths", () => {
|
|
509
|
-
const
|
|
510
|
-
|
|
511
|
-
|
|
504
|
+
const configStore = makeConfigStore({
|
|
505
|
+
current: vi.fn().mockReturnValue({
|
|
506
|
+
piInfrastructureReadPaths: ["/extra/path"],
|
|
507
|
+
}),
|
|
512
508
|
});
|
|
513
|
-
const { session } = createSession({
|
|
509
|
+
const { session } = createSession({ configStore });
|
|
514
510
|
expect(session.getInfrastructureReadDirs()).toEqual([
|
|
515
511
|
"/test/agent",
|
|
516
512
|
"/test/agent/git",
|
|
@@ -528,36 +524,36 @@ describe("PermissionSession", () => {
|
|
|
528
524
|
});
|
|
529
525
|
|
|
530
526
|
describe("config delegation", () => {
|
|
531
|
-
it("refreshConfig delegates to
|
|
532
|
-
const { session,
|
|
527
|
+
it("refreshConfig delegates to configStore.refresh", () => {
|
|
528
|
+
const { session, configStore } = createSession();
|
|
533
529
|
const ctx = makeCtx();
|
|
534
530
|
session.refreshConfig(ctx);
|
|
535
|
-
expect(
|
|
531
|
+
expect(configStore.refresh).toHaveBeenCalledWith(ctx);
|
|
536
532
|
});
|
|
537
533
|
|
|
538
|
-
it("logResolvedConfigPaths delegates to
|
|
539
|
-
const { session,
|
|
534
|
+
it("logResolvedConfigPaths delegates to configStore.logResolvedPaths", () => {
|
|
535
|
+
const { session, configStore } = createSession();
|
|
540
536
|
session.logResolvedConfigPaths();
|
|
541
|
-
expect(
|
|
537
|
+
expect(configStore.logResolvedPaths).toHaveBeenCalled();
|
|
542
538
|
});
|
|
543
539
|
|
|
544
|
-
it("config getter delegates to
|
|
545
|
-
const
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
);
|
|
550
|
-
const { session } = createSession({ runtimeDeps });
|
|
540
|
+
it("config getter delegates to configStore.current()", () => {
|
|
541
|
+
const fakeConfig = { debugLog: true } as typeof DEFAULT_EXTENSION_CONFIG;
|
|
542
|
+
const configStore = makeConfigStore({
|
|
543
|
+
current: vi.fn().mockReturnValue(fakeConfig),
|
|
544
|
+
});
|
|
545
|
+
const { session } = createSession({ configStore });
|
|
551
546
|
expect(session.config).toBe(fakeConfig);
|
|
552
547
|
});
|
|
553
548
|
|
|
554
549
|
it("getToolPreviewLimits returns resolved preview limits from config", () => {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
550
|
+
const configStore = makeConfigStore({
|
|
551
|
+
current: vi.fn().mockReturnValue({
|
|
552
|
+
toolInputPreviewMaxLength: 400,
|
|
553
|
+
toolTextSummaryMaxLength: 120,
|
|
554
|
+
}),
|
|
559
555
|
});
|
|
560
|
-
const { session } = createSession({
|
|
556
|
+
const { session } = createSession({ configStore });
|
|
561
557
|
const limits = session.getToolPreviewLimits();
|
|
562
558
|
expect(limits.toolInputPreviewMaxLength).toBe(400);
|
|
563
559
|
expect(limits.toolTextSummaryMaxLength).toBe(120);
|
|
@@ -573,20 +569,15 @@ describe("PermissionSession", () => {
|
|
|
573
569
|
});
|
|
574
570
|
|
|
575
571
|
describe("reload", () => {
|
|
576
|
-
it("
|
|
577
|
-
const
|
|
572
|
+
it("configures PermissionManager for current context cwd", () => {
|
|
573
|
+
const pm = makePermissionManager();
|
|
574
|
+
const { session } = createSession({ permissionManager: pm });
|
|
578
575
|
const ctx = makeCtx({ cwd: "/project" });
|
|
579
576
|
session.activate(ctx);
|
|
580
577
|
|
|
581
|
-
const pm2 = makePermissionManager();
|
|
582
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
|
|
583
|
-
|
|
584
578
|
session.reload();
|
|
585
579
|
|
|
586
|
-
expect(
|
|
587
|
-
"/test/agent",
|
|
588
|
-
"/project",
|
|
589
|
-
);
|
|
580
|
+
expect(pm.configureForCwd).toHaveBeenCalledWith("/project");
|
|
590
581
|
});
|
|
591
582
|
|
|
592
583
|
it("clears caches and skill entries", () => {
|