@gotgenes/pi-permission-system 16.2.1 → 17.1.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 +26 -0
- package/README.md +2 -1
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +8 -10
- package/src/access-intent/access-path.ts +39 -16
- package/src/access-intent/bash/bash-path-resolver.ts +531 -0
- package/src/access-intent/bash/program.ts +21 -14
- package/src/access-intent/bash/token-classification.ts +1 -1
- package/src/canonicalize-path.ts +11 -5
- package/src/forwarded-permissions/permission-forwarder.ts +11 -1
- package/src/forwarding-manager.ts +7 -1
- package/src/handlers/before-agent-start.ts +1 -0
- package/src/handlers/gates/bash-path-extractor.ts +7 -5
- package/src/handlers/gates/bash-path.ts +13 -13
- package/src/handlers/gates/external-directory.ts +14 -16
- package/src/handlers/gates/path.ts +12 -7
- package/src/handlers/gates/skill-read.ts +7 -1
- package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -4
- package/src/handlers/gates/tool.ts +4 -2
- package/src/index.ts +12 -1
- package/src/input-normalizer.ts +9 -4
- package/src/path-normalizer.ts +70 -0
- package/src/path-utils.ts +39 -28
- package/src/permission-manager.ts +21 -7
- package/src/permission-session.ts +35 -2
- package/src/prompting-gateway.ts +3 -0
- package/src/rule.ts +8 -6
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/src/subagent-context.ts +17 -9
- package/test/access-intent/access-path.test.ts +124 -15
- package/test/access-intent/bash/program.test.ts +178 -80
- package/test/bash-external-directory.test.ts +15 -1
- package/test/canonicalize-path.test.ts +34 -8
- package/test/forwarding-manager.test.ts +7 -1
- package/test/handlers/external-directory-symlink-acceptance.test.ts +26 -4
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
- package/test/handlers/gates/bash-external-directory.test.ts +5 -2
- package/test/handlers/gates/bash-path.test.ts +14 -9
- package/test/handlers/gates/external-directory-policy.test.ts +28 -18
- package/test/handlers/gates/external-directory.test.ts +9 -1
- package/test/handlers/gates/path.test.ts +90 -20
- package/test/handlers/gates/skill-read.test.ts +22 -13
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
- package/test/handlers/gates/tool.test.ts +13 -1
- package/test/helpers/gate-fixtures.ts +11 -2
- package/test/helpers/session-fixtures.ts +3 -0
- package/test/input-normalizer.test.ts +104 -39
- package/test/path-normalizer.test.ts +123 -0
- package/test/path-utils.test.ts +130 -51
- package/test/permission-forwarder.test.ts +1 -0
- package/test/permission-manager-unified.test.ts +40 -0
- package/test/permission-resolver.test.ts +14 -32
- package/test/permission-session.test.ts +41 -0
- package/test/pi-infrastructure-read.test.ts +27 -4
- package/test/prompting-gateway.test.ts +1 -0
- package/test/rule.test.ts +88 -42
- package/test/session-rules.test.ts +21 -4
- package/test/skill-prompt-sanitizer.test.ts +69 -14
- package/test/subagent-context.test.ts +77 -31
- package/test/synthesize.test.ts +13 -11
- package/src/access-intent/bash/cwd-projection.ts +0 -498
package/test/path-utils.test.ts
CHANGED
|
@@ -41,85 +41,111 @@ describe("normalizePathForComparison", () => {
|
|
|
41
41
|
const cwd = "/projects/my-app";
|
|
42
42
|
|
|
43
43
|
test("resolves absolute path unchanged", () => {
|
|
44
|
-
expect(normalizePathForComparison("/usr/local/bin", cwd)).toBe(
|
|
44
|
+
expect(normalizePathForComparison("/usr/local/bin", cwd, "linux")).toBe(
|
|
45
45
|
"/usr/local/bin",
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
test("resolves relative path against cwd", () => {
|
|
50
|
-
expect(normalizePathForComparison("src/foo.ts", cwd)).toBe(
|
|
50
|
+
expect(normalizePathForComparison("src/foo.ts", cwd, "linux")).toBe(
|
|
51
51
|
"/projects/my-app/src/foo.ts",
|
|
52
52
|
);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test("expands bare ~ to homedir", () => {
|
|
56
|
-
expect(normalizePathForComparison("~", cwd)).toBe("/mock/home");
|
|
56
|
+
expect(normalizePathForComparison("~", cwd, "linux")).toBe("/mock/home");
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
test("expands ~/... to homedir-relative path", () => {
|
|
60
|
-
expect(normalizePathForComparison("~/docs/readme.md", cwd)).toBe(
|
|
60
|
+
expect(normalizePathForComparison("~/docs/readme.md", cwd, "linux")).toBe(
|
|
61
61
|
join("/mock/home", "docs/readme.md"),
|
|
62
62
|
);
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
test("expands bare $HOME to homedir", () => {
|
|
66
|
-
expect(normalizePathForComparison("$HOME", cwd)).toBe(
|
|
66
|
+
expect(normalizePathForComparison("$HOME", cwd, "linux")).toBe(
|
|
67
|
+
"/mock/home",
|
|
68
|
+
);
|
|
67
69
|
});
|
|
68
70
|
|
|
69
71
|
test("expands $HOME/... to homedir-relative path", () => {
|
|
70
|
-
expect(normalizePathForComparison("$HOME/.ssh/config", cwd)).toBe(
|
|
72
|
+
expect(normalizePathForComparison("$HOME/.ssh/config", cwd, "linux")).toBe(
|
|
71
73
|
join("/mock/home", ".ssh/config"),
|
|
72
74
|
);
|
|
73
75
|
});
|
|
74
76
|
|
|
75
77
|
test("strips leading @ before resolving", () => {
|
|
76
|
-
expect(normalizePathForComparison("@/usr/local/bin", cwd)).toBe(
|
|
78
|
+
expect(normalizePathForComparison("@/usr/local/bin", cwd, "linux")).toBe(
|
|
77
79
|
"/usr/local/bin",
|
|
78
80
|
);
|
|
79
81
|
});
|
|
80
82
|
|
|
81
83
|
test("strips surrounding quotes", () => {
|
|
82
|
-
expect(normalizePathForComparison("'/usr/local/bin'", cwd)).toBe(
|
|
84
|
+
expect(normalizePathForComparison("'/usr/local/bin'", cwd, "linux")).toBe(
|
|
83
85
|
"/usr/local/bin",
|
|
84
86
|
);
|
|
85
|
-
expect(normalizePathForComparison('"/usr/local/bin"', cwd)).toBe(
|
|
87
|
+
expect(normalizePathForComparison('"/usr/local/bin"', cwd, "linux")).toBe(
|
|
86
88
|
"/usr/local/bin",
|
|
87
89
|
);
|
|
88
90
|
});
|
|
89
91
|
|
|
90
92
|
test("returns empty string for blank/whitespace-only path", () => {
|
|
91
|
-
expect(normalizePathForComparison("", cwd)).toBe("");
|
|
92
|
-
expect(normalizePathForComparison(" ", cwd)).toBe("");
|
|
93
|
+
expect(normalizePathForComparison("", cwd, "linux")).toBe("");
|
|
94
|
+
expect(normalizePathForComparison(" ", cwd, "linux")).toBe("");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ── injected platform flavor (Windows is case-folded, win32-resolved) ────
|
|
98
|
+
|
|
99
|
+
test("win32: lowercases the resolved absolute path", () => {
|
|
100
|
+
expect(
|
|
101
|
+
normalizePathForComparison(
|
|
102
|
+
"C:\\Users\\Foo\\Bar.txt",
|
|
103
|
+
"C:\\Projects",
|
|
104
|
+
"win32",
|
|
105
|
+
),
|
|
106
|
+
).toBe("c:\\users\\foo\\bar.txt");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("win32: resolves a relative path against cwd with win32 rules", () => {
|
|
110
|
+
expect(
|
|
111
|
+
normalizePathForComparison("src\\foo.ts", "C:\\Projects\\App", "win32"),
|
|
112
|
+
).toBe("c:\\projects\\app\\src\\foo.ts");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("posix platform leaves case untouched", () => {
|
|
116
|
+
expect(
|
|
117
|
+
normalizePathForComparison("/Projects/App/Src.ts", cwd, "linux"),
|
|
118
|
+
).toBe("/Projects/App/Src.ts");
|
|
93
119
|
});
|
|
94
120
|
});
|
|
95
121
|
|
|
96
122
|
describe("isPathWithinDirectory", () => {
|
|
97
123
|
test("returns true when path equals directory", () => {
|
|
98
|
-
expect(isPathWithinDirectory("/a/b", "/a/b")).toBe(true);
|
|
124
|
+
expect(isPathWithinDirectory("/a/b", "/a/b", "linux")).toBe(true);
|
|
99
125
|
});
|
|
100
126
|
|
|
101
127
|
test("returns true when path is a direct child", () => {
|
|
102
|
-
expect(isPathWithinDirectory("/a/b/c", "/a/b")).toBe(true);
|
|
128
|
+
expect(isPathWithinDirectory("/a/b/c", "/a/b", "linux")).toBe(true);
|
|
103
129
|
});
|
|
104
130
|
|
|
105
131
|
test("returns true when path is a deep descendant", () => {
|
|
106
|
-
expect(isPathWithinDirectory("/a/b/c/d/e", "/a/b")).toBe(true);
|
|
132
|
+
expect(isPathWithinDirectory("/a/b/c/d/e", "/a/b", "linux")).toBe(true);
|
|
107
133
|
});
|
|
108
134
|
|
|
109
135
|
test("returns false when path is a sibling directory", () => {
|
|
110
|
-
expect(isPathWithinDirectory("/a/bc", "/a/b")).toBe(false);
|
|
136
|
+
expect(isPathWithinDirectory("/a/bc", "/a/b", "linux")).toBe(false);
|
|
111
137
|
});
|
|
112
138
|
|
|
113
139
|
test("returns false when path is outside the directory", () => {
|
|
114
|
-
expect(isPathWithinDirectory("/other/path", "/a/b")).toBe(false);
|
|
140
|
+
expect(isPathWithinDirectory("/other/path", "/a/b", "linux")).toBe(false);
|
|
115
141
|
});
|
|
116
142
|
|
|
117
143
|
test("returns false for empty path", () => {
|
|
118
|
-
expect(isPathWithinDirectory("", "/a/b")).toBe(false);
|
|
144
|
+
expect(isPathWithinDirectory("", "/a/b", "linux")).toBe(false);
|
|
119
145
|
});
|
|
120
146
|
|
|
121
147
|
test("returns false for empty directory", () => {
|
|
122
|
-
expect(isPathWithinDirectory("/a/b", "")).toBe(false);
|
|
148
|
+
expect(isPathWithinDirectory("/a/b", "", "linux")).toBe(false);
|
|
123
149
|
});
|
|
124
150
|
|
|
125
151
|
// ── platform-aware containment (Windows is case-insensitive) ────────────
|
|
@@ -319,49 +345,65 @@ describe("isPathOutsideWorkingDirectory", () => {
|
|
|
319
345
|
});
|
|
320
346
|
|
|
321
347
|
test("returns false when path is inside cwd", () => {
|
|
322
|
-
expect(
|
|
323
|
-
|
|
324
|
-
);
|
|
348
|
+
expect(
|
|
349
|
+
isPathOutsideWorkingDirectory("/projects/my-app/src", cwd, "linux"),
|
|
350
|
+
).toBe(false);
|
|
325
351
|
});
|
|
326
352
|
|
|
327
353
|
test("returns false when path equals cwd", () => {
|
|
328
|
-
expect(
|
|
354
|
+
expect(
|
|
355
|
+
isPathOutsideWorkingDirectory("/projects/my-app", cwd, "linux"),
|
|
356
|
+
).toBe(false);
|
|
329
357
|
});
|
|
330
358
|
|
|
331
359
|
test("returns true when path is outside cwd", () => {
|
|
332
|
-
expect(isPathOutsideWorkingDirectory("/etc/passwd", cwd)).toBe(
|
|
360
|
+
expect(isPathOutsideWorkingDirectory("/etc/passwd", cwd, "linux")).toBe(
|
|
361
|
+
true,
|
|
362
|
+
);
|
|
333
363
|
});
|
|
334
364
|
|
|
335
365
|
test("returns true for home directory when outside cwd", () => {
|
|
336
|
-
expect(isPathOutsideWorkingDirectory("~/secrets", cwd)).toBe(true);
|
|
366
|
+
expect(isPathOutsideWorkingDirectory("~/secrets", cwd, "linux")).toBe(true);
|
|
337
367
|
});
|
|
338
368
|
|
|
339
369
|
test("returns false for relative path resolving inside cwd", () => {
|
|
340
|
-
expect(isPathOutsideWorkingDirectory("src/index.ts", cwd)).toBe(
|
|
370
|
+
expect(isPathOutsideWorkingDirectory("src/index.ts", cwd, "linux")).toBe(
|
|
371
|
+
false,
|
|
372
|
+
);
|
|
341
373
|
});
|
|
342
374
|
|
|
343
375
|
test("returns false for empty path (normalizes to empty string)", () => {
|
|
344
|
-
expect(isPathOutsideWorkingDirectory("", cwd)).toBe(false);
|
|
376
|
+
expect(isPathOutsideWorkingDirectory("", cwd, "linux")).toBe(false);
|
|
345
377
|
});
|
|
346
378
|
|
|
347
379
|
test("returns false for /dev/null regardless of cwd", () => {
|
|
348
|
-
expect(isPathOutsideWorkingDirectory("/dev/null", cwd)).toBe(
|
|
380
|
+
expect(isPathOutsideWorkingDirectory("/dev/null", cwd, "linux")).toBe(
|
|
381
|
+
false,
|
|
382
|
+
);
|
|
349
383
|
});
|
|
350
384
|
|
|
351
385
|
test("returns false for /dev/stdin regardless of cwd", () => {
|
|
352
|
-
expect(isPathOutsideWorkingDirectory("/dev/stdin", cwd)).toBe(
|
|
386
|
+
expect(isPathOutsideWorkingDirectory("/dev/stdin", cwd, "linux")).toBe(
|
|
387
|
+
false,
|
|
388
|
+
);
|
|
353
389
|
});
|
|
354
390
|
|
|
355
391
|
test("returns false for /dev/stdout regardless of cwd", () => {
|
|
356
|
-
expect(isPathOutsideWorkingDirectory("/dev/stdout", cwd)).toBe(
|
|
392
|
+
expect(isPathOutsideWorkingDirectory("/dev/stdout", cwd, "linux")).toBe(
|
|
393
|
+
false,
|
|
394
|
+
);
|
|
357
395
|
});
|
|
358
396
|
|
|
359
397
|
test("returns false for /dev/stderr regardless of cwd", () => {
|
|
360
|
-
expect(isPathOutsideWorkingDirectory("/dev/stderr", cwd)).toBe(
|
|
398
|
+
expect(isPathOutsideWorkingDirectory("/dev/stderr", cwd, "linux")).toBe(
|
|
399
|
+
false,
|
|
400
|
+
);
|
|
361
401
|
});
|
|
362
402
|
|
|
363
403
|
test("returns true for /dev/null/subdir (not a safe path)", () => {
|
|
364
|
-
expect(
|
|
404
|
+
expect(
|
|
405
|
+
isPathOutsideWorkingDirectory("/dev/null/subdir", cwd, "linux"),
|
|
406
|
+
).toBe(true);
|
|
365
407
|
});
|
|
366
408
|
|
|
367
409
|
test("returns true for in-cwd symlink that resolves to external path", () => {
|
|
@@ -370,7 +412,9 @@ describe("isPathOutsideWorkingDirectory", () => {
|
|
|
370
412
|
if (p === "/projects/my-app/link/hosts") return "/etc/hosts";
|
|
371
413
|
return p;
|
|
372
414
|
});
|
|
373
|
-
expect(isPathOutsideWorkingDirectory("./link/hosts", cwd)).toBe(
|
|
415
|
+
expect(isPathOutsideWorkingDirectory("./link/hosts", cwd, "linux")).toBe(
|
|
416
|
+
true,
|
|
417
|
+
);
|
|
374
418
|
});
|
|
375
419
|
|
|
376
420
|
test("returns false for path inside a symlinked cwd", () => {
|
|
@@ -382,7 +426,11 @@ describe("isPathOutsideWorkingDirectory", () => {
|
|
|
382
426
|
return p;
|
|
383
427
|
});
|
|
384
428
|
expect(
|
|
385
|
-
isPathOutsideWorkingDirectory(
|
|
429
|
+
isPathOutsideWorkingDirectory(
|
|
430
|
+
"/tmp/workspace/file.ts",
|
|
431
|
+
symlinkCwd,
|
|
432
|
+
"linux",
|
|
433
|
+
),
|
|
386
434
|
).toBe(false);
|
|
387
435
|
});
|
|
388
436
|
});
|
|
@@ -400,20 +448,34 @@ describe("canonicalNormalizePathForComparison", () => {
|
|
|
400
448
|
if (p === "/projects/link") return "/real/projects/app";
|
|
401
449
|
return p;
|
|
402
450
|
});
|
|
403
|
-
expect(
|
|
404
|
-
"/
|
|
405
|
-
);
|
|
451
|
+
expect(
|
|
452
|
+
canonicalNormalizePathForComparison("/projects/link", cwd, "linux"),
|
|
453
|
+
).toBe("/real/projects/app");
|
|
406
454
|
});
|
|
407
455
|
|
|
408
456
|
test("returns empty string for empty input", () => {
|
|
409
|
-
expect(canonicalNormalizePathForComparison("", cwd)).toBe("");
|
|
457
|
+
expect(canonicalNormalizePathForComparison("", cwd, "linux")).toBe("");
|
|
410
458
|
});
|
|
411
459
|
|
|
412
460
|
test("returns lexical form when no symlinks (identity realpathSync)", () => {
|
|
413
461
|
expect(
|
|
414
|
-
canonicalNormalizePathForComparison(
|
|
462
|
+
canonicalNormalizePathForComparison(
|
|
463
|
+
"/projects/my-app/src/index.ts",
|
|
464
|
+
cwd,
|
|
465
|
+
"linux",
|
|
466
|
+
),
|
|
415
467
|
).toBe("/projects/my-app/src/index.ts");
|
|
416
468
|
});
|
|
469
|
+
|
|
470
|
+
test("win32: lowercases the canonical form", () => {
|
|
471
|
+
expect(
|
|
472
|
+
canonicalNormalizePathForComparison(
|
|
473
|
+
"C:\\Projects\\App\\Src",
|
|
474
|
+
"C:\\Projects\\App",
|
|
475
|
+
"win32",
|
|
476
|
+
),
|
|
477
|
+
).toBe("c:\\projects\\app\\src");
|
|
478
|
+
});
|
|
417
479
|
});
|
|
418
480
|
|
|
419
481
|
describe("isPiInfrastructureRead", () => {
|
|
@@ -427,6 +489,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
427
489
|
"/mock/home/.pi/agent/config.json",
|
|
428
490
|
infraDirs,
|
|
429
491
|
cwd,
|
|
492
|
+
"linux",
|
|
430
493
|
),
|
|
431
494
|
).toBe(true);
|
|
432
495
|
});
|
|
@@ -438,6 +501,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
438
501
|
"/mock/home/.pi/agent/config.json",
|
|
439
502
|
infraDirs,
|
|
440
503
|
cwd,
|
|
504
|
+
"linux",
|
|
441
505
|
),
|
|
442
506
|
).toBe(false);
|
|
443
507
|
});
|
|
@@ -449,6 +513,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
449
513
|
"/projects/my-app/.pi/npm/package.json",
|
|
450
514
|
[],
|
|
451
515
|
cwd,
|
|
516
|
+
"linux",
|
|
452
517
|
),
|
|
453
518
|
).toBe(true);
|
|
454
519
|
});
|
|
@@ -460,14 +525,15 @@ describe("isPiInfrastructureRead", () => {
|
|
|
460
525
|
"/projects/my-app/.pi/git/some-file",
|
|
461
526
|
[],
|
|
462
527
|
cwd,
|
|
528
|
+
"linux",
|
|
463
529
|
),
|
|
464
530
|
).toBe(true);
|
|
465
531
|
});
|
|
466
532
|
|
|
467
533
|
test("returns false for path outside all infra dirs and project dirs", () => {
|
|
468
|
-
expect(
|
|
469
|
-
|
|
470
|
-
);
|
|
534
|
+
expect(
|
|
535
|
+
isPiInfrastructureRead("read", "/etc/passwd", infraDirs, cwd, "linux"),
|
|
536
|
+
).toBe(false);
|
|
471
537
|
});
|
|
472
538
|
|
|
473
539
|
// ── glob patterns ─────────────────────────────────────────────────
|
|
@@ -479,6 +545,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
479
545
|
"/opt/homebrew/Cellar/pi-coding-agent/0.74.0/libexec/lib/node_modules/@earendil-works/pi-coding-agent/SKILL.md",
|
|
480
546
|
["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
|
|
481
547
|
cwd,
|
|
548
|
+
"linux",
|
|
482
549
|
),
|
|
483
550
|
).toBe(true);
|
|
484
551
|
});
|
|
@@ -490,6 +557,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
490
557
|
"/etc/passwd",
|
|
491
558
|
["/opt/homebrew/**/@earendil-works/pi-coding-agent/**"],
|
|
492
559
|
cwd,
|
|
560
|
+
"linux",
|
|
493
561
|
),
|
|
494
562
|
).toBe(false);
|
|
495
563
|
});
|
|
@@ -502,6 +570,7 @@ describe("isPiInfrastructureRead", () => {
|
|
|
502
570
|
"/mock/home/.pi/agent/config.json",
|
|
503
571
|
["~/.pi/agent"],
|
|
504
572
|
cwd,
|
|
573
|
+
"linux",
|
|
505
574
|
),
|
|
506
575
|
).toBe(true);
|
|
507
576
|
});
|
|
@@ -582,35 +651,45 @@ describe("getPathPolicyValues", () => {
|
|
|
582
651
|
const cwd = "/projects/my-app";
|
|
583
652
|
|
|
584
653
|
test("returns only the literal when no base is available", () => {
|
|
585
|
-
expect(getPathPolicyValues("src/foo.ts")).toEqual([
|
|
586
|
-
|
|
654
|
+
expect(getPathPolicyValues("src/foo.ts", {}, "linux")).toEqual([
|
|
655
|
+
"src/foo.ts",
|
|
656
|
+
]);
|
|
657
|
+
expect(getPathPolicyValues("src/foo.ts", {}, "linux")).toEqual([
|
|
658
|
+
"src/foo.ts",
|
|
659
|
+
]);
|
|
587
660
|
});
|
|
588
661
|
|
|
589
662
|
test("adds absolute and project-relative aliases for a relative token", () => {
|
|
590
|
-
expect(getPathPolicyValues("src/foo.ts", { cwd })).toEqual([
|
|
663
|
+
expect(getPathPolicyValues("src/foo.ts", { cwd }, "linux")).toEqual([
|
|
591
664
|
"/projects/my-app/src/foo.ts",
|
|
592
665
|
"src/foo.ts",
|
|
593
666
|
]);
|
|
594
667
|
});
|
|
595
668
|
|
|
596
669
|
test("omits the relative alias for a token outside cwd", () => {
|
|
597
|
-
expect(getPathPolicyValues("/etc/hosts", { cwd })).toEqual([
|
|
670
|
+
expect(getPathPolicyValues("/etc/hosts", { cwd }, "linux")).toEqual([
|
|
671
|
+
"/etc/hosts",
|
|
672
|
+
]);
|
|
598
673
|
});
|
|
599
674
|
|
|
600
675
|
test("resolves against resolveBase while aliasing relative to cwd", () => {
|
|
601
676
|
expect(
|
|
602
|
-
getPathPolicyValues(
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
677
|
+
getPathPolicyValues(
|
|
678
|
+
"foo.txt",
|
|
679
|
+
{
|
|
680
|
+
cwd,
|
|
681
|
+
resolveBase: "/projects/my-app/nested",
|
|
682
|
+
},
|
|
683
|
+
"linux",
|
|
684
|
+
),
|
|
606
685
|
).toEqual(["/projects/my-app/nested/foo.txt", "nested/foo.txt", "foo.txt"]);
|
|
607
686
|
});
|
|
608
687
|
|
|
609
688
|
test("preserves the surface catch-all", () => {
|
|
610
|
-
expect(getPathPolicyValues("*", { cwd })).toEqual(["*"]);
|
|
689
|
+
expect(getPathPolicyValues("*", { cwd }, "linux")).toEqual(["*"]);
|
|
611
690
|
});
|
|
612
691
|
|
|
613
692
|
test("returns empty for blank input", () => {
|
|
614
|
-
expect(getPathPolicyValues(" ", { cwd })).toEqual([]);
|
|
693
|
+
expect(getPathPolicyValues(" ", { cwd }, "linux")).toEqual([]);
|
|
615
694
|
});
|
|
616
695
|
});
|
|
@@ -71,6 +71,46 @@ const sessionAllow = (surface: string, pattern: string): Rule => ({
|
|
|
71
71
|
origin: "session",
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
describe("PermissionManager — injected platform (#510)", () => {
|
|
75
|
+
const winAllow: Ruleset = [
|
|
76
|
+
sessionAllow("external_directory", "C:\\Users\\Foo\\pi\\*"),
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
it("win32 manager folds case for path-surface matching", () => {
|
|
80
|
+
const manager = new PermissionManager({
|
|
81
|
+
globalConfigPath: "/nonexistent/config.json",
|
|
82
|
+
agentsDir: "/nonexistent/agents",
|
|
83
|
+
platform: "win32",
|
|
84
|
+
});
|
|
85
|
+
const result = manager.check(
|
|
86
|
+
{
|
|
87
|
+
kind: "path-values",
|
|
88
|
+
surface: "external_directory",
|
|
89
|
+
values: ["c:\\users\\foo\\pi\\docs\\readme.md"],
|
|
90
|
+
},
|
|
91
|
+
winAllow,
|
|
92
|
+
);
|
|
93
|
+
expect(result.state).toBe("allow");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("posix manager keeps path-surface matching case-sensitive", () => {
|
|
97
|
+
const manager = new PermissionManager({
|
|
98
|
+
globalConfigPath: "/nonexistent/config.json",
|
|
99
|
+
agentsDir: "/nonexistent/agents",
|
|
100
|
+
platform: "linux",
|
|
101
|
+
});
|
|
102
|
+
const result = manager.check(
|
|
103
|
+
{
|
|
104
|
+
kind: "path-values",
|
|
105
|
+
surface: "external_directory",
|
|
106
|
+
values: ["c:\\users\\foo\\pi\\docs\\readme.md"],
|
|
107
|
+
},
|
|
108
|
+
winAllow,
|
|
109
|
+
);
|
|
110
|
+
expect(result.state).not.toBe("allow");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
74
114
|
// Adapters that build an AccessIntent and call the unified `check` entry point,
|
|
75
115
|
// so these tests exercise the single resolution path (#478) without a
|
|
76
116
|
// production-class wrapper used only by tests.
|
|
@@ -120,28 +120,7 @@ describe("PermissionResolver", () => {
|
|
|
120
120
|
});
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
describe("resolve —
|
|
124
|
-
it("forwards a path-values intent with the current session ruleset", () => {
|
|
125
|
-
const { resolver, permissionManager } = makeResolver();
|
|
126
|
-
|
|
127
|
-
resolver.resolve({
|
|
128
|
-
kind: "path-values",
|
|
129
|
-
surface: "path",
|
|
130
|
-
values: ["/proj/src/a.ts", "src/a.ts"],
|
|
131
|
-
agentName: "agent-x",
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
135
|
-
{
|
|
136
|
-
kind: "path-values",
|
|
137
|
-
surface: "path",
|
|
138
|
-
values: ["/proj/src/a.ts", "src/a.ts"],
|
|
139
|
-
agentName: "agent-x",
|
|
140
|
-
},
|
|
141
|
-
[],
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
123
|
+
describe("resolve — session ruleset threading", () => {
|
|
145
124
|
it("applies a recorded session approval on the next call", () => {
|
|
146
125
|
const pm = makePermissionManager();
|
|
147
126
|
const sessionRules = new SessionRules();
|
|
@@ -151,9 +130,12 @@ describe("PermissionResolver", () => {
|
|
|
151
130
|
SessionApproval.single("path", "src/*"),
|
|
152
131
|
);
|
|
153
132
|
resolver.resolve({
|
|
154
|
-
kind: "path
|
|
133
|
+
kind: "access-path",
|
|
155
134
|
surface: "path",
|
|
156
|
-
|
|
135
|
+
path: AccessPath.forPath("src/a.ts", {
|
|
136
|
+
cwd: "/proj",
|
|
137
|
+
platform: "linux",
|
|
138
|
+
}),
|
|
157
139
|
});
|
|
158
140
|
|
|
159
141
|
const passedRules = vi.mocked(pm.check).mock.calls[0][1];
|
|
@@ -169,10 +151,10 @@ describe("PermissionResolver", () => {
|
|
|
169
151
|
describe("resolve — access-path intent", () => {
|
|
170
152
|
it("unwraps the AccessPath via matchValues() into a path-values intent", () => {
|
|
171
153
|
const { resolver, permissionManager } = makeResolver();
|
|
172
|
-
const accessPath = AccessPath.
|
|
173
|
-
"/
|
|
174
|
-
"
|
|
175
|
-
);
|
|
154
|
+
const accessPath = AccessPath.forPath("/tmp/x", {
|
|
155
|
+
cwd: "/workspace",
|
|
156
|
+
platform: "linux",
|
|
157
|
+
});
|
|
176
158
|
|
|
177
159
|
resolver.resolve({
|
|
178
160
|
kind: "access-path",
|
|
@@ -202,10 +184,10 @@ describe("PermissionResolver", () => {
|
|
|
202
184
|
matchedPattern: "/tmp/*",
|
|
203
185
|
});
|
|
204
186
|
const { resolver } = makeResolver(pm);
|
|
205
|
-
const accessPath = AccessPath.
|
|
206
|
-
"/
|
|
207
|
-
"
|
|
208
|
-
);
|
|
187
|
+
const accessPath = AccessPath.forPath("/tmp/x", {
|
|
188
|
+
cwd: "/workspace",
|
|
189
|
+
platform: "linux",
|
|
190
|
+
});
|
|
209
191
|
|
|
210
192
|
const result = resolver.resolve({
|
|
211
193
|
kind: "access-path",
|
|
@@ -135,6 +135,47 @@ describe("PermissionSession", () => {
|
|
|
135
135
|
});
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
+
describe("getPathNormalizer", () => {
|
|
139
|
+
it("returns a normalizer bound to the reset session cwd", () => {
|
|
140
|
+
const { session } = createSession();
|
|
141
|
+
session.resetForNewSession(makeCtx({ cwd: "/projects/app" }));
|
|
142
|
+
|
|
143
|
+
const ap = session.getPathNormalizer().forPath("src/foo.ts");
|
|
144
|
+
|
|
145
|
+
expect(ap.value()).toBe("/projects/app/src/foo.ts");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("rebinds the normalizer cwd on a subsequent reset", () => {
|
|
149
|
+
const { session } = createSession();
|
|
150
|
+
session.resetForNewSession(makeCtx({ cwd: "/projects/app" }));
|
|
151
|
+
session.resetForNewSession(makeCtx({ cwd: "/projects/other" }));
|
|
152
|
+
|
|
153
|
+
expect(session.getPathNormalizer().forPath("a.ts").value()).toBe(
|
|
154
|
+
"/projects/other/a.ts",
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("binds the normalizer on activate, before any reset (no fail-open)", () => {
|
|
159
|
+
const { session } = createSession();
|
|
160
|
+
// A tool call can activate the session before session_start resets it;
|
|
161
|
+
// the normalizer must still track the active ctx cwd.
|
|
162
|
+
session.activate(makeCtx({ cwd: "/projects/activated" }));
|
|
163
|
+
|
|
164
|
+
expect(session.getPathNormalizer().forPath("a.ts").value()).toBe(
|
|
165
|
+
"/projects/activated/a.ts",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("builds a win32 normalizer when constructed with the win32 platform", () => {
|
|
170
|
+
const { session } = createSession({ platform: "win32" });
|
|
171
|
+
session.resetForNewSession(makeCtx({ cwd: "C:\\Projects\\App" }));
|
|
172
|
+
|
|
173
|
+
expect(session.getPathNormalizer().forPath("src\\foo.ts").value()).toBe(
|
|
174
|
+
"c:\\projects\\app\\src\\foo.ts",
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
138
179
|
describe("shutdown", () => {
|
|
139
180
|
it("clears session rules", () => {
|
|
140
181
|
const { session, sessionRules } = createSession();
|