@gotgenes/pi-permission-system 10.1.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 +7 -0
- package/package.json +1 -1
- package/src/index.ts +4 -0
- package/src/permission-manager.ts +69 -3
- package/src/permission-session.ts +6 -18
- package/src/runtime.ts +1 -37
- 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-session.test.ts +67 -94
- package/test/runtime.test.ts +2 -82
|
@@ -30,7 +30,7 @@ import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
|
30
30
|
import type { SessionLogger } from "#src/session-logger";
|
|
31
31
|
import { resolveToolPreviewLimits } from "#src/tool-preview-formatter";
|
|
32
32
|
import type { ToolRegistry } from "#src/tool-registry";
|
|
33
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
33
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Precise mock boundary for PermissionGateHandler integration tests.
|
|
@@ -220,6 +220,78 @@ export function makeToolRegistry(
|
|
|
220
220
|
};
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Surface-dispatching `checkPermission` mock.
|
|
225
|
+
*
|
|
226
|
+
* Builds a `vi.fn()` that returns a `PermissionCheckResult` for each surface,
|
|
227
|
+
* using `bySurface[surface]` when matched and `defaultResult` otherwise.
|
|
228
|
+
* Default fields: `toolName` = the surface string, `source: "tool"`,
|
|
229
|
+
* `origin: "builtin"` — callers override by including the field in the
|
|
230
|
+
* per-surface or default partial (e.g. `{ path: { state: "allow", source: "special" } }`).
|
|
231
|
+
*
|
|
232
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
233
|
+
* mock access (`mock.calls`, `toHaveBeenCalledWith`, etc.).
|
|
234
|
+
*/
|
|
235
|
+
export function makeSurfaceCheck(
|
|
236
|
+
bySurface: Record<
|
|
237
|
+
string,
|
|
238
|
+
Partial<PermissionCheckResult> & { state: PermissionState }
|
|
239
|
+
>,
|
|
240
|
+
defaultResult: Partial<PermissionCheckResult> & { state: PermissionState } = {
|
|
241
|
+
state: "allow",
|
|
242
|
+
},
|
|
243
|
+
) {
|
|
244
|
+
return vi
|
|
245
|
+
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
246
|
+
.mockImplementation((surface): PermissionCheckResult => {
|
|
247
|
+
const base = bySurface[surface] ?? defaultResult;
|
|
248
|
+
return {
|
|
249
|
+
toolName: surface,
|
|
250
|
+
source: "tool",
|
|
251
|
+
origin: "builtin",
|
|
252
|
+
...base,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Bash-surface `checkPermission` mock that dispatches on a command regex.
|
|
259
|
+
*
|
|
260
|
+
* For the `bash` surface: returns a deny result when `opts.deny` matches the
|
|
261
|
+
* command, and an allow result otherwise. For all other surfaces, returns a
|
|
262
|
+
* plain allow result.
|
|
263
|
+
*
|
|
264
|
+
* Return type is intentionally unannotated so callers retain full `vi.fn()`
|
|
265
|
+
* mock access.
|
|
266
|
+
*/
|
|
267
|
+
export function makeBashCommandCheck(opts: {
|
|
268
|
+
deny: RegExp;
|
|
269
|
+
denyMatched: string;
|
|
270
|
+
allowMatched?: string;
|
|
271
|
+
}) {
|
|
272
|
+
return vi
|
|
273
|
+
.fn<MockGateHandlerSession["checkPermission"]>()
|
|
274
|
+
.mockImplementation((surface, input): PermissionCheckResult => {
|
|
275
|
+
if (surface === "bash") {
|
|
276
|
+
const command = (input as { command?: string }).command ?? "";
|
|
277
|
+
return opts.deny.test(command)
|
|
278
|
+
? makeCheckResult({
|
|
279
|
+
state: "deny",
|
|
280
|
+
source: "bash",
|
|
281
|
+
command,
|
|
282
|
+
matchedPattern: opts.denyMatched,
|
|
283
|
+
})
|
|
284
|
+
: makeCheckResult({
|
|
285
|
+
state: "allow",
|
|
286
|
+
source: "bash",
|
|
287
|
+
command,
|
|
288
|
+
matchedPattern: opts.allowMatched,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
return makeCheckResult({ state: "allow" });
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
223
295
|
/**
|
|
224
296
|
* Constructs a PermissionGateHandler with mocked collaborators.
|
|
225
297
|
*
|
|
@@ -229,10 +301,19 @@ export function makeToolRegistry(
|
|
|
229
301
|
export function makeHandler(overrides?: {
|
|
230
302
|
session?: Partial<MockGateHandlerSession>;
|
|
231
303
|
toolRegistry?: Partial<ToolRegistry>;
|
|
304
|
+
/** Sugar: builds the `getAll` mock from a list of tool names. */
|
|
305
|
+
tools?: string[];
|
|
232
306
|
}) {
|
|
233
307
|
const session = makeSession(overrides?.session);
|
|
234
308
|
const events = makeEvents();
|
|
235
|
-
const toolRegistry =
|
|
309
|
+
const toolRegistry =
|
|
310
|
+
overrides?.tools !== undefined
|
|
311
|
+
? makeToolRegistry({
|
|
312
|
+
getAll: vi
|
|
313
|
+
.fn()
|
|
314
|
+
.mockReturnValue(overrides.tools.map((name) => ({ name }))),
|
|
315
|
+
})
|
|
316
|
+
: makeToolRegistry(overrides?.toolRegistry);
|
|
236
317
|
const pipeline = new ToolCallGatePipeline(session);
|
|
237
318
|
const skillInputPipeline = new SkillInputGatePipeline(session);
|
|
238
319
|
const reporter = new GateDecisionReporter(session.logger, events);
|
|
@@ -8,7 +8,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
8
8
|
import { homedir, tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { describe, expect, it } from "vitest";
|
|
11
|
-
import {
|
|
11
|
+
import { getGlobalConfigPath, getProjectConfigPath } from "#src/config-paths";
|
|
12
|
+
import {
|
|
13
|
+
PermissionManager,
|
|
14
|
+
type ScopedPermissionManager,
|
|
15
|
+
} from "#src/permission-manager";
|
|
12
16
|
import type { Rule, Ruleset } from "#src/rule";
|
|
13
17
|
|
|
14
18
|
// ---------------------------------------------------------------------------
|
|
@@ -1349,3 +1353,157 @@ describe("cross-cutting path surface", () => {
|
|
|
1349
1353
|
}
|
|
1350
1354
|
});
|
|
1351
1355
|
});
|
|
1356
|
+
|
|
1357
|
+
// ---------------------------------------------------------------------------
|
|
1358
|
+
// configureForCwd and agentDir construction
|
|
1359
|
+
// ---------------------------------------------------------------------------
|
|
1360
|
+
|
|
1361
|
+
describe("PermissionManager — configureForCwd and agentDir option", () => {
|
|
1362
|
+
/**
|
|
1363
|
+
* Build a temp agentDir with a global config and an optional cwd with a
|
|
1364
|
+
* project config. Returns the paths and a cleanup function.
|
|
1365
|
+
*/
|
|
1366
|
+
function makeAgentDirSetup(opts: {
|
|
1367
|
+
globalPermission: Record<string, unknown>;
|
|
1368
|
+
projectPermission?: Record<string, unknown>;
|
|
1369
|
+
}): {
|
|
1370
|
+
agentDir: string;
|
|
1371
|
+
cwd: string;
|
|
1372
|
+
globalConfigPath: string;
|
|
1373
|
+
projectConfigPath: string;
|
|
1374
|
+
cleanup: () => void;
|
|
1375
|
+
} {
|
|
1376
|
+
const baseDir = mkdtempSync(join(tmpdir(), "pm-agent-dir-test-"));
|
|
1377
|
+
const agentDir = join(baseDir, "agent");
|
|
1378
|
+
const cwd = join(baseDir, "project");
|
|
1379
|
+
|
|
1380
|
+
// Write global config under getGlobalConfigPath(agentDir)
|
|
1381
|
+
const globalConfigPath = getGlobalConfigPath(agentDir);
|
|
1382
|
+
mkdirSync(join(agentDir, "extensions", "pi-permission-system"), {
|
|
1383
|
+
recursive: true,
|
|
1384
|
+
});
|
|
1385
|
+
writeFileSync(
|
|
1386
|
+
globalConfigPath,
|
|
1387
|
+
JSON.stringify({ permission: opts.globalPermission }, null, 2),
|
|
1388
|
+
);
|
|
1389
|
+
|
|
1390
|
+
// Write project config under getProjectConfigPath(cwd)
|
|
1391
|
+
const projectConfigPath = getProjectConfigPath(cwd);
|
|
1392
|
+
mkdirSync(join(cwd, ".pi", "extensions", "pi-permission-system"), {
|
|
1393
|
+
recursive: true,
|
|
1394
|
+
});
|
|
1395
|
+
if (opts.projectPermission) {
|
|
1396
|
+
writeFileSync(
|
|
1397
|
+
projectConfigPath,
|
|
1398
|
+
JSON.stringify({ permission: opts.projectPermission }, null, 2),
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
return {
|
|
1403
|
+
agentDir,
|
|
1404
|
+
cwd,
|
|
1405
|
+
globalConfigPath,
|
|
1406
|
+
projectConfigPath,
|
|
1407
|
+
cleanup: () => rmSync(baseDir, { recursive: true, force: true }),
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
it("ScopedPermissionManager is exported and PermissionManager satisfies it", () => {
|
|
1412
|
+
// Type-level assertion: assigning PermissionManager to ScopedPermissionManager compiles.
|
|
1413
|
+
const manager = new PermissionManager({
|
|
1414
|
+
globalConfigPath: "/nonexistent/config.json",
|
|
1415
|
+
agentsDir: "/nonexistent/agents",
|
|
1416
|
+
});
|
|
1417
|
+
const scoped: ScopedPermissionManager = manager;
|
|
1418
|
+
expect(typeof scoped.configureForCwd).toBe("function");
|
|
1419
|
+
expect(typeof scoped.checkPermission).toBe("function");
|
|
1420
|
+
expect(typeof scoped.getToolPermission).toBe("function");
|
|
1421
|
+
expect(typeof scoped.getConfigIssues).toBe("function");
|
|
1422
|
+
expect(typeof scoped.getPolicyCacheStamp).toBe("function");
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
it("construction with { agentDir } reads global config from getGlobalConfigPath(agentDir)", () => {
|
|
1426
|
+
const { agentDir, cleanup } = makeAgentDirSetup({
|
|
1427
|
+
globalPermission: { read: "deny" },
|
|
1428
|
+
});
|
|
1429
|
+
try {
|
|
1430
|
+
const manager = new PermissionManager({ agentDir });
|
|
1431
|
+
const result = manager.checkPermission("read", { path: "foo.txt" });
|
|
1432
|
+
expect(result.state).toBe("deny");
|
|
1433
|
+
} finally {
|
|
1434
|
+
cleanup();
|
|
1435
|
+
}
|
|
1436
|
+
});
|
|
1437
|
+
|
|
1438
|
+
it("configureForCwd(cwd) applies project config (project overrides global)", () => {
|
|
1439
|
+
const { agentDir, cwd, cleanup } = makeAgentDirSetup({
|
|
1440
|
+
globalPermission: { read: "deny" },
|
|
1441
|
+
projectPermission: { read: "allow" },
|
|
1442
|
+
});
|
|
1443
|
+
try {
|
|
1444
|
+
const manager = new PermissionManager({ agentDir });
|
|
1445
|
+
// Before configureForCwd: global policy applies
|
|
1446
|
+
expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
|
|
1447
|
+
"deny",
|
|
1448
|
+
);
|
|
1449
|
+
|
|
1450
|
+
manager.configureForCwd(cwd);
|
|
1451
|
+
|
|
1452
|
+
// After configureForCwd: project override applies (last-match-wins)
|
|
1453
|
+
expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
|
|
1454
|
+
"allow",
|
|
1455
|
+
);
|
|
1456
|
+
} finally {
|
|
1457
|
+
cleanup();
|
|
1458
|
+
}
|
|
1459
|
+
});
|
|
1460
|
+
|
|
1461
|
+
it("configureForCwd(undefined) reverts to global-only", () => {
|
|
1462
|
+
const { agentDir, cwd, cleanup } = makeAgentDirSetup({
|
|
1463
|
+
globalPermission: { read: "deny" },
|
|
1464
|
+
projectPermission: { read: "allow" },
|
|
1465
|
+
});
|
|
1466
|
+
try {
|
|
1467
|
+
const manager = new PermissionManager({ agentDir });
|
|
1468
|
+
manager.configureForCwd(cwd);
|
|
1469
|
+
expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
|
|
1470
|
+
"allow",
|
|
1471
|
+
);
|
|
1472
|
+
|
|
1473
|
+
manager.configureForCwd(undefined);
|
|
1474
|
+
|
|
1475
|
+
// After reverting: global policy applies again
|
|
1476
|
+
expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
|
|
1477
|
+
"deny",
|
|
1478
|
+
);
|
|
1479
|
+
} finally {
|
|
1480
|
+
cleanup();
|
|
1481
|
+
}
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
it("configureForCwd clears the resolved-permissions cache", () => {
|
|
1485
|
+
const { agentDir, globalConfigPath, cleanup } = makeAgentDirSetup({
|
|
1486
|
+
globalPermission: { read: "allow" },
|
|
1487
|
+
});
|
|
1488
|
+
try {
|
|
1489
|
+
const manager = new PermissionManager({ agentDir });
|
|
1490
|
+
// Warm the cache
|
|
1491
|
+
expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
|
|
1492
|
+
"allow",
|
|
1493
|
+
);
|
|
1494
|
+
// Update global config on disk to deny read
|
|
1495
|
+
writeFileSync(
|
|
1496
|
+
globalConfigPath,
|
|
1497
|
+
JSON.stringify({ permission: { read: "deny" } }, null, 2),
|
|
1498
|
+
);
|
|
1499
|
+
// configureForCwd clears cache + rebuilds loader
|
|
1500
|
+
manager.configureForCwd(undefined);
|
|
1501
|
+
// Should pick up the changed global config
|
|
1502
|
+
expect(manager.checkPermission("read", { path: "foo.txt" }).state).toBe(
|
|
1503
|
+
"deny",
|
|
1504
|
+
);
|
|
1505
|
+
} finally {
|
|
1506
|
+
cleanup();
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
@@ -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");
|
|
@@ -220,8 +217,7 @@ describe("PermissionSession", () => {
|
|
|
220
217
|
describe("resolve", () => {
|
|
221
218
|
it("forwards surface, input, and agentName, applying the empty session ruleset", () => {
|
|
222
219
|
const pm = makePermissionManager();
|
|
223
|
-
|
|
224
|
-
const { session } = createSession();
|
|
220
|
+
const { session } = createSession({ permissionManager: pm });
|
|
225
221
|
|
|
226
222
|
session.resolve("bash", { command: "ls" }, "agent-x");
|
|
227
223
|
|
|
@@ -235,8 +231,7 @@ describe("PermissionSession", () => {
|
|
|
235
231
|
|
|
236
232
|
it("defaults agentName to undefined when omitted", () => {
|
|
237
233
|
const pm = makePermissionManager();
|
|
238
|
-
|
|
239
|
-
const { session } = createSession();
|
|
234
|
+
const { session } = createSession({ permissionManager: pm });
|
|
240
235
|
|
|
241
236
|
session.resolve("read", { path: ".env" });
|
|
242
237
|
|
|
@@ -250,8 +245,7 @@ describe("PermissionSession", () => {
|
|
|
250
245
|
|
|
251
246
|
it("applies a recorded session approval on the next resolve", () => {
|
|
252
247
|
const pm = makePermissionManager();
|
|
253
|
-
|
|
254
|
-
const { session } = createSession();
|
|
248
|
+
const { session } = createSession({ permissionManager: pm });
|
|
255
249
|
|
|
256
250
|
session.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
257
251
|
session.resolve("bash", { command: "git status" });
|
|
@@ -266,17 +260,15 @@ describe("PermissionSession", () => {
|
|
|
266
260
|
});
|
|
267
261
|
|
|
268
262
|
it("returns the PermissionManager's check result", () => {
|
|
269
|
-
const pm = makePermissionManager(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}),
|
|
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 *",
|
|
277
270
|
});
|
|
278
|
-
|
|
279
|
-
const { session } = createSession();
|
|
271
|
+
const { session } = createSession({ permissionManager: pm });
|
|
280
272
|
|
|
281
273
|
const result = session.resolve("bash", { command: "rm -rf /" });
|
|
282
274
|
|
|
@@ -310,28 +302,14 @@ describe("PermissionSession", () => {
|
|
|
310
302
|
});
|
|
311
303
|
|
|
312
304
|
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();
|
|
305
|
+
it("configures the injected PermissionManager for the context cwd", () => {
|
|
306
|
+
const pm = makePermissionManager();
|
|
307
|
+
const { session } = createSession({ permissionManager: pm });
|
|
324
308
|
const ctx = makeCtx({ cwd: "/new/project" });
|
|
325
309
|
|
|
326
310
|
session.resetForNewSession(ctx);
|
|
327
311
|
|
|
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");
|
|
312
|
+
expect(pm.configureForCwd).toHaveBeenCalledWith("/new/project");
|
|
335
313
|
});
|
|
336
314
|
|
|
337
315
|
it("clears cache keys", () => {
|
|
@@ -573,20 +551,15 @@ describe("PermissionSession", () => {
|
|
|
573
551
|
});
|
|
574
552
|
|
|
575
553
|
describe("reload", () => {
|
|
576
|
-
it("
|
|
577
|
-
const
|
|
554
|
+
it("configures PermissionManager for current context cwd", () => {
|
|
555
|
+
const pm = makePermissionManager();
|
|
556
|
+
const { session } = createSession({ permissionManager: pm });
|
|
578
557
|
const ctx = makeCtx({ cwd: "/project" });
|
|
579
558
|
session.activate(ctx);
|
|
580
559
|
|
|
581
|
-
const pm2 = makePermissionManager();
|
|
582
|
-
mockCreatePermissionManagerForCwd.mockReturnValue(pm2);
|
|
583
|
-
|
|
584
560
|
session.reload();
|
|
585
561
|
|
|
586
|
-
expect(
|
|
587
|
-
"/test/agent",
|
|
588
|
-
"/project",
|
|
589
|
-
);
|
|
562
|
+
expect(pm.configureForCwd).toHaveBeenCalledWith("/project");
|
|
590
563
|
});
|
|
591
564
|
|
|
592
565
|
it("clears caches and skill entries", () => {
|
package/test/runtime.test.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
2
|
|
|
4
3
|
// ── logger stub ────────────────────────────────────────────────────────────
|
|
@@ -63,19 +62,9 @@ vi.mock("../src/session-rules", () => ({
|
|
|
63
62
|
}));
|
|
64
63
|
|
|
65
64
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
66
|
-
import {
|
|
67
|
-
getGlobalConfigPath,
|
|
68
|
-
getGlobalLogsDir,
|
|
69
|
-
getProjectConfigPath,
|
|
70
|
-
} from "#src/config-paths";
|
|
65
|
+
import { getGlobalLogsDir } from "#src/config-paths";
|
|
71
66
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
72
|
-
import {
|
|
73
|
-
import {
|
|
74
|
-
createExtensionRuntime,
|
|
75
|
-
createPermissionManagerForCwd,
|
|
76
|
-
derivePiProjectPaths,
|
|
77
|
-
refreshExtensionConfig,
|
|
78
|
-
} from "#src/runtime";
|
|
67
|
+
import { createExtensionRuntime, refreshExtensionConfig } from "#src/runtime";
|
|
79
68
|
|
|
80
69
|
// ── test suite ─────────────────────────────────────────────────────────────
|
|
81
70
|
|
|
@@ -351,75 +340,6 @@ describe("createExtensionRuntime", () => {
|
|
|
351
340
|
});
|
|
352
341
|
});
|
|
353
342
|
|
|
354
|
-
// ── derivePiProjectPaths ───────────────────────────────────────────────────
|
|
355
|
-
|
|
356
|
-
describe("derivePiProjectPaths", () => {
|
|
357
|
-
it("returns null for null cwd", () => {
|
|
358
|
-
expect(derivePiProjectPaths(null)).toBeNull();
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it("returns null for undefined cwd", () => {
|
|
362
|
-
expect(derivePiProjectPaths(undefined)).toBeNull();
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it("returns null for empty string cwd", () => {
|
|
366
|
-
expect(derivePiProjectPaths("")).toBeNull();
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
it("returns projectGlobalConfigPath via getProjectConfigPath", () => {
|
|
370
|
-
const result = derivePiProjectPaths("/my/project");
|
|
371
|
-
expect(result?.projectGlobalConfigPath).toBe(
|
|
372
|
-
getProjectConfigPath("/my/project"),
|
|
373
|
-
);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
it("returns projectAgentsDir as .pi/agent/agents under cwd", () => {
|
|
377
|
-
const result = derivePiProjectPaths("/my/project");
|
|
378
|
-
expect(result?.projectAgentsDir).toBe(
|
|
379
|
-
join("/my/project", ".pi", "agent", "agents"),
|
|
380
|
-
);
|
|
381
|
-
});
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// ── createPermissionManagerForCwd ─────────────────────────────────────────
|
|
385
|
-
|
|
386
|
-
describe("createPermissionManagerForCwd", () => {
|
|
387
|
-
beforeEach(() => {
|
|
388
|
-
// PermissionManager is already mocked as vi.fn() at module scope.
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it("creates a PermissionManager with globalConfigPath from agentDir", () => {
|
|
392
|
-
const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
|
|
393
|
-
MockPM.mockClear();
|
|
394
|
-
createPermissionManagerForCwd("/test/agent", null);
|
|
395
|
-
expect(MockPM).toHaveBeenCalledWith(
|
|
396
|
-
expect.objectContaining({
|
|
397
|
-
globalConfigPath: getGlobalConfigPath("/test/agent"),
|
|
398
|
-
}),
|
|
399
|
-
);
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it("includes projectGlobalConfigPath when cwd is provided", () => {
|
|
403
|
-
const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
|
|
404
|
-
MockPM.mockClear();
|
|
405
|
-
createPermissionManagerForCwd("/test/agent", "/my/project");
|
|
406
|
-
expect(MockPM).toHaveBeenCalledWith(
|
|
407
|
-
expect.objectContaining({
|
|
408
|
-
globalConfigPath: getGlobalConfigPath("/test/agent"),
|
|
409
|
-
projectGlobalConfigPath: getProjectConfigPath("/my/project"),
|
|
410
|
-
}),
|
|
411
|
-
);
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
it("excludes projectGlobalConfigPath when cwd is null", () => {
|
|
415
|
-
const MockPM = PermissionManager as ReturnType<typeof vi.fn>;
|
|
416
|
-
MockPM.mockClear();
|
|
417
|
-
createPermissionManagerForCwd("/test/agent", null);
|
|
418
|
-
const callArg = MockPM.mock.calls[0][0] as Record<string, unknown>;
|
|
419
|
-
expect(callArg.projectGlobalConfigPath).toBeUndefined();
|
|
420
|
-
});
|
|
421
|
-
});
|
|
422
|
-
|
|
423
343
|
// ── refreshExtensionConfig ────────────────────────────────────────────────
|
|
424
344
|
|
|
425
345
|
describe("refreshExtensionConfig", () => {
|