@gotgenes/pi-permission-system 5.18.0 → 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.
- package/CHANGELOG.md +34 -0
- package/README.md +20 -19
- package/package.json +11 -16
- package/src/handlers/gates/bash-path.ts +8 -0
- package/src/handlers/gates/path.ts +12 -1
- package/src/handlers/gates/skill-read.ts +4 -0
- package/src/handlers/lifecycle.ts +1 -1
- package/src/handlers/permission-gate-handler.ts +1 -1
- package/src/pattern-suggest.ts +5 -0
- package/tests/config-modal.test.ts +43 -58
- package/tests/config-reporter.test.ts +31 -34
- package/tests/handlers/external-directory-integration.test.ts +3 -3
- package/tests/handlers/gates/bash-path.test.ts +57 -3
- package/tests/handlers/gates/path.test.ts +82 -9
- package/tests/handlers/tool-call.test.ts +2 -2
- package/tests/pattern-suggest.test.ts +15 -0
- package/tests/permission-manager-unified.test.ts +26 -0
- package/tests/permission-system.test.ts +313 -358
|
@@ -119,7 +119,7 @@ describe("describeBashPathGate", () => {
|
|
|
119
119
|
it("returns GateDescriptor when a token evaluates to ask", async () => {
|
|
120
120
|
const checkPermission = vi
|
|
121
121
|
.fn<CheckPermissionFn>()
|
|
122
|
-
.mockReturnValue(makeCheckResult({ state: "ask" }));
|
|
122
|
+
.mockReturnValue(makeCheckResult({ state: "ask", matchedPattern: "*" }));
|
|
123
123
|
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
124
124
|
const result = await describeBashPathGate(
|
|
125
125
|
makeTcc({ input: { command: "cat .env" } }),
|
|
@@ -135,7 +135,9 @@ describe("describeBashPathGate", () => {
|
|
|
135
135
|
it("descriptor includes triggering token in prompt message", async () => {
|
|
136
136
|
const checkPermission = vi
|
|
137
137
|
.fn<CheckPermissionFn>()
|
|
138
|
-
.mockReturnValue(
|
|
138
|
+
.mockReturnValue(
|
|
139
|
+
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
140
|
+
);
|
|
139
141
|
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
140
142
|
const result = (await describeBashPathGate(
|
|
141
143
|
makeTcc({ input: { command: "cat .env" } }),
|
|
@@ -149,7 +151,9 @@ describe("describeBashPathGate", () => {
|
|
|
149
151
|
it("descriptor decision uses surface 'path'", async () => {
|
|
150
152
|
const checkPermission = vi
|
|
151
153
|
.fn<CheckPermissionFn>()
|
|
152
|
-
.mockReturnValue(
|
|
154
|
+
.mockReturnValue(
|
|
155
|
+
makeCheckResult({ state: "deny", matchedPattern: "*.env" }),
|
|
156
|
+
);
|
|
153
157
|
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
154
158
|
const result = (await describeBashPathGate(
|
|
155
159
|
makeTcc({ input: { command: "cat .env" } }),
|
|
@@ -257,4 +261,54 @@ describe("describeBashPathGate", () => {
|
|
|
257
261
|
expect(isGateDescriptor(result)).toBe(true);
|
|
258
262
|
expect((result as GateDescriptor).preCheck?.state).toBe("deny");
|
|
259
263
|
});
|
|
264
|
+
|
|
265
|
+
it("returns null when all tokens match only the universal default", async () => {
|
|
266
|
+
const checkPermission = vi.fn<CheckPermissionFn>().mockReturnValue(
|
|
267
|
+
makeCheckResult({
|
|
268
|
+
state: "ask",
|
|
269
|
+
matchedPattern: undefined,
|
|
270
|
+
source: "special",
|
|
271
|
+
origin: "builtin",
|
|
272
|
+
}),
|
|
273
|
+
);
|
|
274
|
+
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
275
|
+
const result = await describeBashPathGate(
|
|
276
|
+
makeTcc({ input: { command: "cat .env" } }),
|
|
277
|
+
checkPermission,
|
|
278
|
+
getSessionRuleset,
|
|
279
|
+
);
|
|
280
|
+
expect(result).toBeNull();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("ignores tokens matching universal default but fires for explicit rule matches", async () => {
|
|
284
|
+
const checkPermission = vi
|
|
285
|
+
.fn<CheckPermissionFn>()
|
|
286
|
+
.mockImplementation((_surface, input) => {
|
|
287
|
+
const record = input as Record<string, unknown>;
|
|
288
|
+
if (record.path === ".env") {
|
|
289
|
+
return makeCheckResult({
|
|
290
|
+
state: "deny",
|
|
291
|
+
matchedPattern: "*.env",
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
// Other tokens match only the universal default
|
|
295
|
+
return makeCheckResult({
|
|
296
|
+
state: "ask",
|
|
297
|
+
matchedPattern: undefined,
|
|
298
|
+
source: "special",
|
|
299
|
+
origin: "builtin",
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
const getSessionRuleset = vi.fn<() => Rule[]>().mockReturnValue([]);
|
|
303
|
+
const result = await describeBashPathGate(
|
|
304
|
+
makeTcc({ input: { command: "cat src/foo.ts .env" } }),
|
|
305
|
+
checkPermission,
|
|
306
|
+
getSessionRuleset,
|
|
307
|
+
);
|
|
308
|
+
expect(result).not.toBeNull();
|
|
309
|
+
expect(isGateDescriptor(result)).toBe(true);
|
|
310
|
+
const desc = result as GateDescriptor;
|
|
311
|
+
expect(desc.preCheck?.state).toBe("deny");
|
|
312
|
+
expect(desc.decision.value).toBe(".env");
|
|
313
|
+
});
|
|
260
314
|
});
|
|
@@ -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?:
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
},
|
|
@@ -121,6 +121,21 @@ describe("suggestSessionPattern", () => {
|
|
|
121
121
|
});
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
+
describe("path surface", () => {
|
|
125
|
+
it("returns directory-scoped pattern for a file path", () => {
|
|
126
|
+
const result = suggestSessionPattern("path", "src/.env");
|
|
127
|
+
expect(result).toMatchObject({
|
|
128
|
+
surface: "path",
|
|
129
|
+
pattern: "src/*",
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("label includes path pattern", () => {
|
|
134
|
+
const result = suggestSessionPattern("path", "src/.env");
|
|
135
|
+
expect(result.label).toBe('Yes, allow path "src/*" for this session');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
124
139
|
describe("path-bearing tool surfaces", () => {
|
|
125
140
|
it("returns directory-scoped pattern for read with a file path", () => {
|
|
126
141
|
const result = suggestSessionPattern("read", "/outside/project/file.ts");
|
|
@@ -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", () => {
|