@gotgenes/pi-permission-system 5.9.0 → 5.11.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 +30 -0
- package/package.json +1 -1
- package/src/forwarding-manager.ts +1 -1
- package/src/handlers/before-agent-start.ts +76 -76
- package/src/handlers/gates/descriptor.ts +1 -1
- package/src/handlers/index.ts +6 -15
- package/src/handlers/lifecycle.ts +55 -59
- package/src/handlers/permission-gate-handler.ts +346 -0
- package/src/index.ts +46 -54
- package/src/permission-prompter.ts +23 -6
- package/src/permission-session.ts +281 -0
- package/src/runtime.ts +5 -30
- package/src/session-logger.ts +1 -1
- package/src/skill-prompt-sanitizer.ts +15 -4
- package/src/tool-registry.ts +6 -0
- package/tests/handlers/before-agent-start.test.ts +116 -167
- package/tests/handlers/input-events.test.ts +87 -92
- package/tests/handlers/input.test.ts +98 -128
- package/tests/handlers/lifecycle.test.ts +97 -227
- package/tests/handlers/tool-call-events.test.ts +146 -166
- package/tests/handlers/tool-call.test.ts +102 -97
- package/tests/permission-prompter.test.ts +1 -1
- package/tests/permission-session.test.ts +607 -0
- package/tests/runtime.test.ts +2 -77
- package/src/handlers/input.ts +0 -126
- package/src/handlers/tool-call.ts +0 -210
- package/src/handlers/types.ts +0 -90
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
2
|
import { describe, expect, it, vi } from "vitest";
|
|
3
3
|
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
import {
|
|
5
|
+
getEventInput,
|
|
6
|
+
PermissionGateHandler,
|
|
7
|
+
} from "../../src/handlers/permission-gate-handler";
|
|
8
|
+
import type { PermissionSession } from "../../src/permission-session";
|
|
9
|
+
import type { ToolRegistry } from "../../src/tool-registry";
|
|
10
|
+
import type { PermissionCheckResult, PermissionState } from "../../src/types";
|
|
8
11
|
|
|
9
12
|
// ── SDK stubs ──────────────────────────────────────────────────────────────
|
|
10
13
|
vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => {
|
|
@@ -55,49 +58,58 @@ function makePermissionResult(
|
|
|
55
58
|
return { state, toolName: "read", source: "tool", origin: "builtin" };
|
|
56
59
|
}
|
|
57
60
|
|
|
58
|
-
function makeSession(
|
|
61
|
+
function makeSession(
|
|
62
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
63
|
+
): PermissionSession {
|
|
59
64
|
return {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
66
|
+
activate: vi.fn(),
|
|
67
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
68
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("allow")),
|
|
69
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
70
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
71
|
+
approveSessionRule: vi.fn(),
|
|
72
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([]),
|
|
73
|
+
getInfrastructureDirs: vi
|
|
74
|
+
.fn()
|
|
75
|
+
.mockReturnValue(["/test/agent", "/test/agent/git"]),
|
|
76
|
+
getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
77
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
78
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
73
79
|
...overrides,
|
|
80
|
+
} as unknown as PermissionSession;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function makeEvents() {
|
|
84
|
+
return {
|
|
85
|
+
emit: vi.fn(),
|
|
86
|
+
on: vi.fn().mockReturnValue(() => undefined),
|
|
74
87
|
};
|
|
75
88
|
}
|
|
76
89
|
|
|
77
|
-
function
|
|
90
|
+
function makeToolRegistry(overrides: Partial<ToolRegistry> = {}): ToolRegistry {
|
|
78
91
|
return {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
82
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
83
|
-
createPermissionManagerForCwd: vi.fn(),
|
|
84
|
-
refreshExtensionConfig: vi.fn(),
|
|
85
|
-
logResolvedConfigPaths: vi.fn(),
|
|
86
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
87
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
88
|
-
promptPermission: vi
|
|
89
|
-
.fn()
|
|
90
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
91
|
-
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
92
|
-
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
93
|
-
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
94
|
-
stopPermissionRpcHandlers: vi.fn(),
|
|
95
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
96
|
-
setActiveTools: vi.fn(),
|
|
92
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }, { name: "bash" }]),
|
|
93
|
+
setActive: vi.fn(),
|
|
97
94
|
...overrides,
|
|
98
95
|
};
|
|
99
96
|
}
|
|
100
97
|
|
|
98
|
+
function makeHandler(overrides?: {
|
|
99
|
+
session?: Partial<Record<keyof PermissionSession, unknown>>;
|
|
100
|
+
toolRegistry?: Partial<ToolRegistry>;
|
|
101
|
+
}): {
|
|
102
|
+
handler: PermissionGateHandler;
|
|
103
|
+
session: PermissionSession;
|
|
104
|
+
toolRegistry: ToolRegistry;
|
|
105
|
+
} {
|
|
106
|
+
const session = makeSession(overrides?.session);
|
|
107
|
+
const events = makeEvents();
|
|
108
|
+
const toolRegistry = makeToolRegistry(overrides?.toolRegistry);
|
|
109
|
+
const handler = new PermissionGateHandler(session, events, toolRegistry);
|
|
110
|
+
return { handler, session, toolRegistry };
|
|
111
|
+
}
|
|
112
|
+
|
|
101
113
|
// ── getEventInput ──────────────────────────────────────────────────────────
|
|
102
114
|
|
|
103
115
|
describe("getEventInput", () => {
|
|
@@ -127,24 +139,19 @@ describe("getEventInput", () => {
|
|
|
127
139
|
// ── handleToolCall ─────────────────────────────────────────────────────────
|
|
128
140
|
|
|
129
141
|
describe("handleToolCall", () => {
|
|
130
|
-
it("
|
|
131
|
-
const ctx = makeCtx();
|
|
132
|
-
const deps = makeDeps();
|
|
133
|
-
await handleToolCall(deps, makeToolCallEvent("read"), ctx);
|
|
134
|
-
expect(deps.session.runtimeContext).toBe(ctx);
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("starts forwarded permission polling", async () => {
|
|
142
|
+
it("activates session with ctx", async () => {
|
|
138
143
|
const ctx = makeCtx();
|
|
139
|
-
const
|
|
140
|
-
await handleToolCall(
|
|
141
|
-
expect(
|
|
144
|
+
const { handler, session } = makeHandler();
|
|
145
|
+
await handler.handleToolCall(makeToolCallEvent("read"), ctx);
|
|
146
|
+
expect(session.activate).toHaveBeenCalledWith(ctx);
|
|
142
147
|
});
|
|
143
148
|
|
|
144
149
|
it("blocks when tool name cannot be resolved", async () => {
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
const { handler } = makeHandler();
|
|
151
|
+
const result = await handler.handleToolCall(
|
|
152
|
+
{ type: "tool_call" },
|
|
153
|
+
makeCtx(),
|
|
154
|
+
);
|
|
148
155
|
expect(result).toEqual({
|
|
149
156
|
block: true,
|
|
150
157
|
reason: expect.stringContaining("tool"),
|
|
@@ -152,11 +159,12 @@ describe("handleToolCall", () => {
|
|
|
152
159
|
});
|
|
153
160
|
|
|
154
161
|
it("blocks when tool is not registered", async () => {
|
|
155
|
-
const
|
|
156
|
-
|
|
162
|
+
const { handler } = makeHandler({
|
|
163
|
+
toolRegistry: {
|
|
164
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
165
|
+
},
|
|
157
166
|
});
|
|
158
|
-
const result = await handleToolCall(
|
|
159
|
-
deps,
|
|
167
|
+
const result = await handler.handleToolCall(
|
|
160
168
|
makeToolCallEvent("unknown-tool"),
|
|
161
169
|
makeCtx(),
|
|
162
170
|
);
|
|
@@ -164,10 +172,8 @@ describe("handleToolCall", () => {
|
|
|
164
172
|
});
|
|
165
173
|
|
|
166
174
|
it("returns empty object when tool is allowed", async () => {
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const result = await handleToolCall(
|
|
170
|
-
deps,
|
|
175
|
+
const { handler } = makeHandler();
|
|
176
|
+
const result = await handler.handleToolCall(
|
|
171
177
|
makeToolCallEvent("read"),
|
|
172
178
|
makeCtx(),
|
|
173
179
|
);
|
|
@@ -175,17 +181,12 @@ describe("handleToolCall", () => {
|
|
|
175
181
|
});
|
|
176
182
|
|
|
177
183
|
it("blocks when tool is denied by policy", async () => {
|
|
178
|
-
const
|
|
179
|
-
session:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
.fn()
|
|
183
|
-
.mockReturnValue(makePermissionResult("deny")),
|
|
184
|
-
} as unknown as SessionState["permissionManager"],
|
|
185
|
-
}),
|
|
184
|
+
const { handler } = makeHandler({
|
|
185
|
+
session: {
|
|
186
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
|
|
187
|
+
},
|
|
186
188
|
});
|
|
187
|
-
const result = await handleToolCall(
|
|
188
|
-
deps,
|
|
189
|
+
const result = await handler.handleToolCall(
|
|
189
190
|
makeToolCallEvent("read"),
|
|
190
191
|
makeCtx(),
|
|
191
192
|
);
|
|
@@ -205,9 +206,13 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
205
206
|
normalizedLocation: "/skills/librarian/SKILL.md",
|
|
206
207
|
normalizedBaseDir: "/skills/librarian",
|
|
207
208
|
};
|
|
208
|
-
const
|
|
209
|
-
session:
|
|
210
|
-
|
|
209
|
+
const { handler } = makeHandler({
|
|
210
|
+
session: {
|
|
211
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
|
|
212
|
+
},
|
|
213
|
+
toolRegistry: {
|
|
214
|
+
getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
215
|
+
},
|
|
211
216
|
});
|
|
212
217
|
const event = {
|
|
213
218
|
type: "tool_call",
|
|
@@ -215,7 +220,7 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
215
220
|
toolName: "read",
|
|
216
221
|
input: { path: "/skills/librarian/SKILL.md" },
|
|
217
222
|
};
|
|
218
|
-
const result = await handleToolCall(
|
|
223
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
219
224
|
expect(result).toMatchObject({ block: true });
|
|
220
225
|
});
|
|
221
226
|
|
|
@@ -228,9 +233,13 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
228
233
|
normalizedLocation: "/skills/librarian/SKILL.md",
|
|
229
234
|
normalizedBaseDir: "/skills/librarian",
|
|
230
235
|
};
|
|
231
|
-
const
|
|
232
|
-
session:
|
|
233
|
-
|
|
236
|
+
const { handler } = makeHandler({
|
|
237
|
+
session: {
|
|
238
|
+
getActiveSkillEntries: vi.fn().mockReturnValue([skillEntry]),
|
|
239
|
+
},
|
|
240
|
+
toolRegistry: {
|
|
241
|
+
getAll: vi.fn().mockReturnValue([{ toolName: "read" }]),
|
|
242
|
+
},
|
|
234
243
|
});
|
|
235
244
|
const event = {
|
|
236
245
|
type: "tool_call",
|
|
@@ -238,7 +247,7 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
238
247
|
toolName: "read",
|
|
239
248
|
input: { path: "/test/project/src/index.ts" },
|
|
240
249
|
};
|
|
241
|
-
const result = await handleToolCall(
|
|
250
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
242
251
|
expect(result).toEqual({});
|
|
243
252
|
});
|
|
244
253
|
});
|
|
@@ -247,15 +256,13 @@ describe("handleToolCall — skill-read gate", () => {
|
|
|
247
256
|
|
|
248
257
|
describe("handleToolCall — external-directory gate", () => {
|
|
249
258
|
it("blocks a read of a path outside cwd when policy is deny", async () => {
|
|
250
|
-
const
|
|
251
|
-
session:
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}),
|
|
258
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
259
|
+
const { handler } = makeHandler({
|
|
260
|
+
session: {
|
|
261
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
|
|
262
|
+
},
|
|
263
|
+
toolRegistry: {
|
|
264
|
+
getAll: vi.fn().mockReturnValue([{ name: "read" }]),
|
|
265
|
+
},
|
|
259
266
|
});
|
|
260
267
|
const event = {
|
|
261
268
|
type: "tool_call",
|
|
@@ -263,7 +270,7 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
263
270
|
name: "read",
|
|
264
271
|
input: { path: "/outside/project/file.ts" },
|
|
265
272
|
};
|
|
266
|
-
const result = await handleToolCall(
|
|
273
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
267
274
|
expect(result).toMatchObject({ block: true });
|
|
268
275
|
});
|
|
269
276
|
});
|
|
@@ -272,15 +279,13 @@ describe("handleToolCall — external-directory gate", () => {
|
|
|
272
279
|
|
|
273
280
|
describe("handleToolCall — bash external-directory gate", () => {
|
|
274
281
|
it("blocks a bash command referencing an external path when policy is deny", async () => {
|
|
275
|
-
const
|
|
276
|
-
session:
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}),
|
|
283
|
-
getAllTools: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
282
|
+
const { handler } = makeHandler({
|
|
283
|
+
session: {
|
|
284
|
+
checkPermission: vi.fn().mockReturnValue(makePermissionResult("deny")),
|
|
285
|
+
},
|
|
286
|
+
toolRegistry: {
|
|
287
|
+
getAll: vi.fn().mockReturnValue([{ name: "bash" }]),
|
|
288
|
+
},
|
|
284
289
|
});
|
|
285
290
|
const event = {
|
|
286
291
|
type: "tool_call",
|
|
@@ -288,7 +293,7 @@ describe("handleToolCall — bash external-directory gate", () => {
|
|
|
288
293
|
name: "bash",
|
|
289
294
|
input: { command: "cat /outside/project/file.ts" },
|
|
290
295
|
};
|
|
291
|
-
const result = await handleToolCall(
|
|
296
|
+
const result = await handler.handleToolCall(event, makeCtx());
|
|
292
297
|
expect(result).toMatchObject({ block: true });
|
|
293
298
|
});
|
|
294
299
|
});
|
|
@@ -15,8 +15,8 @@ vi.mock("../src/forwarded-permissions/polling", () => ({
|
|
|
15
15
|
|
|
16
16
|
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
17
17
|
import { DEFAULT_EXTENSION_CONFIG } from "../src/extension-config";
|
|
18
|
-
import type { PromptPermissionDetails } from "../src/handlers/types";
|
|
19
18
|
import type { PermissionPromptDecision } from "../src/permission-dialog";
|
|
19
|
+
import type { PromptPermissionDetails } from "../src/permission-prompter";
|
|
20
20
|
import {
|
|
21
21
|
PermissionPrompter,
|
|
22
22
|
type PermissionPrompterDeps,
|