@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.
- package/CHANGELOG.md +33 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +6 -3
- package/src/access-intent/bash/bash-path-resolver.ts +16 -12
- package/src/access-intent/bash/program.ts +0 -4
- package/src/access-intent/bash/token-classification.ts +23 -0
- package/src/handlers/before-agent-start.ts +1 -2
- package/src/handlers/gates/external-directory.ts +2 -12
- package/src/handlers/gates/skill-read.ts +4 -8
- package/src/handlers/gates/tool-call-gate-pipeline.ts +62 -24
- package/src/handlers/gates/tool.ts +10 -14
- package/src/index.ts +8 -6
- package/src/input-normalizer.ts +54 -56
- package/src/path-normalizer.ts +30 -0
- package/src/permission-event-rpc.ts +21 -15
- package/src/permission-manager.ts +3 -6
- package/src/permission-session.ts +0 -5
- package/src/permissions-service.ts +33 -12
- package/src/skill-prompt-sanitizer.ts +8 -21
- package/test/access-intent/bash/program.test.ts +1 -1
- package/test/access-intent/bash/token-classification.test.ts +75 -0
- package/test/bash-external-directory.test.ts +38 -0
- package/test/composition-root.test.ts +22 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -3
- package/test/handlers/gates/external-directory.test.ts +0 -1
- package/test/handlers/gates/skill-read.test.ts +16 -12
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +73 -0
- package/test/handlers/gates/tool.test.ts +25 -16
- package/test/helpers/gate-fixtures.ts +0 -3
- package/test/input-normalizer.test.ts +163 -270
- package/test/path-normalizer.test.ts +43 -0
- package/test/path-utils.test.ts +1 -1
- package/test/permission-event-rpc.test.ts +80 -65
- package/test/permission-manager-unified.test.ts +134 -145
- package/test/permissions-service.test.ts +84 -72
- package/test/service.test.ts +56 -103
- 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 =
|
|
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 =
|
|
270
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
480
|
-
|
|
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 =
|
|
497
|
-
|
|
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 =
|
|
514
|
-
|
|
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 =
|
|
530
|
-
|
|
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 =
|
|
900
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
1431
|
-
expect(
|
|
1432
|
-
|
|
1433
|
-
);
|
|
1434
|
-
expect(
|
|
1435
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1487
|
-
path: "secret.txt",
|
|
1488
|
-
});
|
|
1495
|
+
const pathResult = checkPath(manager, "secret.txt");
|
|
1489
1496
|
expect(pathResult.state).toBe("allow");
|
|
1490
|
-
const readResult =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
1787
|
-
)
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2952
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|