@gotgenes/pi-permission-system 5.18.1 → 5.18.3
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 +31 -0
- package/README.md +19 -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/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/permission-manager-unified.test.ts +26 -0
- package/tests/permission-system.test.ts +313 -358
|
@@ -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
|
},
|
|
@@ -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", () => {
|