@gotgenes/pi-permission-system 17.0.0 → 17.1.1

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 (59) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/package.json +1 -1
  3. package/src/access-intent/access-path.ts +5 -5
  4. package/src/access-intent/bash/bash-path-resolver.ts +535 -0
  5. package/src/access-intent/bash/program.ts +21 -14
  6. package/src/access-intent/bash/token-classification.ts +24 -1
  7. package/src/canonicalize-path.ts +11 -5
  8. package/src/forwarded-permissions/permission-forwarder.ts +11 -1
  9. package/src/forwarding-manager.ts +7 -1
  10. package/src/handlers/before-agent-start.ts +1 -1
  11. package/src/handlers/gates/bash-path-extractor.ts +7 -5
  12. package/src/handlers/gates/external-directory.ts +7 -18
  13. package/src/handlers/gates/path.ts +3 -2
  14. package/src/handlers/gates/skill-read.ts +4 -2
  15. package/src/handlers/gates/tool-call-gate-pipeline.ts +20 -4
  16. package/src/handlers/gates/tool.ts +4 -2
  17. package/src/index.ts +12 -1
  18. package/src/input-normalizer.ts +9 -4
  19. package/src/path-normalizer.ts +100 -0
  20. package/src/path-utils.ts +39 -28
  21. package/src/permission-manager.ts +21 -7
  22. package/src/permission-session.ts +35 -2
  23. package/src/prompting-gateway.ts +3 -0
  24. package/src/rule.ts +8 -6
  25. package/src/skill-prompt-sanitizer.ts +8 -10
  26. package/src/subagent-context.ts +17 -9
  27. package/test/access-intent/access-path.test.ts +73 -24
  28. package/test/access-intent/bash/program.test.ts +131 -65
  29. package/test/access-intent/bash/token-classification.test.ts +75 -0
  30. package/test/bash-external-directory.test.ts +53 -1
  31. package/test/canonicalize-path.test.ts +34 -8
  32. package/test/forwarding-manager.test.ts +7 -1
  33. package/test/handlers/external-directory-symlink-acceptance.test.ts +23 -4
  34. package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -1
  35. package/test/handlers/gates/bash-external-directory.test.ts +5 -1
  36. package/test/handlers/gates/bash-path.test.ts +6 -1
  37. package/test/handlers/gates/external-directory-policy.test.ts +26 -8
  38. package/test/handlers/gates/external-directory.test.ts +8 -1
  39. package/test/handlers/gates/path.test.ts +53 -14
  40. package/test/handlers/gates/skill-read.test.ts +26 -13
  41. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +2 -1
  42. package/test/handlers/gates/tool.test.ts +13 -1
  43. package/test/helpers/gate-fixtures.ts +10 -0
  44. package/test/helpers/session-fixtures.ts +3 -0
  45. package/test/input-normalizer.test.ts +104 -39
  46. package/test/path-normalizer.test.ts +166 -0
  47. package/test/path-utils.test.ts +130 -51
  48. package/test/permission-forwarder.test.ts +1 -0
  49. package/test/permission-manager-unified.test.ts +40 -0
  50. package/test/permission-resolver.test.ts +12 -3
  51. package/test/permission-session.test.ts +41 -0
  52. package/test/pi-infrastructure-read.test.ts +27 -4
  53. package/test/prompting-gateway.test.ts +1 -0
  54. package/test/rule.test.ts +88 -42
  55. package/test/session-rules.test.ts +21 -4
  56. package/test/skill-prompt-sanitizer.test.ts +37 -16
  57. package/test/subagent-context.test.ts +77 -31
  58. package/test/synthesize.test.ts +13 -11
  59. package/src/access-intent/bash/cwd-projection.ts +0 -500
@@ -1,8 +1,12 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  import { describeSkillReadGate } from "#src/handlers/gates/skill-read";
3
3
  import type { ToolCallContext } from "#src/handlers/gates/types";
4
+ import { PathNormalizer } from "#src/path-normalizer";
4
5
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
5
6
 
7
+ // All test tccs use cwd "/test/project"; one normalizer serves every call.
8
+ const normalizer = new PathNormalizer("linux", "/test/project");
9
+
6
10
  // ── SDK stubs ──────────────────────────────────────────────────────────────
