@gotgenes/pi-permission-system 5.5.0 → 5.6.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.
- package/CHANGELOG.md +23 -0
- package/package.json +1 -1
- package/src/handlers/before-agent-start.ts +7 -7
- package/src/handlers/gates/bash-external-directory.ts +70 -67
- package/src/handlers/gates/descriptor.ts +115 -0
- package/src/handlers/gates/external-directory.ts +62 -130
- package/src/handlers/gates/index.ts +12 -4
- package/src/handlers/gates/runner.ts +144 -0
- package/src/handlers/gates/skill-read.ts +40 -59
- package/src/handlers/gates/tool.ts +35 -104
- package/src/handlers/gates/types.ts +0 -2
- package/src/handlers/input.ts +3 -3
- package/src/handlers/lifecycle.ts +21 -21
- package/src/handlers/tool-call.ts +121 -20
- package/src/handlers/types.ts +20 -7
- package/src/index.ts +6 -1
- package/src/runtime.ts +17 -9
- package/tests/handlers/before-agent-start.test.ts +17 -27
- package/tests/handlers/gates/bash-external-directory.test.ts +129 -184
- package/tests/handlers/gates/external-directory.test.ts +118 -264
- package/tests/handlers/gates/runner.test.ts +361 -0
- package/tests/handlers/gates/skill-read.test.ts +86 -137
- package/tests/handlers/gates/tool.test.ts +109 -346
- package/tests/handlers/input-events.test.ts +10 -21
- package/tests/handlers/input.test.ts +26 -43
- package/tests/handlers/lifecycle.test.ts +47 -66
- package/tests/handlers/tool-call-events.test.ts +29 -40
- package/tests/handlers/tool-call.test.ts +19 -30
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type {
|
|
4
|
+
GateBypass,
|
|
5
|
+
GateDescriptor,
|
|
6
|
+
} from "../../../src/handlers/gates/descriptor";
|
|
7
|
+
import {
|
|
8
|
+
isGateBypass,
|
|
9
|
+
isGateDescriptor,
|
|
10
|
+
} from "../../../src/handlers/gates/descriptor";
|
|
11
|
+
import { describeExternalDirectoryGate } from "../../../src/handlers/gates/external-directory";
|
|
4
12
|
import type { ToolCallContext } from "../../../src/handlers/gates/types";
|
|
5
|
-
import type { HandlerDeps } from "../../../src/handlers/types";
|
|
6
|
-
import type { PermissionEventBus } from "../../../src/permission-events";
|
|
7
|
-
import type { PermissionCheckResult } from "../../../src/types";
|
|
8
13
|
|
|
9
|
-
// ── helpers
|
|
14
|
+
// ── helpers ───────────────────────────��────────────────────────────��───────
|
|
10
15
|
|
|
11
16
|
function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
12
17
|
return {
|
|
@@ -19,302 +24,151 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
|
|
|
19
24
|
};
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
state: "allow" | "deny" | "ask",
|
|
24
|
-
overrides: Partial<PermissionCheckResult> = {},
|
|
25
|
-
): PermissionCheckResult {
|
|
26
|
-
return {
|
|
27
|
-
state,
|
|
28
|
-
toolName: "external_directory",
|
|
29
|
-
source: "special",
|
|
30
|
-
origin: "builtin",
|
|
31
|
-
...overrides,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function makeEvents(): PermissionEventBus {
|
|
36
|
-
return { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) };
|
|
37
|
-
}
|
|
27
|
+
// ── tests ────────────────────��────────────────────────────────────��────────
|
|
38
28
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
config: { debugLog: false, permissionReviewLog: true, yoloMode: false },
|
|
45
|
-
runtimeContext: {} as HandlerDeps["runtime"]["runtimeContext"],
|
|
46
|
-
permissionManager: {
|
|
47
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
48
|
-
},
|
|
49
|
-
sessionRules: {
|
|
50
|
-
approve: vi.fn(),
|
|
51
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
52
|
-
clear: vi.fn(),
|
|
53
|
-
},
|
|
54
|
-
writeReviewLog: vi.fn(),
|
|
55
|
-
...overrides,
|
|
56
|
-
} as unknown as HandlerDeps["runtime"];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function makeDeps(overrides: Record<string, unknown> = {}): HandlerDeps {
|
|
60
|
-
const { runtime: runtimeOverrides, events, ...rest } = overrides;
|
|
61
|
-
return {
|
|
62
|
-
runtime: makeRuntime(runtimeOverrides as Record<string, unknown>),
|
|
63
|
-
events: events ?? makeEvents(),
|
|
64
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
65
|
-
promptPermission: vi
|
|
66
|
-
.fn()
|
|
67
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
68
|
-
...rest,
|
|
69
|
-
} as unknown as HandlerDeps;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── tests ──────────────────────────────────────────────────────────────────
|
|
73
|
-
|
|
74
|
-
describe("evaluateExternalDirectoryGate", () => {
|
|
75
|
-
it("returns null when no CWD", async () => {
|
|
76
|
-
const tcc = makeTcc({ cwd: undefined });
|
|
77
|
-
const result = await evaluateExternalDirectoryGate(tcc, makeDeps());
|
|
29
|
+
describe("describeExternalDirectoryGate", () => {
|
|
30
|
+
it("returns null when no CWD", () => {
|
|
31
|
+
const result = describeExternalDirectoryGate(makeTcc({ cwd: undefined }), [
|
|
32
|
+
"/test/agent",
|
|
33
|
+
]);
|
|
78
34
|
expect(result).toBeNull();
|
|
79
35
|
});
|
|
80
36
|
|
|
81
|
-
it("returns null when tool is not path-bearing",
|
|
82
|
-
const
|
|
83
|
-
|
|
37
|
+
it("returns null when tool is not path-bearing", () => {
|
|
38
|
+
const result = describeExternalDirectoryGate(
|
|
39
|
+
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
40
|
+
["/test/agent"],
|
|
41
|
+
);
|
|
84
42
|
expect(result).toBeNull();
|
|
85
43
|
});
|
|
86
44
|
|
|
87
|
-
it("returns null when path is inside CWD",
|
|
88
|
-
const
|
|
89
|
-
|
|
45
|
+
it("returns null when path is inside CWD", () => {
|
|
46
|
+
const result = describeExternalDirectoryGate(
|
|
47
|
+
makeTcc({ input: { path: "/test/project/src/index.ts" } }),
|
|
48
|
+
["/test/agent"],
|
|
49
|
+
);
|
|
90
50
|
expect(result).toBeNull();
|
|
91
51
|
});
|
|
92
52
|
|
|
93
|
-
// ── Pi infrastructure read bypass
|
|
53
|
+
// ── Pi infrastructure read bypass ─────────────────���────────────────────
|
|
94
54
|
|
|
95
|
-
it("
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
input: { path: "/test/agent/git/some-package/SKILL.md" },
|
|
101
|
-
});
|
|
102
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
103
|
-
expect(result).toEqual({ action: "allow" });
|
|
104
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
105
|
-
"permissions:decision",
|
|
106
|
-
expect.objectContaining({
|
|
107
|
-
resolution: "infrastructure_auto_allowed",
|
|
108
|
-
result: "allow",
|
|
55
|
+
it("returns GateBypass for read targeting an infra dir", () => {
|
|
56
|
+
const result = describeExternalDirectoryGate(
|
|
57
|
+
makeTcc({
|
|
58
|
+
toolName: "read",
|
|
59
|
+
input: { path: "/test/agent/git/some-package/SKILL.md" },
|
|
109
60
|
}),
|
|
61
|
+
["/test/agent", "/test/agent/git"],
|
|
110
62
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
config: {
|
|
119
|
-
debugLog: false,
|
|
120
|
-
permissionReviewLog: true,
|
|
121
|
-
yoloMode: false,
|
|
122
|
-
piInfrastructureReadPaths: ["/custom/infra"],
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
events,
|
|
126
|
-
});
|
|
127
|
-
const tcc = makeTcc({
|
|
128
|
-
toolName: "read",
|
|
129
|
-
input: { path: "/custom/infra/SKILL.md" },
|
|
130
|
-
});
|
|
131
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
132
|
-
expect(result).toEqual({ action: "allow" });
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("does NOT bypass for write tools targeting infra dirs", async () => {
|
|
136
|
-
const deps = makeDeps({
|
|
137
|
-
runtime: {
|
|
138
|
-
permissionManager: {
|
|
139
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
140
|
-
},
|
|
141
|
-
},
|
|
63
|
+
expect(result).not.toBeNull();
|
|
64
|
+
expect(isGateBypass(result)).toBe(true);
|
|
65
|
+
const bypass = result as GateBypass;
|
|
66
|
+
expect(bypass.action).toBe("allow");
|
|
67
|
+
expect(bypass.decision).toMatchObject({
|
|
68
|
+
resolution: "infrastructure_auto_allowed",
|
|
69
|
+
result: "allow",
|
|
142
70
|
});
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
input: { path: "/test/agent/git/some-file.ts", content: "x" },
|
|
71
|
+
expect(bypass.log).toMatchObject({
|
|
72
|
+
event: "permission_request.infrastructure_auto_allowed",
|
|
146
73
|
});
|
|
147
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
148
|
-
expect(result).toMatchObject({ action: "block" });
|
|
149
74
|
});
|
|
150
75
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
runtime: {
|
|
157
|
-
permissionManager: {
|
|
158
|
-
checkPermission: vi.fn().mockReturnValue(
|
|
159
|
-
makeCheckResult("allow", {
|
|
160
|
-
source: "session",
|
|
161
|
-
matchedPattern: "/outside/project/*",
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
164
|
-
},
|
|
165
|
-
sessionRules: {
|
|
166
|
-
approve: vi.fn(),
|
|
167
|
-
getRuleset: vi.fn().mockReturnValue([
|
|
168
|
-
{
|
|
169
|
-
surface: "external_directory",
|
|
170
|
-
pattern: "/outside/project/*",
|
|
171
|
-
action: "allow",
|
|
172
|
-
},
|
|
173
|
-
]),
|
|
174
|
-
clear: vi.fn(),
|
|
175
|
-
},
|
|
176
|
-
},
|
|
177
|
-
events,
|
|
178
|
-
});
|
|
179
|
-
const tcc = makeTcc();
|
|
180
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
181
|
-
expect(result).toEqual({ action: "allow" });
|
|
182
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
183
|
-
"permissions:decision",
|
|
184
|
-
expect.objectContaining({
|
|
185
|
-
resolution: "session_approved",
|
|
186
|
-
matchedPattern: "/outside/project/*",
|
|
76
|
+
it("returns GateBypass respecting custom infraDirs", () => {
|
|
77
|
+
const result = describeExternalDirectoryGate(
|
|
78
|
+
makeTcc({
|
|
79
|
+
toolName: "read",
|
|
80
|
+
input: { path: "/custom/infra/SKILL.md" },
|
|
187
81
|
}),
|
|
82
|
+
["/custom/infra"],
|
|
188
83
|
);
|
|
84
|
+
expect(isGateBypass(result)).toBe(true);
|
|
189
85
|
});
|
|
190
86
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
runtime: {
|
|
197
|
-
permissionManager: {
|
|
198
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
199
|
-
},
|
|
200
|
-
},
|
|
201
|
-
events,
|
|
202
|
-
});
|
|
203
|
-
const tcc = makeTcc();
|
|
204
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
205
|
-
expect(result).toMatchObject({ action: "block" });
|
|
206
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
207
|
-
"permissions:decision",
|
|
208
|
-
expect.objectContaining({
|
|
209
|
-
surface: "external_directory",
|
|
210
|
-
result: "deny",
|
|
211
|
-
resolution: "policy_deny",
|
|
87
|
+
it("does NOT bypass for write tools targeting infra dirs", () => {
|
|
88
|
+
const result = describeExternalDirectoryGate(
|
|
89
|
+
makeTcc({
|
|
90
|
+
toolName: "write",
|
|
91
|
+
input: { path: "/test/agent/git/some-file.ts", content: "x" },
|
|
212
92
|
}),
|
|
93
|
+
["/test/agent", "/test/agent/git"],
|
|
213
94
|
);
|
|
95
|
+
// Should be a GateDescriptor (needs permission check), not a bypass
|
|
96
|
+
expect(result).not.toBeNull();
|
|
97
|
+
expect(isGateDescriptor(result)).toBe(true);
|
|
214
98
|
});
|
|
215
99
|
|
|
216
|
-
// ──
|
|
100
|
+
// ── GateDescriptor for external paths ─────────────────────────────────��
|
|
217
101
|
|
|
218
|
-
it("
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
};
|
|
224
|
-
const deps = makeDeps({
|
|
225
|
-
runtime: {
|
|
226
|
-
permissionManager: {
|
|
227
|
-
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
228
|
-
},
|
|
229
|
-
sessionRules,
|
|
230
|
-
},
|
|
231
|
-
promptPermission: vi
|
|
232
|
-
.fn()
|
|
233
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
234
|
-
});
|
|
235
|
-
const tcc = makeTcc();
|
|
236
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
237
|
-
expect(result).toEqual({ action: "allow" });
|
|
238
|
-
expect(sessionRules.approve).not.toHaveBeenCalled();
|
|
102
|
+
it("returns GateDescriptor with surface 'external_directory'", () => {
|
|
103
|
+
const result = describeExternalDirectoryGate(makeTcc(), ["/test/agent"]);
|
|
104
|
+
expect(isGateDescriptor(result)).toBe(true);
|
|
105
|
+
const desc = result as GateDescriptor;
|
|
106
|
+
expect(desc.surface).toBe("external_directory");
|
|
239
107
|
});
|
|
240
108
|
|
|
241
|
-
|
|
109
|
+
it("decision value is the external path", () => {
|
|
110
|
+
const result = describeExternalDirectoryGate(
|
|
111
|
+
makeTcc({ input: { path: "/outside/project/file.ts" } }),
|
|
112
|
+
["/test/agent"],
|
|
113
|
+
) as GateDescriptor;
|
|
114
|
+
expect(result.decision.value).toBe("/outside/project/file.ts");
|
|
115
|
+
expect(result.decision.surface).toBe("external_directory");
|
|
116
|
+
});
|
|
242
117
|
|
|
243
|
-
it("
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
});
|
|
260
|
-
const tcc = makeTcc();
|
|
261
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
262
|
-
expect(result).toEqual({ action: "allow" });
|
|
263
|
-
expect(sessionRules.approve).toHaveBeenCalledWith(
|
|
118
|
+
it("input contains normalized path for checkPermission", () => {
|
|
119
|
+
const result = describeExternalDirectoryGate(
|
|
120
|
+
makeTcc({ input: { path: "/outside/project/file.ts" } }),
|
|
121
|
+
["/test/agent"],
|
|
122
|
+
) as GateDescriptor;
|
|
123
|
+
expect(result.input).toHaveProperty("path");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("sessionApproval uses deriveApprovalPattern", () => {
|
|
127
|
+
const result = describeExternalDirectoryGate(
|
|
128
|
+
makeTcc({ input: { path: "/outside/project/file.ts" } }),
|
|
129
|
+
["/test/agent"],
|
|
130
|
+
) as GateDescriptor;
|
|
131
|
+
expect(result.sessionApproval).toBeDefined();
|
|
132
|
+
expect(result.sessionApproval).toHaveProperty(
|
|
133
|
+
"surface",
|
|
264
134
|
"external_directory",
|
|
265
|
-
expect.any(String),
|
|
266
135
|
);
|
|
136
|
+
expect(result.sessionApproval).toHaveProperty("pattern");
|
|
267
137
|
});
|
|
268
138
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
events,
|
|
280
|
-
promptPermission: vi
|
|
281
|
-
.fn()
|
|
282
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
283
|
-
});
|
|
284
|
-
const tcc = makeTcc();
|
|
285
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
286
|
-
expect(result).toMatchObject({ action: "block" });
|
|
287
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
288
|
-
"permissions:decision",
|
|
289
|
-
expect.objectContaining({
|
|
290
|
-
result: "deny",
|
|
291
|
-
resolution: "user_denied",
|
|
292
|
-
}),
|
|
139
|
+
it("messages contain the external path", () => {
|
|
140
|
+
const result = describeExternalDirectoryGate(
|
|
141
|
+
makeTcc({ input: { path: "/outside/project/file.ts" } }),
|
|
142
|
+
["/test/agent"],
|
|
143
|
+
) as GateDescriptor;
|
|
144
|
+
expect(result.messages.denyReason).toContain("/outside/project/file.ts");
|
|
145
|
+
expect(result.messages.unavailableReason).toContain(
|
|
146
|
+
"/outside/project/file.ts",
|
|
293
147
|
);
|
|
294
148
|
});
|
|
295
149
|
|
|
296
|
-
|
|
150
|
+
it("promptDetails includes path and tool_call source", () => {
|
|
151
|
+
const result = describeExternalDirectoryGate(
|
|
152
|
+
makeTcc({ toolName: "read", agentName: "agent-1", toolCallId: "tc-5" }),
|
|
153
|
+
["/test/agent"],
|
|
154
|
+
) as GateDescriptor;
|
|
155
|
+
expect(result.promptDetails).toMatchObject({
|
|
156
|
+
source: "tool_call",
|
|
157
|
+
agentName: "agent-1",
|
|
158
|
+
toolCallId: "tc-5",
|
|
159
|
+
toolName: "read",
|
|
160
|
+
path: "/outside/project/file.ts",
|
|
161
|
+
});
|
|
162
|
+
});
|
|
297
163
|
|
|
298
|
-
it("
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
},
|
|
306
|
-
events,
|
|
307
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
164
|
+
it("logContext includes path and message", () => {
|
|
165
|
+
const result = describeExternalDirectoryGate(makeTcc(), [
|
|
166
|
+
"/test/agent",
|
|
167
|
+
]) as GateDescriptor;
|
|
168
|
+
expect(result.logContext).toMatchObject({
|
|
169
|
+
source: "tool_call",
|
|
170
|
+
path: "/outside/project/file.ts",
|
|
308
171
|
});
|
|
309
|
-
|
|
310
|
-
const result = await evaluateExternalDirectoryGate(tcc, deps);
|
|
311
|
-
expect(result).toMatchObject({ action: "block" });
|
|
312
|
-
expect(events.emit).toHaveBeenCalledWith(
|
|
313
|
-
"permissions:decision",
|
|
314
|
-
expect.objectContaining({
|
|
315
|
-
result: "deny",
|
|
316
|
-
resolution: "confirmation_unavailable",
|
|
317
|
-
}),
|
|
318
|
-
);
|
|
172
|
+
expect(result.logContext.message).toBeDefined();
|
|
319
173
|
});
|
|
320
174
|
});
|