@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
|
@@ -1,14 +1,5 @@
|
|
|
1
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
5
1
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
6
2
|
import {
|
|
7
|
-
confirmPermission,
|
|
8
|
-
processForwardedPermissionRequests,
|
|
9
|
-
} from "#src/forwarded-permissions/polling";
|
|
10
|
-
import {
|
|
11
|
-
createPermissionForwardingLocation,
|
|
12
3
|
resolvePermissionForwardingTargetSessionId,
|
|
13
4
|
SUBAGENT_PARENT_SESSION_ENV_CANDIDATES,
|
|
14
5
|
SUBAGENT_PARENT_SESSION_ENV_KEY,
|
|
@@ -249,276 +240,3 @@ describe("resolvePermissionForwardingTargetSessionId — registry resolution", (
|
|
|
249
240
|
).toBe("parent-from-env");
|
|
250
241
|
});
|
|
251
242
|
});
|
|
252
|
-
|
|
253
|
-
describe("processForwardedPermissionRequests", () => {
|
|
254
|
-
test("emits a UI prompt event before showing a forwarded permission dialog", async () => {
|
|
255
|
-
const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
|
|
256
|
-
try {
|
|
257
|
-
const forwardingDir = join(root, "forwarding");
|
|
258
|
-
const location = createPermissionForwardingLocation(
|
|
259
|
-
forwardingDir,
|
|
260
|
-
"parent-session",
|
|
261
|
-
);
|
|
262
|
-
mkdirSync(location.requestsDir, { recursive: true });
|
|
263
|
-
mkdirSync(location.responsesDir, { recursive: true });
|
|
264
|
-
writeFileSync(
|
|
265
|
-
join(location.requestsDir, "req-forwarded.json"),
|
|
266
|
-
JSON.stringify({
|
|
267
|
-
id: "req-forwarded",
|
|
268
|
-
createdAt: Date.now(),
|
|
269
|
-
requesterSessionId: "child-session",
|
|
270
|
-
targetSessionId: "parent-session",
|
|
271
|
-
requesterAgentName: "Explore",
|
|
272
|
-
message: "Allow git push?",
|
|
273
|
-
}),
|
|
274
|
-
"utf-8",
|
|
275
|
-
);
|
|
276
|
-
|
|
277
|
-
const events = {
|
|
278
|
-
emit: vi.fn(),
|
|
279
|
-
on: vi.fn().mockReturnValue(() => undefined),
|
|
280
|
-
};
|
|
281
|
-
const requestPermissionDecisionFromUi = vi
|
|
282
|
-
.fn()
|
|
283
|
-
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
284
|
-
|
|
285
|
-
await processForwardedPermissionRequests(
|
|
286
|
-
{
|
|
287
|
-
hasUI: true,
|
|
288
|
-
ui: { select: vi.fn(), input: vi.fn() },
|
|
289
|
-
sessionManager: {
|
|
290
|
-
getSessionId: vi.fn().mockReturnValue("parent-session"),
|
|
291
|
-
},
|
|
292
|
-
} as unknown as ExtensionContext,
|
|
293
|
-
{
|
|
294
|
-
forwardingDir,
|
|
295
|
-
subagentSessionsDir: join(root, "subagents"),
|
|
296
|
-
events,
|
|
297
|
-
logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
|
|
298
|
-
writeReviewLog: vi.fn(),
|
|
299
|
-
requestPermissionDecisionFromUi,
|
|
300
|
-
shouldAutoApprove: () => false,
|
|
301
|
-
},
|
|
302
|
-
);
|
|
303
|
-
|
|
304
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
305
|
-
"permissions:ui_prompt",
|
|
306
|
-
expect.objectContaining({
|
|
307
|
-
requestId: "req-forwarded",
|
|
308
|
-
source: "tool_call",
|
|
309
|
-
surface: null,
|
|
310
|
-
value: null,
|
|
311
|
-
agentName: "Explore",
|
|
312
|
-
message: expect.stringContaining("Allow git push?"),
|
|
313
|
-
forwarding: {
|
|
314
|
-
requesterAgentName: "Explore",
|
|
315
|
-
requesterSessionId: "child-session",
|
|
316
|
-
},
|
|
317
|
-
}),
|
|
318
|
-
);
|
|
319
|
-
expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
|
|
320
|
-
} finally {
|
|
321
|
-
rmSync(root, { recursive: true, force: true });
|
|
322
|
-
}
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
test("emits a non-degraded UI prompt event when the request carries display fields", async () => {
|
|
326
|
-
const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
|
|
327
|
-
try {
|
|
328
|
-
const forwardingDir = join(root, "forwarding");
|
|
329
|
-
const location = createPermissionForwardingLocation(
|
|
330
|
-
forwardingDir,
|
|
331
|
-
"parent-session",
|
|
332
|
-
);
|
|
333
|
-
mkdirSync(location.requestsDir, { recursive: true });
|
|
334
|
-
mkdirSync(location.responsesDir, { recursive: true });
|
|
335
|
-
writeFileSync(
|
|
336
|
-
join(location.requestsDir, "req-forwarded-rich.json"),
|
|
337
|
-
JSON.stringify({
|
|
338
|
-
id: "req-forwarded-rich",
|
|
339
|
-
createdAt: Date.now(),
|
|
340
|
-
requesterSessionId: "child-session",
|
|
341
|
-
targetSessionId: "parent-session",
|
|
342
|
-
requesterAgentName: "Explore",
|
|
343
|
-
message: "Allow git push?",
|
|
344
|
-
source: "tool_call",
|
|
345
|
-
surface: "bash",
|
|
346
|
-
value: "git push",
|
|
347
|
-
}),
|
|
348
|
-
"utf-8",
|
|
349
|
-
);
|
|
350
|
-
|
|
351
|
-
const events = {
|
|
352
|
-
emit: vi.fn(),
|
|
353
|
-
on: vi.fn().mockReturnValue(() => undefined),
|
|
354
|
-
};
|
|
355
|
-
const requestPermissionDecisionFromUi = vi
|
|
356
|
-
.fn()
|
|
357
|
-
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
358
|
-
|
|
359
|
-
await processForwardedPermissionRequests(
|
|
360
|
-
{
|
|
361
|
-
hasUI: true,
|
|
362
|
-
ui: { select: vi.fn(), input: vi.fn() },
|
|
363
|
-
sessionManager: {
|
|
364
|
-
getSessionId: vi.fn().mockReturnValue("parent-session"),
|
|
365
|
-
},
|
|
366
|
-
} as unknown as ExtensionContext,
|
|
367
|
-
{
|
|
368
|
-
forwardingDir,
|
|
369
|
-
subagentSessionsDir: join(root, "subagents"),
|
|
370
|
-
events,
|
|
371
|
-
logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
|
|
372
|
-
writeReviewLog: vi.fn(),
|
|
373
|
-
requestPermissionDecisionFromUi,
|
|
374
|
-
shouldAutoApprove: () => false,
|
|
375
|
-
},
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
379
|
-
"permissions:ui_prompt",
|
|
380
|
-
expect.objectContaining({
|
|
381
|
-
requestId: "req-forwarded-rich",
|
|
382
|
-
source: "tool_call",
|
|
383
|
-
surface: "bash",
|
|
384
|
-
value: "git push",
|
|
385
|
-
agentName: "Explore",
|
|
386
|
-
message: expect.stringContaining("Allow git push?"),
|
|
387
|
-
forwarding: {
|
|
388
|
-
requesterAgentName: "Explore",
|
|
389
|
-
requesterSessionId: "child-session",
|
|
390
|
-
},
|
|
391
|
-
}),
|
|
392
|
-
);
|
|
393
|
-
expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
|
|
394
|
-
} finally {
|
|
395
|
-
rmSync(root, { recursive: true, force: true });
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("does not emit a UI prompt event when forwarded permission auto-approves", async () => {
|
|
400
|
-
const root = mkdtempSync(join(tmpdir(), "permission-forwarding-"));
|
|
401
|
-
try {
|
|
402
|
-
const forwardingDir = join(root, "forwarding");
|
|
403
|
-
const location = createPermissionForwardingLocation(
|
|
404
|
-
forwardingDir,
|
|
405
|
-
"parent-session",
|
|
406
|
-
);
|
|
407
|
-
mkdirSync(location.requestsDir, { recursive: true });
|
|
408
|
-
mkdirSync(location.responsesDir, { recursive: true });
|
|
409
|
-
writeFileSync(
|
|
410
|
-
join(location.requestsDir, "req-forwarded-auto.json"),
|
|
411
|
-
JSON.stringify({
|
|
412
|
-
id: "req-forwarded-auto",
|
|
413
|
-
createdAt: Date.now(),
|
|
414
|
-
requesterSessionId: "child-session",
|
|
415
|
-
targetSessionId: "parent-session",
|
|
416
|
-
requesterAgentName: "Explore",
|
|
417
|
-
message: "Allow git push?",
|
|
418
|
-
}),
|
|
419
|
-
"utf-8",
|
|
420
|
-
);
|
|
421
|
-
|
|
422
|
-
const events = {
|
|
423
|
-
emit: vi.fn(),
|
|
424
|
-
on: vi.fn().mockReturnValue(() => undefined),
|
|
425
|
-
};
|
|
426
|
-
const requestPermissionDecisionFromUi = vi.fn();
|
|
427
|
-
|
|
428
|
-
await processForwardedPermissionRequests(
|
|
429
|
-
{
|
|
430
|
-
hasUI: true,
|
|
431
|
-
ui: { select: vi.fn(), input: vi.fn() },
|
|
432
|
-
sessionManager: {
|
|
433
|
-
getSessionId: vi.fn().mockReturnValue("parent-session"),
|
|
434
|
-
},
|
|
435
|
-
} as unknown as ExtensionContext,
|
|
436
|
-
{
|
|
437
|
-
forwardingDir,
|
|
438
|
-
subagentSessionsDir: join(root, "subagents"),
|
|
439
|
-
events,
|
|
440
|
-
logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
|
|
441
|
-
writeReviewLog: vi.fn(),
|
|
442
|
-
requestPermissionDecisionFromUi,
|
|
443
|
-
shouldAutoApprove: () => true,
|
|
444
|
-
},
|
|
445
|
-
);
|
|
446
|
-
|
|
447
|
-
expect(events.emit).not.toHaveBeenCalledWith(
|
|
448
|
-
"permissions:ui_prompt",
|
|
449
|
-
expect.anything(),
|
|
450
|
-
);
|
|
451
|
-
expect(requestPermissionDecisionFromUi).not.toHaveBeenCalled();
|
|
452
|
-
} finally {
|
|
453
|
-
rmSync(root, { recursive: true, force: true });
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
describe("confirmPermission", () => {
|
|
459
|
-
test("shows the UI dialog but does not emit a UI prompt event (the prompter does)", async () => {
|
|
460
|
-
const events = {
|
|
461
|
-
emit: vi.fn(),
|
|
462
|
-
on: vi.fn().mockReturnValue(() => undefined),
|
|
463
|
-
};
|
|
464
|
-
const requestPermissionDecisionFromUi = vi
|
|
465
|
-
.fn()
|
|
466
|
-
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
467
|
-
|
|
468
|
-
await confirmPermission(
|
|
469
|
-
{
|
|
470
|
-
hasUI: true,
|
|
471
|
-
ui: { select: vi.fn(), input: vi.fn() },
|
|
472
|
-
} as unknown as ExtensionContext,
|
|
473
|
-
"Allow git push?",
|
|
474
|
-
{
|
|
475
|
-
forwardingDir: "/tmp/forwarding",
|
|
476
|
-
subagentSessionsDir: "/tmp/subagents",
|
|
477
|
-
events,
|
|
478
|
-
logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
|
|
479
|
-
writeReviewLog: vi.fn(),
|
|
480
|
-
requestPermissionDecisionFromUi,
|
|
481
|
-
shouldAutoApprove: () => false,
|
|
482
|
-
},
|
|
483
|
-
);
|
|
484
|
-
|
|
485
|
-
expect(requestPermissionDecisionFromUi).toHaveBeenCalled();
|
|
486
|
-
expect(events.emit).not.toHaveBeenCalledWith(
|
|
487
|
-
"permissions:ui_prompt",
|
|
488
|
-
expect.anything(),
|
|
489
|
-
);
|
|
490
|
-
});
|
|
491
|
-
|
|
492
|
-
test("does not show a dialog or emit when there is no active UI", async () => {
|
|
493
|
-
const events = {
|
|
494
|
-
emit: vi.fn(),
|
|
495
|
-
on: vi.fn().mockReturnValue(() => undefined),
|
|
496
|
-
};
|
|
497
|
-
const requestPermissionDecisionFromUi = vi.fn();
|
|
498
|
-
|
|
499
|
-
await confirmPermission(
|
|
500
|
-
{
|
|
501
|
-
hasUI: false,
|
|
502
|
-
sessionManager: {
|
|
503
|
-
getSessionDir: vi.fn().mockReturnValue(null),
|
|
504
|
-
},
|
|
505
|
-
} as unknown as ExtensionContext,
|
|
506
|
-
"Allow git push?",
|
|
507
|
-
{
|
|
508
|
-
forwardingDir: "/tmp/forwarding",
|
|
509
|
-
subagentSessionsDir: "/tmp/subagents",
|
|
510
|
-
events,
|
|
511
|
-
logger: { writeReviewLog: vi.fn(), writeDebugLog: vi.fn() },
|
|
512
|
-
writeReviewLog: vi.fn(),
|
|
513
|
-
requestPermissionDecisionFromUi,
|
|
514
|
-
shouldAutoApprove: () => false,
|
|
515
|
-
},
|
|
516
|
-
);
|
|
517
|
-
|
|
518
|
-
expect(events.emit).not.toHaveBeenCalledWith(
|
|
519
|
-
"permissions:ui_prompt",
|
|
520
|
-
expect.anything(),
|
|
521
|
-
);
|
|
522
|
-
expect(requestPermissionDecisionFromUi).not.toHaveBeenCalled();
|
|
523
|
-
});
|
|
524
|
-
});
|
|
@@ -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
|
+
});
|