@gotgenes/pi-permission-system 5.18.1 → 5.18.2

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.
@@ -4,6 +4,7 @@ import type { GateDescriptor } from "../../../src/handlers/gates/descriptor";
4
4
  import { isGateDescriptor } from "../../../src/handlers/gates/descriptor";
5
5
  import { describePathGate } from "../../../src/handlers/gates/path";
6
6
  import type { ToolCallContext } from "../../../src/handlers/gates/types";
7
+ import type { Rule } from "../../../src/rule";
7
8
  import type { PermissionCheckResult } from "../../../src/types";
8
9
 
9
10
  // ── helpers ────────────────────────────────────────────────────────────────
@@ -35,17 +36,20 @@ type CheckPermissionFn = (
35
36
  surface: string,
36
37
  input: unknown,
37
38
  agentName?: string,
38
- sessionRules?: unknown[],
39
+ sessionRules?: Rule[],
39
40
  ) => PermissionCheckResult;
40
41
 
41
42
  // ── tests ──────────────────────────────────────────────────────────────────
42
43
 
43
44
  describe("describePathGate", () => {
45
+ const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
46
+
44
47
  it("returns null for non-path-bearing tools", () => {
45
48
  const checkPermission = vi.fn<CheckPermissionFn>();
46
49
  const result = describePathGate(
47
50
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
48
51
  checkPermission,
52
+ getSessionRuleset,
49
53
  );
50
54
  expect(result).toBeNull();
51
55
  expect(checkPermission).not.toHaveBeenCalled();
@@ -56,6 +60,7 @@ describe("describePathGate", () => {
56
60
  const result = describePathGate(
57
61
  makeTcc({ toolName: "read", input: {} }),
58
62
  checkPermission,
63
+ getSessionRuleset,
59
64
  );
60
65
  expect(result).toBeNull();
61
66
  });
@@ -64,10 +69,49 @@ describe("describePathGate", () => {
64
69
  const checkPermission = vi
65
70
  .fn<CheckPermissionFn>()
66
71
  .mockReturnValue(makeCheckResult({ state: "allow" }));
67
- const result = describePathGate(makeTcc(), checkPermission);
72
+ const result = describePathGate(
73
+ makeTcc(),
74
+ checkPermission,
75
+ getSessionRuleset,
76
+ );
68
77
  expect(result).toBeNull();
69
78
  });
70
79
 
80
+ it("returns null when matchedPattern is undefined (universal default)", () => {
81
+ const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
82
+ makeCheckResult({
83
+ state: "ask",
84
+ matchedPattern: undefined,
85
+ source: "special",
86
+ origin: "builtin",
87
+ }),
88
+ );
89
+ const result = describePathGate(
90
+ makeTcc(),
91
+ checkPermission,
92
+ getSessionRuleset,
93
+ );
94
+ expect(result).toBeNull();
95
+ });
96
+
97
+ it("returns GateDescriptor when matchedPattern is defined (explicit path rule)", () => {
98
+ const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
99
+ makeCheckResult({
100
+ state: "ask",
101
+ matchedPattern: "*.env",
102
+ source: "special",
103
+ origin: "global",
104
+ }),
105
+ );
106
+ const result = describePathGate(
107
+ makeTcc(),
108
+ checkPermission,
109
+ getSessionRuleset,
110
+ );
111
+ expect(result).not.toBeNull();
112
+ expect(isGateDescriptor(result)).toBe(true);
113
+ });
114
+
71
115
  it("returns GateDescriptor when path check result is deny", () => {
72
116
  const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
73
117
  makeCheckResult({
@@ -75,7 +119,11 @@ describe("describePathGate", () => {
75
119
  matchedPattern: "*.env",
76
120
  }),
77
121
  );
78
- const result = describePathGate(makeTcc(), checkPermission);
122
+ const result = describePathGate(
123
+ makeTcc(),
124
+ checkPermission,
125
+ getSessionRuleset,
126
+ );
79
127
  expect(result).not.toBeNull();
80
128
  expect(isGateDescriptor(result)).toBe(true);
81
129
  const desc = result as GateDescriptor;
@@ -90,7 +138,11 @@ describe("describePathGate", () => {
90
138
  matchedPattern: "*.env",
91
139
  }),
92
140
  );
93
- const result = describePathGate(makeTcc(), checkPermission);
141
+ const result = describePathGate(
142
+ makeTcc(),
143
+ checkPermission,
144
+ getSessionRuleset,
145
+ );
94
146
  expect(result).not.toBeNull();
95
147
  expect(isGateDescriptor(result)).toBe(true);
96
148
  const desc = result as GateDescriptor;
@@ -101,10 +153,11 @@ describe("describePathGate", () => {
101
153
  it("descriptor has correct session approval surface and pattern", () => {
102
154
  const checkPermission = vi
103
155
  .fn<CheckPermissionFn>()
104
- .mockReturnValue(makeCheckResult({ state: "ask" }));
156
+ .mockReturnValue(makeCheckResult({ state: "ask", matchedPattern: "*" }));
105
157
  const result = describePathGate(
106
158
  makeTcc({ input: { path: "/test/project/src/.env" } }),
107
159
  checkPermission,
160
+ getSessionRuleset,
108
161
  ) as GateDescriptor;
109
162
  expect(result.sessionApproval).toBeDefined();
110
163
  expect(result.sessionApproval).toHaveProperty("surface", "path");
@@ -114,10 +167,13 @@ describe("describePathGate", () => {
114
167
  it("descriptor messages reference the file path", () => {
115
168
  const checkPermission = vi
116
169
  .fn<CheckPermissionFn>()
117
- .mockReturnValue(makeCheckResult({ state: "deny" }));
170
+ .mockReturnValue(
171
+ makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
172
+ );
118
173
  const result = describePathGate(
119
174
  makeTcc(),
120
175
  checkPermission,
176
+ getSessionRuleset,
121
177
  ) as GateDescriptor;
122
178
  expect(result.messages.denyReason).toContain(".env");
123
179
  expect(result.messages.unavailableReason).toContain(".env");
@@ -126,24 +182,41 @@ describe("describePathGate", () => {
126
182
  it("descriptor decision uses surface 'path' and the file path as value", () => {
127
183
  const checkPermission = vi
128
184
  .fn<CheckPermissionFn>()
129
- .mockReturnValue(makeCheckResult({ state: "deny" }));
185
+ .mockReturnValue(
186
+ makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
187
+ );
130
188
  const result = describePathGate(
131
189
  makeTcc(),
132
190
  checkPermission,
191
+ getSessionRuleset,
133
192
  ) as GateDescriptor;
134
193
  expect(result.decision.surface).toBe("path");
135
194
  expect(result.decision.value).toBe(".env");
136
195
  });
137
196
 
138
- it("passes agentName to checkPermission", () => {
197
+ it("passes agentName and session rules to checkPermission", () => {
198
+ const sessionRules: Rule[] = [
199
+ {
200
+ surface: "path",
201
+ pattern: "/project/*",
202
+ action: "allow",
203
+ origin: "session",
204
+ },
205
+ ];
206
+ const getSession = vi.fn<() => Rule[]>().mockReturnValue(sessionRules);
139
207
  const checkPermission = vi
140
208
  .fn<CheckPermissionFn>()
141
209
  .mockReturnValue(makeCheckResult({ state: "allow" }));
142
- describePathGate(makeTcc({ agentName: "my-agent" }), checkPermission);
210
+ describePathGate(
211
+ makeTcc({ agentName: "my-agent" }),
212
+ checkPermission,
213
+ getSession,
214
+ );
143
215
  expect(checkPermission).toHaveBeenCalledWith(
144
216
  "path",
145
217
  { path: ".env" },
146
218
  "my-agent",
219
+ sessionRules,
147
220
  );
148
221
  });
149
222
  });
@@ -307,7 +307,7 @@ describe("handleToolCall — path gate (tools)", () => {
307
307
  .mockImplementation(
308
308
  (surface: string, _input: unknown, _agentName?: string) => {
309
309
  if (surface === "path") {
310
- return makePermissionResult("deny");
310
+ return { ...makePermissionResult("deny"), matchedPattern: "*.env" };
311
311
  }
312
312
  return makePermissionResult("allow");
313
313
  },
@@ -354,7 +354,7 @@ describe("handleToolCall — bash path gate", () => {
354
354
  .mockImplementation(
355
355
  (surface: string, _input: unknown, _agentName?: string) => {
356
356
  if (surface === "path") {
357
- return makePermissionResult("deny");
357
+ return { ...makePermissionResult("deny"), matchedPattern: "*.env" };
358
358
  }
359
359
  return makePermissionResult("allow");
360
360
  },
@@ -1209,6 +1209,32 @@ describe("cross-cutting path surface", () => {
1209
1209
  }
1210
1210
  });
1211
1211
 
1212
+ it("universal default produces undefined matchedPattern for gate skip (#58)", () => {
1213
+ const { manager, cleanup } = makeManagerWithConfig({
1214
+ "*": "ask",
1215
+ read: "allow",
1216
+ find: "allow",
1217
+ });
1218
+ try {
1219
+ // No explicit "path" key → matchedPattern must be undefined so the
1220
+ // path gate skips (describePathGate returns null).
1221
+ const result = manager.checkPermission("path", {
1222
+ path: "src/main.ts",
1223
+ });
1224
+ expect(result.state).toBe("ask");
1225
+ expect(result.matchedPattern).toBeUndefined();
1226
+
1227
+ // Meanwhile the tool-level check should allow read.
1228
+ const readResult = manager.checkPermission("read", {
1229
+ path: "src/main.ts",
1230
+ });
1231
+ expect(readResult.state).toBe("allow");
1232
+ expect(readResult.matchedPattern).toBe("*");
1233
+ } finally {
1234
+ cleanup();
1235
+ }
1236
+ });
1237
+
1212
1238
  // ── Last-match-wins ordering ────────────────────────────────────────────
1213
1239
 
1214
1240
  it("last-match-wins: catch-all after deny overrides the deny", () => {