@gotgenes/pi-permission-system 17.1.0 → 18.0.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +1 -0
  3. package/package.json +1 -1
  4. package/src/access-intent/access-intent.ts +6 -3
  5. package/src/access-intent/bash/bash-path-resolver.ts +16 -12
  6. package/src/access-intent/bash/program.ts +0 -4
  7. package/src/access-intent/bash/token-classification.ts +23 -0
  8. package/src/handlers/before-agent-start.ts +1 -2
  9. package/src/handlers/gates/external-directory.ts +2 -12
  10. package/src/handlers/gates/skill-read.ts +4 -8
  11. package/src/handlers/gates/tool-call-gate-pipeline.ts +62 -24
  12. package/src/handlers/gates/tool.ts +10 -14
  13. package/src/index.ts +8 -6
  14. package/src/input-normalizer.ts +54 -56
  15. package/src/path-normalizer.ts +30 -0
  16. package/src/permission-event-rpc.ts +21 -15
  17. package/src/permission-manager.ts +3 -6
  18. package/src/permission-session.ts +0 -5
  19. package/src/permissions-service.ts +33 -12
  20. package/src/skill-prompt-sanitizer.ts +8 -21
  21. package/test/access-intent/bash/program.test.ts +1 -1
  22. package/test/access-intent/bash/token-classification.test.ts +75 -0
  23. package/test/bash-external-directory.test.ts +38 -0
  24. package/test/composition-root.test.ts +22 -0
  25. package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -3
  26. package/test/handlers/gates/external-directory.test.ts +0 -1
  27. package/test/handlers/gates/skill-read.test.ts +16 -12
  28. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +73 -0
  29. package/test/handlers/gates/tool.test.ts +25 -16
  30. package/test/helpers/gate-fixtures.ts +0 -3
  31. package/test/input-normalizer.test.ts +163 -270
  32. package/test/path-normalizer.test.ts +43 -0
  33. package/test/path-utils.test.ts +1 -1
  34. package/test/permission-event-rpc.test.ts +80 -65
  35. package/test/permission-manager-unified.test.ts +134 -145
  36. package/test/permissions-service.test.ts +84 -72
  37. package/test/service.test.ts +56 -103
  38. package/test/skill-prompt-sanitizer.test.ts +31 -65
@@ -14,6 +14,7 @@ import {
14
14
  getProjectAgentsDir,
15
15
  getProjectConfigPath,
16
16
  } from "#src/config-paths";
17
+ import { getPathPolicyValues } from "#src/path-utils";
17
18
  import {
18
19
  PermissionManager,
19
20
  type ScopedPermissionManager,
@@ -140,6 +141,23 @@ function checkPathValues(
140
141
  );
141
142
  }
142
143
 
144
+ function checkPath(
145
+ manager: PermissionManager,
146
+ path: string,
147
+ opts: { cwd?: string } = {},
148
+ surface = "path",
149
+ agentName?: string,
150
+ sessionRules?: Ruleset,
151
+ ): PermissionCheckResult {
152
+ return checkPathValues(
153
+ manager,
154
+ getPathPolicyValues(path, opts.cwd ? { cwd: opts.cwd } : {}, "linux"),
155
+ agentName,
156
+ sessionRules,
157
+ surface,
158
+ );
159
+ }
160
+
143
161
  // ---------------------------------------------------------------------------
144
162
  // Step 5: session rules concatenated — wins over config/default
145
163
  // ---------------------------------------------------------------------------
