@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +2 -1
  3. package/package.json +1 -1
  4. package/src/access-intent/access-intent.ts +8 -10
  5. package/src/access-intent/access-path.ts +39 -16
  6. package/src/access-intent/bash/bash-path-resolver.ts +531 -0
  7. package/src/access-intent/bash/program.ts +21 -14
  8. package/src/access-intent/bash/token-classification.ts +1 -1
  9. package/src/canonicalize-path.ts +11 -5
  10. package/src/forwarded-permissions/permission-forwarder.ts +11 -1
  11. package/src/forwarding-manager.ts +7 -1
  12. package/src/handlers/before-agent-start.ts +1 -0
  13. package/src/handlers/gates/bash-path-extractor.ts +7 -5
  14. package/src/handlers/gates/bash-path.ts +13 -13
  15. package/src/handlers/gates/external-directory.ts +14 -16
  16. package/src/handlers/gates/path.ts +12 -7
  17. package/src/handlers/gates/skill-read.ts +7 -1
  18. package/src/handlers/gates/tool-call-gate-pipeline.ts +21 -4
  19. package/src/handlers/gates/tool.ts +4 -2
  20. package/src/index.ts +12 -1
  21. package/src/input-normalizer.ts +9 -4
  22. package/src/path-normalizer.ts +70 -0
  23. package/src/path-utils.ts +39 -28
  24. package/src/permission-manager.ts +21 -7
  25. package/src/permission-session.ts +35 -2
  26. package/src/prompting-gateway.ts +3 -0
  27. package/src/rule.ts +8 -6
  28. package/src/skill-prompt-sanitizer.ts +15 -4
  29. package/src/subagent-context.ts +17 -9
  30. package/test/access-intent/access-path.test.ts +124 -15
  31. package/test/access-intent/bash/program.test.ts +178 -80
  32. package/test/bash-external-directory.test.ts +15 -1
  33. package/test/canonicalize-path.test.ts +34 -8
  34. package/test/forwarding-manager.test.ts +7 -1
  35. package/test/handlers/external-directory-symlink-acceptance.test.ts +26 -4
  36. package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
  37. package/test/handlers/gates/bash-external-directory.test.ts +5 -2
  38. package/test/handlers/gates/bash-path.test.ts +14 -9
  39. package/test/handlers/gates/external-directory-policy.test.ts +28 -18
  40. package/test/handlers/gates/external-directory.test.ts +9 -1
  41. package/test/handlers/gates/path.test.ts +90 -20
  42. package/test/handlers/gates/skill-read.test.ts +22 -13
  43. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
  44. package/test/handlers/gates/tool.test.ts +13 -1
  45. package/test/helpers/gate-fixtures.ts +11 -2
  46. package/test/helpers/session-fixtures.ts +3 -0
  47. package/test/input-normalizer.test.ts +104 -39
  48. package/test/path-normalizer.test.ts +123 -0
  49. package/test/path-utils.test.ts +130 -51
  50. package/test/permission-forwarder.test.ts +1 -0
  51. package/test/permission-manager-unified.test.ts +40 -0
  52. package/test/permission-resolver.test.ts +14 -32
  53. package/test/permission-session.test.ts +41 -0
  54. package/test/pi-infrastructure-read.test.ts +27 -4
  55. package/test/prompting-gateway.test.ts +1 -0
  56. package/test/rule.test.ts +88 -42
  57. package/test/session-rules.test.ts +21 -4
  58. package/test/skill-prompt-sanitizer.test.ts +69 -14
  59. package/test/subagent-context.test.ts +77 -31
  60. package/test/synthesize.test.ts +13 -11
  61. package/src/access-intent/bash/cwd-projection.ts +0 -498