7
11
  vi.mock("@earendil-works/pi-coding-agent", async (importOriginal) => {
8
12
  const original =
@@ -41,34 +45,39 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
41
45
 
42
46
  describe("describeSkillReadGate", () => {
43
47
  it("returns null when tool is not read", () => {
44
- const result = describeSkillReadGate(makeTcc({ toolName: "write" }), () => [
45
- makeSkillEntry(),
46
- ]);
48
+ const result = describeSkillReadGate(
49
+ makeTcc({ toolName: "write" }),
50
+ normalizer,
51
+ () => [makeSkillEntry()],
52
+ );
47
53
  expect(result).toBeNull();
48
54
  });
49
55
 
50
56
  it("returns null when no active skill entries", () => {
51
- const result = describeSkillReadGate(makeTcc(), () => []);
57
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => []);
52
58
  expect(result).toBeNull();
53
59
  });
54
60
 
55
61
  it("returns null when read path does not match any skill", () => {
56
62
  const result = describeSkillReadGate(
57
63
  makeTcc({ input: { path: "/test/project/src/index.ts" } }),
64
+ normalizer,
58
65
  () => [makeSkillEntry()],
59
66
  );
60
67
  expect(result).toBeNull();
61
68
  });
62
69
 
63
70
  it("returns null when input has no path", () => {
64
- const result = describeSkillReadGate(makeTcc({ input: {} }), () => [
65
- makeSkillEntry(),
66
- ]);
71
+ const result = describeSkillReadGate(
72
+ makeTcc({ input: {} }),
73
+ normalizer,
74
+ () => [makeSkillEntry()],
75
+ );
67
76
  expect(result).toBeNull();
68
77
  });
69
78
 
70
79
  it("returns GateDescriptor with preResolved.state matching skill entry state (ask)", () => {
71
- const result = describeSkillReadGate(makeTcc(), () => [
80
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => [
72
81
  makeSkillEntry({ state: "ask" }),
73
82
  ]);
74
83
  expect(result).not.toBeNull();
@@ -77,7 +86,7 @@ describe("describeSkillReadGate", () => {
77
86
  });
78
87
 
79
88
  it("returns GateDescriptor with preResolved.state matching skill entry state (allow)", () => {
80
- const result = describeSkillReadGate(makeTcc(), () => [
89
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => [
81
90
  makeSkillEntry({ state: "allow" }),
82
91
  ]);
83
92
  expect(result).not.toBeNull();
@@ -86,7 +95,7 @@ describe("describeSkillReadGate", () => {
86
95
  });
87
96
 
88
97
  it("returns GateDescriptor with preResolved.state matching skill entry state (deny)", () => {
89
- const result = describeSkillReadGate(makeTcc(), () => [
98
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => [
90
99
  makeSkillEntry({ state: "deny" }),
91
100
  ]);
92
101
  expect(result).not.toBeNull();
@@ -95,7 +104,7 @@ describe("describeSkillReadGate", () => {
95
104
  });
96
105
 
97
106
  it("decision surface is 'skill' and decision value is the skill name", () => {
98
- const result = describeSkillReadGate(makeTcc(), () => [
107
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => [
99
108
  makeSkillEntry({ name: "my-skill" }),
100
109
  ])!;
101
110
  expect(result.decision.surface).toBe("skill");
@@ -103,7 +112,7 @@ describe("describeSkillReadGate", () => {
103
112
  });
104
113
 
105
114
  it("denialContext contains the skill name and read path", () => {
106
- const result = describeSkillReadGate(makeTcc(), () => [
115
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => [
107
116
  makeSkillEntry({ name: "librarian" }),
108
117
  ])!;
109
118
  expect(result.denialContext).toEqual({
@@ -117,6 +126,7 @@ describe("describeSkillReadGate", () => {
117
126
  it("promptDetails includes skill_read source and skillName", () => {
118
127
  const result = describeSkillReadGate(
119
128
  makeTcc({ agentName: "test-agent", toolCallId: "tc-42" }),
129
+ normalizer,
120
130
  () => [makeSkillEntry({ name: "my-skill" })],
121
131
  )!;
122
132
  expect(result.promptDetails).toMatchObject({
@@ -132,6 +142,7 @@ describe("describeSkillReadGate", () => {
132
142
  it("logContext includes skill_read source and skillName", () => {
133
143
  const result = describeSkillReadGate(
134
144
  makeTcc({ agentName: "agent-1" }),
145
+ normalizer,
135
146
  () => [makeSkillEntry({ name: "librarian" })],
136
147
  )!;
137
148
  expect(result.logContext).toMatchObject({
@@ -142,7 +153,9 @@ describe("describeSkillReadGate", () => {
142
153
  });
143
154
 
144
155
  it("surface is 'skill' on the descriptor", () => {
145
- const result = describeSkillReadGate(makeTcc(), () => [makeSkillEntry()])!;
156
+ const result = describeSkillReadGate(makeTcc(), normalizer, () => [
157
+ makeSkillEntry(),
158
+ ])!;
146
159
  expect(result.surface).toBe("skill");
147
160
  });
148
161
  });
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type { AccessPath } from "#src/access-intent/access-path";
4
4
  import { ToolCallGatePipeline } from "#src/handlers/gates/tool-call-gate-pipeline";
5
+ import { PathNormalizer } from "#src/path-normalizer";
5
6
 
6
7
  import {
7
8
  makeGateInputs,
@@ -172,7 +173,7 @@ describe("ToolCallGatePipeline", () => {
172
173
  expect(mockBashProgramParse).toHaveBeenCalledTimes(1);
173
174
  expect(mockBashProgramParse).toHaveBeenCalledWith(
174
175
  "echo hello",
175
- "/test/project",
176
+ expect.any(PathNormalizer),
176
177
  );
177
178
  });
178
179
 
@@ -53,6 +53,7 @@ describe("describeToolGate", () => {
53
53
  makeTcc({ toolName: "read" }),
54
54
  makeCheckResult("ask"),
55
55
  makeFormatter(),
56
+ "linux",
56
57
  );
57
58
  expect(desc.surface).toBe("read");
58
59
  expect(desc.decision.surface).toBe("read");
@@ -63,6 +64,7 @@ describe("describeToolGate", () => {
63
64
  makeTcc({ toolName: "write" }),
64
65
  makeCheckResult("ask"),
65
66
  makeFormatter(),
67
+ "linux",
66
68
  );
67
69
  expect(desc.decision.value).toBe("write");
68
70
  });
@@ -76,6 +78,7 @@ describe("describeToolGate", () => {
76
78
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
77
79
  check,
78
80
  makeFormatter(),
81
+ "linux",
79
82
  );
80
83
  expect(desc.surface).toBe("bash");
81
84
  expect(desc.decision.surface).toBe("bash");
@@ -91,6 +94,7 @@ describe("describeToolGate", () => {
91
94
  makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
92
95
  check,
93
96
  makeFormatter(),
97
+ "linux",
94
98
  );
95
99
  expect(desc.surface).toBe("mcp");
96
100
  expect(desc.decision.surface).toBe("mcp");
@@ -99,7 +103,7 @@ describe("describeToolGate", () => {
99
103
 
100
104
  it("populates denialContext with kind 'tool' and check result", () => {
101
105
  const check = makeCheckResult("deny", { toolName: "read" });
102
- const desc = describeToolGate(makeTcc(), check, makeFormatter());
106
+ const desc = describeToolGate(makeTcc(), check, makeFormatter(), "linux");
103
107
  expect(desc.denialContext).toEqual({
104
108
  kind: "tool",
105
109
  check,
@@ -114,6 +118,7 @@ describe("describeToolGate", () => {
114
118
  makeTcc({ agentName: "my-agent" }),
115
119
  check,
116
120
  makeFormatter(),
121
+ "linux",
117
122
  );
118
123
  expect(desc.denialContext.agentName).toBe("my-agent");
119
124
  });
@@ -124,6 +129,7 @@ describe("describeToolGate", () => {
124
129
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
125
130
  check,
126
131
  makeFormatter(),
132
+ "linux",
127
133
  );
128
134
  expect(desc.denialContext).toMatchObject({
129
135
  kind: "tool",
@@ -140,6 +146,7 @@ describe("describeToolGate", () => {
140
146
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
141
147
  check,
142
148
  makeFormatter(),
149
+ "linux",
143
150
  );
144
151
  expect(desc.sessionApproval).toBeDefined();
145
152
  expect(desc.sessionApproval?.surface).toBe("bash");
@@ -156,6 +163,7 @@ describe("describeToolGate", () => {
156
163
  }),
157
164
  check,
158
165
  makeFormatter(),
166
+ "linux",
159
167
  );
160
168
  expect(desc.sessionApproval?.surface).toBe("edit");
161
169
  expect(desc.sessionApproval?.representativePattern).toBe("/test/project/*");
@@ -174,6 +182,7 @@ describe("describeToolGate", () => {
174
182
  }),
175
183
  check,
176
184
  makeFormatter(),
185
+ "linux",
177
186
  );
178
187
  expect(desc.sessionApproval?.representativePattern).toBe(
179
188
  "/test/project/src/*",
@@ -186,6 +195,7 @@ describe("describeToolGate", () => {
186
195
  makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
187
196
  check,
188
197
  makeFormatter(),
198
+ "linux",
189
199
  );
190
200
  expect(desc.promptDetails).toMatchObject({
191
201
  source: "tool_call",
@@ -203,6 +213,7 @@ describe("describeToolGate", () => {
203
213
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
204
214
  check,
205
215
  makeFormatter(),
216
+ "linux",
206
217
  );
207
218
  expect(desc.logContext).toMatchObject({
208
219
  source: "tool_call",
@@ -216,6 +227,7 @@ describe("describeToolGate", () => {
216
227
  makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
217
228
  makeCheckResult("ask", { toolName: "edit" }),
218
229
  makeFormatter(),
230
+ "linux",
219
231
  );
220
232
  expect(desc.surface).toBe("edit");
221
233
  expect(desc.input).toEqual({ path: "/a.ts" });
@@ -10,6 +10,7 @@ import { GateRunner } from "#src/handlers/gates/runner";
10
10
  import type { SkillInputGateInputs } from "#src/handlers/gates/skill-input-gate-pipeline";
11
11
  import type { ToolCallGateInputs } from "#src/handlers/gates/tool-call-gate-pipeline";
12
12
  import type { ToolCallContext } from "#src/handlers/gates/types";
13
+ import { PathNormalizer } from "#src/path-normalizer";
13
14
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
14
15
  import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
15
16
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
@@ -253,6 +254,8 @@ export function makeGateInputs(
253
254
  getActiveSkillEntries?: () => SkillPromptEntry[];
254
255
  getInfrastructureReadDirs?: () => string[];
255
256
  getToolPreviewLimits?: () => ToolPreviewFormatterOptions;
257
+ getPathNormalizer?: () => PathNormalizer;
258
+ getPlatform?: () => NodeJS.Platform;
256
259
  } = {},
257
260
  ): ToolCallGateInputs {
258
261
  return {
@@ -268,6 +271,13 @@ export function makeGateInputs(
268
271
  toolTextSummaryMaxLength: 100,
269
272
  toolInputLogPreviewMaxLength: 200,
270
273
  })),
274
+ getPathNormalizer:
275
+ overrides.getPathNormalizer ??
276
+ vi.fn<() => PathNormalizer>(
277
+ () => new PathNormalizer(process.platform, "/test/cwd"),
278
+ ),
279
+ getPlatform:
280
+ overrides.getPlatform ?? vi.fn<() => NodeJS.Platform>(() => "linux"),
271
281
  };
272
282
  }
273
283
 
@@ -126,6 +126,7 @@ export function makeRealSession(overrides?: {
126
126
  sessionRules?: SessionRules;
127
127
  configStore?: SessionConfigStore;
128
128
  gateway?: PromptingGatewayLifecycle;
129
+ platform?: NodeJS.Platform;
129
130
  }): {
130
131
  session: PermissionSession;
131
132
  paths: ExtensionPaths;
@@ -146,6 +147,7 @@ export function makeRealSession(overrides?: {
146
147
  const sessionRules = overrides?.sessionRules ?? new SessionRules();
147
148
  const configStore = overrides?.configStore ?? makeConfigStore();
148
149
  const gateway = overrides?.gateway ?? makeGateway();
150
+ const platform = overrides?.platform ?? process.platform;
149
151
  const session = new PermissionSession(
150
152
  paths,
151
153
  forwarding,
@@ -153,6 +155,7 @@ export function makeRealSession(overrides?: {
153
155
  sessionRules,
154
156
  configStore,
155
157
  gateway,
158
+ platform,
156
159
  );
157
160
  return {
158
161
  session,