@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
|
@@ -3,11 +3,11 @@ import { describe, expect, it, vi } from "vitest";
|
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
extractSkillNameFromInput,
|
|
6
|
-
|
|
7
|
-
} from "../../src/handlers/
|
|
8
|
-
import type {
|
|
9
|
-
import type {
|
|
10
|
-
import type {
|
|
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 { PermissionState } from "../../src/types";
|
|
11
11
|
|
|
12
12
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
13
13
|
|
|
@@ -34,49 +34,51 @@ function makeInputEvent(text: string) {
|
|
|
34
34
|
return { text };
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
function makeSession(
|
|
37
|
+
function makeSession(
|
|
38
|
+
overrides: Partial<Record<keyof PermissionSession, unknown>> = {},
|
|
39
|
+
): PermissionSession {
|
|
38
40
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
getRuleset: vi.fn().mockReturnValue([]),
|
|
50
|
-
clear: vi.fn(),
|
|
51
|
-
} as unknown as SessionState["sessionRules"],
|
|
41
|
+
logger: { debug: vi.fn(), review: vi.fn(), warn: vi.fn() },
|
|
42
|
+
activate: vi.fn(),
|
|
43
|
+
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
44
|
+
checkPermission: vi.fn().mockReturnValue({ state: "allow" }),
|
|
45
|
+
getToolPermission: vi.fn().mockReturnValue("allow" as PermissionState),
|
|
46
|
+
getSessionRuleset: vi.fn().mockReturnValue([]),
|
|
47
|
+
approveSessionRule: vi.fn(),
|
|
48
|
+
canPrompt: vi.fn().mockReturnValue(true),
|
|
49
|
+
prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
|
|
50
|
+
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
52
51
|
...overrides,
|
|
52
|
+
} as unknown as PermissionSession;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function makeEvents() {
|
|
56
|
+
return {
|
|
57
|
+
emit: vi.fn(),
|
|
58
|
+
on: vi.fn().mockReturnValue(() => undefined),
|
|
53
59
|
};
|
|
54
60
|
}
|
|
55
61
|
|
|
56
|
-
function
|
|
62
|
+
function makeToolRegistry(): ToolRegistry {
|
|
57
63
|
return {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
61
|
-
getPiInfrastructureReadPaths: vi.fn().mockReturnValue([]),
|
|
62
|
-
createPermissionManagerForCwd: vi.fn(),
|
|
63
|
-
refreshExtensionConfig: vi.fn(),
|
|
64
|
-
logResolvedConfigPaths: vi.fn(),
|
|
65
|
-
resolveAgentName: vi.fn().mockReturnValue(null),
|
|
66
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
67
|
-
promptPermission: vi
|
|
68
|
-
.fn()
|
|
69
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
70
|
-
createPermissionRequestId: vi.fn().mockReturnValue("req-id"),
|
|
71
|
-
events: { emit: vi.fn(), on: vi.fn().mockReturnValue(() => undefined) },
|
|
72
|
-
forwarding: { start: vi.fn(), stop: vi.fn() },
|
|
73
|
-
stopPermissionRpcHandlers: vi.fn(),
|
|
74
|
-
getAllTools: vi.fn().mockReturnValue([]),
|
|
75
|
-
setActiveTools: vi.fn(),
|
|
76
|
-
...overrides,
|
|
64
|
+
getAll: vi.fn().mockReturnValue([]),
|
|
65
|
+
setActive: vi.fn(),
|
|
77
66
|
};
|
|
78
67
|
}
|
|
79
68
|
|
|
69
|
+
function makeHandler(overrides?: {
|
|
70
|
+
session?: Partial<Record<keyof PermissionSession, unknown>>;
|
|
71
|
+
}): {
|
|
72
|
+
handler: PermissionGateHandler;
|
|
73
|
+
session: PermissionSession;
|
|
74
|
+
} {
|
|
75
|
+
const session = makeSession(overrides?.session);
|
|
76
|
+
const events = makeEvents();
|
|
77
|
+
const toolRegistry = makeToolRegistry();
|
|
78
|
+
const handler = new PermissionGateHandler(session, events, toolRegistry);
|
|
79
|
+
return { handler, session };
|
|
80
|
+
}
|
|
81
|
+
|
|
80
82
|
// ── extractSkillNameFromInput ──────────────────────────────────────────────
|
|
81
83
|
|
|
82
84
|
describe("extractSkillNameFromInput", () => {
|
|
@@ -114,24 +116,16 @@ describe("extractSkillNameFromInput", () => {
|
|
|
114
116
|
// ── handleInput ───────────────────────────────────────────────────────────
|
|
115
117
|
|
|
116
118
|
describe("handleInput", () => {
|
|
117
|
-
it("
|
|
118
|
-
const ctx = makeCtx();
|
|
119
|
-
const deps = makeDeps();
|
|
120
|
-
await handleInput(deps, makeInputEvent("hello"), ctx);
|
|
121
|
-
expect(deps.session.runtimeContext).toBe(ctx);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it("starts forwarded permission polling", async () => {
|
|
119
|
+
it("activates session with ctx", async () => {
|
|
125
120
|
const ctx = makeCtx();
|
|
126
|
-
const
|
|
127
|
-
await handleInput(
|
|
128
|
-
expect(
|
|
121
|
+
const { handler, session } = makeHandler();
|
|
122
|
+
await handler.handleInput(makeInputEvent("hello"), ctx);
|
|
123
|
+
expect(session.activate).toHaveBeenCalledWith(ctx);
|
|
129
124
|
});
|
|
130
125
|
|
|
131
126
|
it("returns continue for non-skill input", async () => {
|
|
132
|
-
const
|
|
133
|
-
const result = await handleInput(
|
|
134
|
-
deps,
|
|
127
|
+
const { handler } = makeHandler();
|
|
128
|
+
const result = await handler.handleInput(
|
|
135
129
|
makeInputEvent("just a message"),
|
|
136
130
|
makeCtx(),
|
|
137
131
|
);
|
|
@@ -139,18 +133,14 @@ describe("handleInput", () => {
|
|
|
139
133
|
});
|
|
140
134
|
|
|
141
135
|
it("does not check permissions for non-skill input", async () => {
|
|
142
|
-
const
|
|
143
|
-
await handleInput(
|
|
144
|
-
expect(
|
|
145
|
-
deps.session.permissionManager.checkPermission,
|
|
146
|
-
).not.toHaveBeenCalled();
|
|
136
|
+
const { handler, session } = makeHandler();
|
|
137
|
+
await handler.handleInput(makeInputEvent("just a message"), makeCtx());
|
|
138
|
+
expect(session.checkPermission).not.toHaveBeenCalled();
|
|
147
139
|
});
|
|
148
140
|
|
|
149
141
|
it("returns continue when skill is allowed", async () => {
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const result = await handleInput(
|
|
153
|
-
deps,
|
|
142
|
+
const { handler } = makeHandler();
|
|
143
|
+
const result = await handler.handleInput(
|
|
154
144
|
makeInputEvent("/skill:librarian"),
|
|
155
145
|
makeCtx(),
|
|
156
146
|
);
|
|
@@ -158,16 +148,12 @@ describe("handleInput", () => {
|
|
|
158
148
|
});
|
|
159
149
|
|
|
160
150
|
it("returns handled when skill is denied", async () => {
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
session: makeSession({
|
|
166
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
167
|
-
}),
|
|
151
|
+
const { handler } = makeHandler({
|
|
152
|
+
session: {
|
|
153
|
+
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
154
|
+
},
|
|
168
155
|
});
|
|
169
|
-
const result = await handleInput(
|
|
170
|
-
deps,
|
|
156
|
+
const result = await handler.handleInput(
|
|
171
157
|
makeInputEvent("/skill:librarian"),
|
|
172
158
|
makeCtx(),
|
|
173
159
|
);
|
|
@@ -176,15 +162,12 @@ describe("handleInput", () => {
|
|
|
176
162
|
|
|
177
163
|
it("shows a warning notification when skill is denied and UI is available", async () => {
|
|
178
164
|
const ctx = makeCtx({ hasUI: true });
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
session: makeSession({
|
|
184
|
-
permissionManager: pm as unknown as SessionState["permissionManager"],
|
|
185
|
-
}),
|
|
165
|
+
const { handler } = makeHandler({
|
|
166
|
+
session: {
|
|
167
|
+
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
168
|
+
},
|
|
186
169
|
});
|
|
187
|
-
await handleInput(
|
|
170
|
+
await handler.handleInput(makeInputEvent("/skill:librarian"), ctx);
|
|
188
171
|
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
189
172
|
expect.stringContaining("librarian"),
|
|
190
173
|
"warning",
|
|
@@ -193,26 +176,23 @@ describe("handleInput", () => {
|
|
|
193
176
|
|
|
194
177
|
it("does not show a warning notification when skill is denied and UI is absent", async () => {
|
|
195
178
|
const ctx = makeCtx({ hasUI: false });
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}),
|
|
179
|
+
const { handler } = makeHandler({
|
|
180
|
+
session: {
|
|
181
|
+
checkPermission: vi.fn().mockReturnValue({ state: "deny" }),
|
|
182
|
+
},
|
|
201
183
|
});
|
|
202
|
-
await handleInput(
|
|
184
|
+
await handler.handleInput(makeInputEvent("/skill:librarian"), ctx);
|
|
203
185
|
expect(ctx.ui.notify).not.toHaveBeenCalled();
|
|
204
186
|
});
|
|
205
187
|
|
|
206
188
|
it("returns handled when skill requires approval but no UI is available", async () => {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(false),
|
|
189
|
+
const { handler } = makeHandler({
|
|
190
|
+
session: {
|
|
191
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
192
|
+
canPrompt: vi.fn().mockReturnValue(false),
|
|
193
|
+
},
|
|
213
194
|
});
|
|
214
|
-
const result = await handleInput(
|
|
215
|
-
deps,
|
|
195
|
+
const result = await handler.handleInput(
|
|
216
196
|
makeInputEvent("/skill:librarian"),
|
|
217
197
|
makeCtx(),
|
|
218
198
|
);
|
|
@@ -220,38 +200,30 @@ describe("handleInput", () => {
|
|
|
220
200
|
});
|
|
221
201
|
|
|
222
202
|
it("prompts and returns continue when skill ask is approved", async () => {
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
.fn()
|
|
231
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
203
|
+
const { handler, session } = makeHandler({
|
|
204
|
+
session: {
|
|
205
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
206
|
+
prompt: vi
|
|
207
|
+
.fn()
|
|
208
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
209
|
+
},
|
|
232
210
|
});
|
|
233
|
-
const result = await handleInput(
|
|
234
|
-
deps,
|
|
211
|
+
const result = await handler.handleInput(
|
|
235
212
|
makeInputEvent("/skill:librarian"),
|
|
236
213
|
makeCtx(),
|
|
237
214
|
);
|
|
238
215
|
expect(result).toEqual({ action: "continue" });
|
|
239
|
-
expect(
|
|
216
|
+
expect(session.prompt).toHaveBeenCalledOnce();
|
|
240
217
|
});
|
|
241
218
|
|
|
242
219
|
it("returns handled when skill ask is denied by user", async () => {
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
canRequestPermissionConfirmation: vi.fn().mockReturnValue(true),
|
|
249
|
-
promptPermission: vi
|
|
250
|
-
.fn()
|
|
251
|
-
.mockResolvedValue({ approved: false, state: "denied" }),
|
|
220
|
+
const { handler } = makeHandler({
|
|
221
|
+
session: {
|
|
222
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
223
|
+
prompt: vi.fn().mockResolvedValue({ approved: false, state: "denied" }),
|
|
224
|
+
},
|
|
252
225
|
});
|
|
253
|
-
const result = await handleInput(
|
|
254
|
-
deps,
|
|
226
|
+
const result = await handler.handleInput(
|
|
255
227
|
makeInputEvent("/skill:librarian"),
|
|
256
228
|
makeCtx(),
|
|
257
229
|
);
|
|
@@ -259,19 +231,17 @@ describe("handleInput", () => {
|
|
|
259
231
|
});
|
|
260
232
|
|
|
261
233
|
it("passes agentName in the prompt permission request", async () => {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
.fn()
|
|
271
|
-
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
234
|
+
const { handler, session } = makeHandler({
|
|
235
|
+
session: {
|
|
236
|
+
checkPermission: vi.fn().mockReturnValue({ state: "ask" }),
|
|
237
|
+
resolveAgentName: vi.fn().mockReturnValue("code-agent"),
|
|
238
|
+
prompt: vi
|
|
239
|
+
.fn()
|
|
240
|
+
.mockResolvedValue({ approved: true, state: "approved" }),
|
|
241
|
+
},
|
|
272
242
|
});
|
|
273
|
-
await handleInput(
|
|
274
|
-
expect(
|
|
243
|
+
await handler.handleInput(makeInputEvent("/skill:librarian"), makeCtx());
|
|
244
|
+
expect(session.prompt).toHaveBeenCalledWith(
|
|
275
245
|
expect.anything(),
|
|
276
246
|
expect.objectContaining({
|
|
277
247
|
agentName: "code-agent",
|