@@ -21,7 +21,7 @@ vi.mock("node:fs", () => ({
21
21
 
22
22
  import { AccessPath } from "#src/access-intent/access-path";
23
23
 
24
- describe("AccessPath.forExternalDirectory", () => {
24
+ describe("AccessPath.forPath", () => {
25
25
  const cwd = "/projects/my-app";
26
26
 
27
27
  beforeEach(() => {
@@ -36,19 +36,25 @@ describe("AccessPath.forExternalDirectory", () => {
36
36
  p.startsWith("/tmp") ? `/private${p}` : p,
37
37
  );
38
38
  expect(
39
- AccessPath.forExternalDirectory("/tmp/x", cwd).matchValues(),
39
+ AccessPath.forPath("/tmp/x", { cwd, platform: "linux" }).matchValues(),
40
40
  ).toEqual(["/tmp/x", "/private/tmp/x"]);
41
41
  });
42
42
 
43
43
  test("deduplicates when the canonical form equals the lexical form", () => {
44
44
  expect(
45
- AccessPath.forExternalDirectory("/etc/hosts", cwd).matchValues(),
45
+ AccessPath.forPath("/etc/hosts", {
46
+ cwd,
47
+ platform: "linux",
48
+ }).matchValues(),
46
49
  ).toEqual(["/etc/hosts"]);
47
50
  });
48
51
 
49
52
  test("keeps the relative aliases for an in-cwd token without duplicating", () => {
50
53
  expect(
51
- AccessPath.forExternalDirectory("src/foo.ts", cwd).matchValues(),
54
+ AccessPath.forPath("src/foo.ts", {
55
+ cwd,
56
+ platform: "linux",
57
+ }).matchValues(),
52
58
  ).toEqual(["/projects/my-app/src/foo.ts", "src/foo.ts"]);
53
59
  });
54
60
 
@@ -56,9 +62,68 @@ describe("AccessPath.forExternalDirectory", () => {
56
62
  // Force canonicalizePath to return the original (no-op symlink resolution
57
63
  // effectively means canonical === lexical, handled by dedup).
58
64
  expect(
59
- AccessPath.forExternalDirectory("/etc/hosts", cwd).matchValues(),
65
+ AccessPath.forPath("/etc/hosts", {
66
+ cwd,
67
+ platform: "linux",
68
+ }).matchValues(),
60
69
  ).not.toHaveLength(0);
61
70
  });
71
+
72
+ test("resolves a relative token against an explicit resolveBase", () => {
73
+ // The cd-folded effective base differs from cwd (the bash-path case).
74
+ expect(
75
+ AccessPath.forPath("foo.ts", {
76
+ cwd,
77
+ resolveBase: "/projects/my-app/sub",
78
+ platform: "linux",
79
+ }).matchValues(),
80
+ ).toEqual(["/projects/my-app/sub/foo.ts", "sub/foo.ts", "foo.ts"]);
81
+ });
82
+
83
+ test("adds the canonical alias resolved against resolveBase", () => {
84
+ realpathSync.mockImplementation((p: string) =>
85
+ p === "/projects/my-app/sub/foo.ts" ? "/real/foo.ts" : p,
86
+ );
87
+ expect(
88
+ AccessPath.forPath("foo.ts", {
89
+ cwd,
90
+ resolveBase: "/projects/my-app/sub",
91
+ platform: "linux",
92
+ }).matchValues(),
93
+ ).toEqual([
94
+ "/projects/my-app/sub/foo.ts",
95
+ "sub/foo.ts",
96
+ "foo.ts",
97
+ "/real/foo.ts",
98
+ ]);
99
+ });
100
+ });
101
+
102
+ describe("platform option", () => {
103
+ test("win32: builds lexical/match/boundary values with win32 rules", () => {
104
+ const ap = AccessPath.forPath("src\\foo.ts", {
105
+ cwd: "C:\\Projects\\App",
106
+ platform: "win32",
107
+ });
108
+ expect(ap.value()).toBe("c:\\projects\\app\\src\\foo.ts");
109
+ expect(ap.boundaryValue()).toBe("c:\\projects\\app\\src\\foo.ts");
110
+ expect(ap.matchValues()).toEqual([
111
+ "c:\\projects\\app\\src\\foo.ts",
112
+ "src\\foo.ts",
113
+ ]);
114
+ });
115
+
116
+ test("win32: lowercases the symlink-resolved boundary value", () => {
117
+ realpathSync.mockImplementation((p: string) =>
118
+ p === "c:\\projects\\app\\link" ? "C:\\Real\\App" : p,
119
+ );
120
+ expect(
121
+ AccessPath.forPath("link", {
122
+ cwd: "C:\\Projects\\App",
123
+ platform: "win32",
124
+ }).boundaryValue(),
125
+ ).toBe("c:\\real\\app");
126
+ });
62
127
  });
63
128
 
64
129
  describe("boundaryValue()", () => {
@@ -67,18 +132,26 @@ describe("AccessPath.forExternalDirectory", () => {
67
132
  p.startsWith("/tmp") ? `/private${p}` : p,
68
133
  );
69
134
  expect(
70
- AccessPath.forExternalDirectory("/tmp/x", cwd).boundaryValue(),
135
+ AccessPath.forPath("/tmp/x", {
136
+ cwd,
137
+ platform: "linux",
138
+ }).boundaryValue(),
71
139
  ).toBe("/private/tmp/x");
72
140
  });
73
141
 
74
142
  test("returns the lexical form when path has no symlinks", () => {
75
143
  expect(
76
- AccessPath.forExternalDirectory("/etc/hosts", cwd).boundaryValue(),
144
+ AccessPath.forPath("/etc/hosts", {
145
+ cwd,
146
+ platform: "linux",
147
+ }).boundaryValue(),
77
148
  ).toBe("/etc/hosts");
78
149
  });
79
150
 
80
151
  test("returns empty string for empty input", () => {
81
- expect(AccessPath.forExternalDirectory("", cwd).boundaryValue()).toBe("");
152
+ expect(
153
+ AccessPath.forPath("", { cwd, platform: "linux" }).boundaryValue(),
154
+ ).toBe("");
82
155
  });
83
156
  });
84
157
 
@@ -88,20 +161,56 @@ describe("AccessPath.forExternalDirectory", () => {
88
161
  p.startsWith("/tmp") ? `/private${p}` : p,
89
162
  );
90
163
  // Even when the path resolves to a different canonical, value() stays lexical.
91
- expect(AccessPath.forExternalDirectory("/tmp/x", cwd).value()).toBe(
92
- "/tmp/x",
93
- );
164
+ expect(
165
+ AccessPath.forPath("/tmp/x", { cwd, platform: "linux" }).value(),
166
+ ).toBe("/tmp/x");
94
167
  });
95
168
 
96
169
  test("normalizes the path against cwd", () => {
97
170
  // A relative path becomes an absolute lexical value.
98
- expect(AccessPath.forExternalDirectory("src/foo.ts", cwd).value()).toBe(
99
- "/projects/my-app/src/foo.ts",
100
- );
171
+ expect(
172
+ AccessPath.forPath("src/foo.ts", { cwd, platform: "linux" }).value(),
173
+ ).toBe("/projects/my-app/src/foo.ts");
174
+ });
175
+
176
+ test("normalizes a relative path against an explicit resolveBase", () => {
177
+ expect(
178
+ AccessPath.forPath("foo.ts", {
179
+ cwd,
180
+ resolveBase: "/projects/my-app/sub",
181
+ platform: "linux",
182
+ }).value(),
183
+ ).toBe("/projects/my-app/sub/foo.ts");
101
184
  });
102
185
 
103
186
  test("returns empty string for empty input", () => {
104
- expect(AccessPath.forExternalDirectory("", cwd).value()).toBe("");
187
+ expect(AccessPath.forPath("", { cwd, platform: "linux" }).value()).toBe(
188
+ "",
189
+ );
105
190
  });
106
191
  });
107
192
  });
193
+
194
+ describe("AccessPath.forLiteral", () => {
195
+ beforeEach(() => {
196
+ realpathSync.mockReset();
197
+ realpathSync.mockImplementation((p: string) => p);
198
+ });
199
+
200
+ test("matchValues() carries only the literal — no canonical, no absolute", () => {
201
+ expect(AccessPath.forLiteral("foo.ts").matchValues()).toEqual(["foo.ts"]);
202
+ });
203
+
204
+ test("boundaryValue() is empty (no outside-cwd notion for an unknown base)", () => {
205
+ expect(AccessPath.forLiteral("foo.ts").boundaryValue()).toBe("");
206
+ });
207
+
208
+ test("value() returns the literal", () => {
209
+ expect(AccessPath.forLiteral("foo.ts").value()).toBe("foo.ts");
210
+ });
211
+
212
+ test("an empty literal yields no match values", () => {
213
+ expect(AccessPath.forLiteral("").matchValues()).toEqual([]);
214
+ expect(AccessPath.forLiteral("").value()).toBe("");
215
+ });
216
+ });