@@ -150,10 +168,11 @@ describe("checkPermission — session rules", () => {
150
168
  const sessionRules: Ruleset = [
151
169
  sessionAllow("external_directory", "/other/project"),
152
170
  ];
153
- const result = checkTool(
171
+ const result = checkPath(
154
172
  manager,
173
+ "/other/project",
174
+ {},
155
175
  "external_directory",
156
- { path: "/other/project" },
157
176
  undefined,
158
177
  sessionRules,
159
178
  );
@@ -266,9 +285,12 @@ describe("checkPermission — source derivation and matchedPattern", () => {
266
285
  external_directory: { "/trusted/*": "allow" },
267
286
  });
268
287
  try {
269
- const result = checkTool(manager, "external_directory", {
270
- path: "/trusted/repo",
271
- });
288
+ const result = checkPath(
289
+ manager,
290
+ "/trusted/repo",
291
+ {},
292
+ "external_directory",
293
+ );
272
294
  expect(result.state).toBe("allow");
273
295
  expect(result.source).toBe("special");
274
296
  expect(result.matchedPattern).toBe("/trusted/*");
@@ -279,9 +301,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
279
301
 
280
302
  it("source is 'special' even for a default match (no config rule)", () => {
281
303
  const manager = makeManager();
282
- const result = checkTool(manager, "external_directory", {
283
- path: "/some/path",
284
- });
304
+ const result = checkPath(manager, "/some/path", {}, "external_directory");
285
305
  expect(result.state).toBe("ask");
286
306
  expect(result.source).toBe("special");
287
307
  expect(result.matchedPattern).toBeUndefined();
@@ -289,9 +309,7 @@ describe("checkPermission — source derivation and matchedPattern", () => {
289
309
 
290
310
  it("matchedPattern is undefined for a default match", () => {
291
311
  const manager = makeManager();
292
- const result = checkTool(manager, "external_directory", {
293
- path: "/unknown",
294
- });
312
+ const result = checkPath(manager, "/unknown", {}, "external_directory");
295
313
  expect(result.matchedPattern).toBeUndefined();
296
314
  });
297
315
  });
@@ -476,9 +494,12 @@ describe("checkPermission — home path expansion in external_directory rules",
476
494
  external_directory: { "~/trusted/*": "allow" },
477
495
  });
478
496
  try {
479
- const result = checkTool(manager, "external_directory", {
480
- path: join(homedir(), "trusted/repo"),
481
- });
497
+ const result = checkPath(
498
+ manager,
499
+ join(homedir(), "trusted/repo"),
500
+ {},
501
+ "external_directory",
502
+ );
482
503
  expect(result.state).toBe("allow");
483
504
  expect(result.source).toBe("special");
484
505
  expect(result.matchedPattern).toBe("~/trusted/*");
@@ -493,9 +514,12 @@ describe("checkPermission — home path expansion in external_directory rules",
493
514
  external_directory: { "$HOME/trusted/*": "allow" },
494
515
  });
495
516
  try {
496
- const result = checkTool(manager, "external_directory", {
497
- path: join(homedir(), "trusted/repo"),
498
- });
517
+ const result = checkPath(
518
+ manager,
519
+ join(homedir(), "trusted/repo"),
520
+ {},
521
+ "external_directory",
522
+ );
499
523
  expect(result.state).toBe("allow");
500
524
  expect(result.source).toBe("special");
501
525
  expect(result.matchedPattern).toBe("$HOME/trusted/*");
@@ -510,9 +534,12 @@ describe("checkPermission — home path expansion in external_directory rules",
510
534
  external_directory: { "~/private/*": "deny" },
511
535
  });
512
536
  try {
513
- const result = checkTool(manager, "external_directory", {
514
- path: join(homedir(), "private/secrets.txt"),
515
- });
537
+ const result = checkPath(
538
+ manager,
539
+ join(homedir(), "private/secrets.txt"),
540
+ {},
541
+ "external_directory",
542
+ );
516
543
  expect(result.state).toBe("deny");
517
544
  expect(result.matchedPattern).toBe("~/private/*");
518
545
  } finally {
@@ -526,9 +553,12 @@ describe("checkPermission — home path expansion in external_directory rules",
526
553
  external_directory: { "~/trusted/*": "allow" },
527
554
  });
528
555
  try {
529
- const result = checkTool(manager, "external_directory", {
530
- path: "/tmp/not-home/file",
531
- });
556
+ const result = checkPath(
557
+ manager,
558
+ "/tmp/not-home/file",
559
+ {},
560
+ "external_directory",
561
+ );
532
562
  // Falls back to the "*": "ask" default — no allow from the ~/trusted/* rule.
533
563
  expect(result.state).toBe("ask");
534
564
  expect(result.matchedPattern).toBeUndefined();
@@ -896,9 +926,12 @@ describe("PermissionManager with in-memory PolicyLoader", () => {
896
926
  },
897
927
  },
898
928
  });
899
- const result = checkTool(manager, "external_directory", {
900
- path: "/trusted/repo",
901
- });
929
+ const result = checkPath(
930
+ manager,
931
+ "/trusted/repo",
932
+ {},
933
+ "external_directory",
934
+ );
902
935
  expect(result.state).toBe("allow");
903
936
  expect(result.source).toBe("special");
904
937
  });
@@ -1075,7 +1108,7 @@ describe("checkPermission — per-tool path patterns", () => {
1075
1108
  read: { "*": "allow", "*.env": "deny" },
1076
1109
  });
1077
1110
  try {
1078
- const result = checkTool(manager, "read", { path: ".env" });
1111
+ const result = checkPath(manager, ".env", {}, "read");
1079
1112
  expect(result.state).toBe("deny");
1080
1113
  expect(result.matchedPattern).toBe("*.env");
1081
1114
  } finally {
@@ -1088,9 +1121,7 @@ describe("checkPermission — per-tool path patterns", () => {
1088
1121
  read: { "*": "allow", "*.env": "deny" },
1089
1122
  });
1090
1123
  try {
1091
- const result = checkTool(manager, "read", {
1092
- path: "src/main.ts",
1093
- });
1124
+ const result = checkPath(manager, "src/main.ts", {}, "read");
1094
1125
  expect(result.state).toBe("allow");
1095
1126
  } finally {
1096
1127
  cleanup();
@@ -1102,9 +1133,7 @@ describe("checkPermission — per-tool path patterns", () => {
1102
1133
  write: { "*": "deny", "src/*": "allow" },
1103
1134
  });
1104
1135
  try {
1105
- const result = checkTool(manager, "write", {
1106
- path: "src/main.ts",
1107
- });
1136
+ const result = checkPath(manager, "src/main.ts", {}, "write");
1108
1137
  expect(result.state).toBe("allow");
1109
1138
  expect(result.matchedPattern).toBe("src/*");
1110
1139
  } finally {
@@ -1117,9 +1146,7 @@ describe("checkPermission — per-tool path patterns", () => {
1117
1146
  write: { "*": "deny", "src/*": "allow" },
1118
1147
  });
1119
1148
  try {
1120
- const result = checkTool(manager, "write", {
1121
- path: "vendor/lib.ts",
1122
- });
1149
+ const result = checkPath(manager, "vendor/lib.ts", {}, "write");
1123
1150
  expect(result.state).toBe("deny");
1124
1151
  } finally {
1125
1152
  cleanup();
@@ -1131,7 +1158,7 @@ describe("checkPermission — per-tool path patterns", () => {
1131
1158
  read: "allow",
1132
1159
  });
1133
1160
  try {
1134
- const result = checkTool(manager, "read", { path: ".env" });
1161
+ const result = checkPath(manager, ".env", {}, "read");
1135
1162
  expect(result.state).toBe("allow");
1136
1163
  } finally {
1137
1164
  cleanup();
@@ -1143,9 +1170,7 @@ describe("checkPermission — per-tool path patterns", () => {
1143
1170
  read: "deny",
1144
1171
  });
1145
1172
  try {
1146
- const result = checkTool(manager, "read", {
1147
- path: "src/main.ts",
1148
- });
1173
+ const result = checkPath(manager, "src/main.ts", {}, "read");
1149
1174
  expect(result.state).toBe("deny");
1150
1175
  } finally {
1151
1176
  cleanup();
@@ -1158,10 +1183,11 @@ describe("checkPermission — per-tool path patterns", () => {
1158
1183
  });
1159
1184
  try {
1160
1185
  const sessionRules: Ruleset = [sessionAllow("read", ".env")];
1161
- const result = checkTool(
1186
+ const result = checkPath(
1162
1187
  manager,
1188
+ ".env",
1189
+ {},
1163
1190
  "read",
1164
- { path: ".env" },
1165
1191
  undefined,
1166
1192
  sessionRules,
1167
1193
  );
@@ -1208,7 +1234,7 @@ describe("cross-cutting path surface", () => {
1208
1234
  read: "allow",
1209
1235
  });
1210
1236
  try {
1211
- const result = checkTool(manager, "path", { path: ".env" });
1237
+ const result = checkPath(manager, ".env");
1212
1238
  expect(result.state).toBe("deny");
1213
1239
  expect(result.matchedPattern).toBe("*.env");
1214
1240
  } finally {
@@ -1222,7 +1248,7 @@ describe("cross-cutting path surface", () => {
1222
1248
  read: "allow",
1223
1249
  });
1224
1250
  try {
1225
- const result = checkTool(manager, "path", { path: "README.md" });
1251
+ const result = checkPath(manager, "README.md");
1226
1252
  expect(result.state).toBe("allow");
1227
1253
  } finally {
1228
1254
  cleanup();
@@ -1236,14 +1262,10 @@ describe("cross-cutting path surface", () => {
1236
1262
  });
1237
1263
  try {
1238
1264
  // path surface allows, per-tool denies
1239
- const readResult = checkTool(manager, "read", {
1240
- path: "data.secret",
1241
- });
1265
+ const readResult = checkPath(manager, "data.secret", {}, "read");
1242
1266
  expect(readResult.state).toBe("deny");
1243
1267
  // path surface also allows
1244
- const pathResult = checkTool(manager, "path", {
1245
- path: "data.secret",
1246
- });
1268
+ const pathResult = checkPath(manager, "data.secret");
1247
1269
  expect(pathResult.state).toBe("allow");
1248
1270
  } finally {
1249
1271
  cleanup();
@@ -1268,10 +1290,11 @@ describe("cross-cutting path surface", () => {
1268
1290
  });
1269
1291
  try {
1270
1292
  const sessionRules: Ruleset = [sessionAllow("path", "/project/.env")];
1271
- const result = checkTool(
1293
+ const result = checkPath(
1272
1294
  manager,
1295
+ "/project/.env",
1296
+ {},
1273
1297
  "path",
1274
- { path: "/project/.env" },
1275
1298
  undefined,
1276
1299
  sessionRules,
1277
1300
  );
@@ -1288,7 +1311,7 @@ describe("cross-cutting path surface", () => {
1288
1311
  });
1289
1312
  try {
1290
1313
  // path surface falls through to universal default
1291
- const result = checkTool(manager, "path", { path: ".env" });
1314
+ const result = checkPath(manager, ".env");
1292
1315
  expect(result.state).toBe("ask");
1293
1316
  } finally {
1294
1317
  cleanup();
@@ -1304,16 +1327,12 @@ describe("cross-cutting path surface", () => {
1304
1327
  try {
1305
1328
  // No explicit "path" key → matchedPattern must be undefined so the
1306
1329
  // path gate skips (describePathGate returns null).
1307
- const result = checkTool(manager, "path", {
1308
- path: "src/main.ts",
1309
- });
1330
+ const result = checkPath(manager, "src/main.ts");
1310
1331
  expect(result.state).toBe("ask");
1311
1332
  expect(result.matchedPattern).toBeUndefined();
1312
1333
 
1313
1334
  // Meanwhile the tool-level check should allow read.
1314
- const readResult = checkTool(manager, "read", {
1315
- path: "src/main.ts",
1316
- });
1335
+ const readResult = checkPath(manager, "src/main.ts", {}, "read");
1317
1336
  expect(readResult.state).toBe("allow");
1318
1337
  expect(readResult.matchedPattern).toBe("*");
1319
1338
  } finally {
@@ -1362,7 +1381,7 @@ describe("cross-cutting path surface", () => {
1362
1381
  },
1363
1382
  });
1364
1383
  try {
1365
- const result = checkTool(manager, "read", { path: ".env" });
1384
+ const result = checkPath(manager, ".env", {}, "read");
1366
1385
  expect(result.state).toBe("deny");
1367
1386
  expect(result.reason).toBe("Environment files contain secrets");
1368
1387
  expect(result.matchedPattern).toBe("*.env");
@@ -1394,7 +1413,7 @@ describe("cross-cutting path surface", () => {
1394
1413
  path: { "*.env": "deny", "*": "allow" },
1395
1414
  });
1396
1415
  try {
1397
- const result = checkTool(manager, "path", { path: ".env" });
1416
+ const result = checkPath(manager, ".env");
1398
1417
  // "*" is last and matches .env → allow (the deny is shadowed)
1399
1418
  expect(result.state).toBe("allow");
1400
1419
  } finally {
@@ -1408,7 +1427,7 @@ describe("cross-cutting path surface", () => {
1408
1427
  path: { "*": "allow", "*.env": "deny" },
1409
1428
  });
1410
1429
  try {
1411
- const result = checkTool(manager, "path", { path: ".env" });
1430
+ const result = checkPath(manager, ".env");
1412
1431
  expect(result.state).toBe("deny");
1413
1432
  } finally {
1414
1433
  cleanup();
@@ -1427,22 +1446,12 @@ describe("cross-cutting path surface", () => {
1427
1446
  },
1428
1447
  });
1429
1448
  try {
1430
- expect(checkTool(manager, "path", { path: ".env" }).state).toBe("deny");
1431
- expect(checkTool(manager, "path", { path: ".env.local" }).state).toBe(
1432
- "deny",
1433
- );
1434
- expect(
1435
- checkTool(manager, "path", { path: ".env.production" }).state,
1436
- ).toBe("deny");
1437
- expect(checkTool(manager, "path", { path: "src/.env" }).state).toBe(
1438
- "deny",
1439
- );
1440
- expect(checkTool(manager, "path", { path: ".env.example" }).state).toBe(
1441
- "allow",
1442
- );
1443
- expect(checkTool(manager, "path", { path: "README.md" }).state).toBe(
1444
- "allow",
1445
- );
1449
+ expect(checkPath(manager, ".env").state).toBe("deny");
1450
+ expect(checkPath(manager, ".env.local").state).toBe("deny");
1451
+ expect(checkPath(manager, ".env.production").state).toBe("deny");
1452
+ expect(checkPath(manager, "src/.env").state).toBe("deny");
1453
+ expect(checkPath(manager, ".env.example").state).toBe("allow");
1454
+ expect(checkPath(manager, "README.md").state).toBe("allow");
1446
1455
  } finally {
1447
1456
  cleanup();
1448
1457
  }
@@ -1455,7 +1464,7 @@ describe("cross-cutting path surface", () => {
1455
1464
  "*": "allow",
1456
1465
  });
1457
1466
  try {
1458
- const result = checkTool(manager, "path", { path: ".env" });
1467
+ const result = checkPath(manager, ".env");
1459
1468
  expect(result.state).toBe("allow");
1460
1469
  } finally {
1461
1470
  cleanup();
@@ -1467,14 +1476,14 @@ describe("cross-cutting path surface", () => {
1467
1476
  "*": "deny",
1468
1477
  });
1469
1478
  try {
1470
- const result = checkTool(manager, "path", { path: ".env" });
1479
+ const result = checkPath(manager, ".env");
1471
1480
  expect(result.state).toBe("deny");
1472
1481
  } finally {
1473
1482
  cleanup();
1474
1483
  }
1475
1484
  });
1476
1485
 
1477
- // ── Composition: path allows, per-tool denies ──────────────────────────
1486
+ // ── Composition: path allows, per-tool denies ──────────────────────
1478
1487
 
1479
1488
  it("per-tool deny still blocks even when path surface allows", () => {
1480
1489
  const { manager, cleanup } = makeManagerWithConfig({
@@ -1483,13 +1492,9 @@ describe("cross-cutting path surface", () => {
1483
1492
  });
1484
1493
  try {
1485
1494
  // path gate passes (allow), but tool gate denies
1486
- const pathResult = checkTool(manager, "path", {
1487
- path: "secret.txt",
1488
- });
1495
+ const pathResult = checkPath(manager, "secret.txt");
1489
1496
  expect(pathResult.state).toBe("allow");
1490
- const readResult = checkTool(manager, "read", {
1491
- path: "secret.txt",
1492
- });
1497
+ const readResult = checkPath(manager, "secret.txt", {}, "read");
1493
1498
  expect(readResult.state).toBe("deny");
1494
1499
  } finally {
1495
1500
  cleanup();
@@ -1507,9 +1512,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
1507
1512
  path: { "*": "allow", "~/.ssh/*": "deny" },
1508
1513
  });
1509
1514
  try {
1510
- const result = checkTool(manager, "path", {
1511
- path: "~/.ssh/config",
1512
- });
1515
+ const result = checkPath(manager, "~/.ssh/config");
1513
1516
  expect(result.state).toBe("deny");
1514
1517
  expect(result.matchedPattern).toBe("~/.ssh/*");
1515
1518
  } finally {
@@ -1522,9 +1525,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
1522
1525
  path: { "*": "allow", "~/.ssh/*": "deny" },
1523
1526
  });
1524
1527
  try {
1525
- const result = checkTool(manager, "path", {
1526
- path: `${homedir()}/.ssh/config`,
1527
- });
1528
+ const result = checkPath(manager, `${homedir()}/.ssh/config`);
1528
1529
  expect(result.state).toBe("deny");
1529
1530
  expect(result.matchedPattern).toBe("~/.ssh/*");
1530
1531
  } finally {
@@ -1537,9 +1538,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
1537
1538
  path: { "*": "allow", "$HOME/.ssh/*": "deny" },
1538
1539
  });
1539
1540
  try {
1540
- const result = checkTool(manager, "path", {
1541
- path: "$HOME/.ssh/config",
1542
- });
1541
+ const result = checkPath(manager, "$HOME/.ssh/config");
1543
1542
  expect(result.state).toBe("deny");
1544
1543
  expect(result.matchedPattern).toBe("$HOME/.ssh/*");
1545
1544
  } finally {
@@ -1552,9 +1551,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
1552
1551
  path: { "*": "allow", "~/.ssh/*": "deny" },
1553
1552
  });
1554
1553
  try {
1555
- const result = checkTool(manager, "path", {
1556
- path: `${homedir()}/.ssh/config`,
1557
- });
1554
+ const result = checkPath(manager, `${homedir()}/.ssh/config`);
1558
1555
  expect(result.state).toBe("deny");
1559
1556
  } finally {
1560
1557
  cleanup();
@@ -1566,7 +1563,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
1566
1563
  path: { "*": "allow", "*.env": "deny" },
1567
1564
  });
1568
1565
  try {
1569
- const result = checkTool(manager, "path", { path: ".env" });
1566
+ const result = checkPath(manager, ".env");
1570
1567
  expect(result.state).toBe("deny");
1571
1568
  expect(result.matchedPattern).toBe("*.env");
1572
1569
  } finally {
@@ -1580,9 +1577,7 @@ describe("cross-cutting path surface — home-expanded values", () => {
1580
1577
  read: { "*": "allow", "~/.ssh/*": "deny" },
1581
1578
  });
1582
1579
  try {
1583
- const result = checkTool(manager, "read", {
1584
- path: "~/.ssh/config",
1585
- });
1580
+ const result = checkPath(manager, "~/.ssh/config", {}, "read");
1586
1581
  expect(result.state).toBe("deny");
1587
1582
  expect(result.matchedPattern).toBe("~/.ssh/*");
1588
1583
  } finally {
@@ -1664,7 +1659,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
1664
1659
  });
1665
1660
  try {
1666
1661
  const manager = new PermissionManager({ agentDir });
1667
- const result = checkTool(manager, "read", { path: "foo.txt" });
1662
+ const result = checkPath(manager, "foo.txt", {}, "read");
1668
1663
  expect(result.state).toBe("deny");
1669
1664
  } finally {
1670
1665
  cleanup();
@@ -1679,16 +1674,12 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
1679
1674
  try {
1680
1675
  const manager = new PermissionManager({ agentDir });
1681
1676
  // Before configureForCwd: global policy applies
1682
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1683
- "deny",
1684
- );
1677
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("deny");
1685
1678
 
1686
1679
  manager.configureForCwd(cwd);
1687
1680
 
1688
1681
  // After configureForCwd: project override applies (last-match-wins)
1689
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1690
- "allow",
1691
- );
1682
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("allow");
1692
1683
  } finally {
1693
1684
  cleanup();
1694
1685
  }
@@ -1702,16 +1693,12 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
1702
1693
  try {
1703
1694
  const manager = new PermissionManager({ agentDir });
1704
1695
  manager.configureForCwd(cwd);
1705
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1706
- "allow",
1707
- );
1696
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("allow");
1708
1697
 
1709
1698
  manager.configureForCwd(undefined);
1710
1699
 
1711
1700
  // After reverting: global policy applies again
1712
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1713
- "deny",
1714
- );
1701
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("deny");
1715
1702
  } finally {
1716
1703
  cleanup();
1717
1704
  }
@@ -1724,9 +1711,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
1724
1711
  try {
1725
1712
  const manager = new PermissionManager({ agentDir });
1726
1713
  // Warm the cache
1727
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1728
- "allow",
1729
- );
1714
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("allow");
1730
1715
  // Update global config on disk to deny read
1731
1716
  writeFileSync(
1732
1717
  globalConfigPath,
@@ -1735,9 +1720,7 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
1735
1720
  // configureForCwd clears cache + rebuilds loader
1736
1721
  manager.configureForCwd(undefined);
1737
1722
  // Should pick up the changed global config
1738
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1739
- "deny",
1740
- );
1723
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("deny");
1741
1724
  } finally {
1742
1725
  cleanup();
1743
1726
  }
@@ -1778,13 +1761,11 @@ describe("PermissionManager — configureForCwd and agentDir option", () => {
1778
1761
  manager.configureForCwd(cwd);
1779
1762
 
1780
1763
  // Without an agent name: global allow applies.
1781
- expect(checkTool(manager, "read", { path: "foo.txt" }).state).toBe(
1782
- "allow",
1783
- );
1764
+ expect(checkPath(manager, "foo.txt", {}, "read").state).toBe("allow");
1784
1765
  // With the "coder" agent: project-agent deny overrides global allow.
1785
- expect(
1786
- checkTool(manager, "read", { path: "foo.txt" }, "coder").state,
1787
- ).toBe("deny");
1766
+ expect(checkPath(manager, "foo.txt", {}, "read", "coder").state).toBe(
1767
+ "deny",
1768
+ );
1788
1769
  } finally {
1789
1770
  cleanup();
1790
1771
  }
@@ -2693,20 +2674,22 @@ permission:
2693
2674
  );
2694
2675
 
2695
2676
  try {
2696
- const allowed = checkTool(
2677
+ const allowed = checkPath(
2697
2678
  manager,
2679
+ `${homedir()}/Downloads/file.txt`,
2680
+ {},
2698
2681
  "external_directory",
2699
- { path: `${homedir()}/Downloads/file.txt` },
2700
2682
  "trusted",
2701
2683
  );
2702
2684
  expect(allowed.state).toBe("allow");
2703
2685
  expect(allowed.matchedPattern).toBe("~/Downloads/*");
2704
2686
  expect(allowed.source).toBe("special");
2705
2687
 
2706
- const denied = checkTool(
2688
+ const denied = checkPath(
2707
2689
  manager,
2690
+ `${homedir()}/Documents/secret.txt`,
2691
+ {},
2708
2692
  "external_directory",
2709
- { path: `${homedir()}/Documents/secret.txt` },
2710
2693
  "trusted",
2711
2694
  );
2712
2695
  expect(denied.state).toBe("deny");
@@ -2890,10 +2873,11 @@ test("checkPermission returns source 'session' when session rules cover the exte
2890
2873
  },
2891
2874
  ];
2892
2875
 
2893
- const result = checkTool(
2876
+ const result = checkPath(
2894
2877
  manager,
2878
+ "/other/project/src/foo.ts",
2879
+ {},
2895
2880
  "external_directory",
2896
- { path: "/other/project/src/foo.ts" },
2897
2881
  undefined,
2898
2882
  sessionRules,
2899
2883
  );
@@ -2941,16 +2925,20 @@ test("checkPermission with empty session rules is identical to call without sess
2941
2925
  });
2942
2926
 
2943
2927
  try {
2944
- const withEmpty = checkTool(
2928
+ const withEmpty = checkPath(
2945
2929
  manager,
2930
+ "/other/project/foo.ts",
2931
+ {},
2946
2932
  "external_directory",
2947
- { path: "/other/project/foo.ts" },
2948
2933
  undefined,
2949
2934
  [],
2950
2935
  );
2951
- const withoutArg = checkTool(manager, "external_directory", {
2952
- path: "/other/project/foo.ts",
2953
- });
2936
+ const withoutArg = checkPath(
2937
+ manager,
2938
+ "/other/project/foo.ts",
2939
+ {},
2940
+ "external_directory",
2941
+ );
2954
2942
  const expected: PermissionCheckResult = {
2955
2943
  toolName: "external_directory",
2956
2944
  state: "deny",
@@ -3019,10 +3007,11 @@ test("session rules override config deny for external_directory", () => {
3019
3007
  },
3020
3008
  ];
3021
3009
 
3022
- const result = checkTool(
3010
+ const result = checkPath(
3023
3011
  manager,
3012
+ "/other/project/src/foo.ts",
3013
+ {},
3024
3014
  "external_directory",
3025
- { path: "/other/project/src/foo.ts" },
3026
3015
  undefined,
3027
3016
  sessionRules,
3028
3017
  );
@@ -3299,7 +3288,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
3299
3288
  });
3300
3289
  try {
3301
3290
  manager.configureForCwd(cwd);
3302
- const result = checkTool(manager, "read", { path: "src/App.jsx" });
3291
+ const result = checkPath(manager, "src/App.jsx", { cwd }, "read");
3303
3292
  expect(result.state).toBe("allow");
3304
3293
  expect(result.matchedPattern).toBe(`${cwd}/*`);
3305
3294
  } finally {
@@ -3313,7 +3302,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
3313
3302
  });
3314
3303
  try {
3315
3304
  manager.configureForCwd(cwd);
3316
- const result = checkTool(manager, "read", { path: "src/App.jsx" });
3305
+ const result = checkPath(manager, "src/App.jsx", { cwd }, "read");
3317
3306
  expect(result.state).toBe("deny");
3318
3307
  expect(result.matchedPattern).toBe("src/*");
3319
3308
  } finally {
@@ -3331,7 +3320,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
3331
3320
  });
3332
3321
  try {
3333
3322
  manager.configureForCwd(cwd);
3334
- const result = checkTool(manager, "read", { path: "src/App.jsx" });
3323
+ const result = checkPath(manager, "src/App.jsx", { cwd }, "read");
3335
3324
  // The later "src/*" deny wins over the earlier absolute allow.
3336
3325
  expect(result.state).toBe("deny");
3337
3326
  expect(result.matchedPattern).toBe("src/*");
@@ -3346,7 +3335,7 @@ describe("checkPermission — cwd-aware path policy values", () => {
3346
3335
  });
3347
3336
  try {
3348
3337
  manager.configureForCwd(cwd);
3349
- const result = checkTool(manager, "path", { path: "src/App.jsx" });
3338
+ const result = checkPath(manager, "src/App.jsx", { cwd });
3350
3339
  expect(result.state).toBe("allow");
3351
3340
  expect(result.matchedPattern).toBe(`${cwd}/*`);
3352
3341
  } finally {