@gotgenes/pi-permission-system 17.1.1 → 18.0.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 +26 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +6 -3
- package/src/access-intent/bash/program.ts +0 -4
- package/src/handlers/gates/tool-call-gate-pipeline.ts +61 -22
- package/src/handlers/gates/tool.ts +10 -14
- package/src/index.ts +8 -6
- package/src/input-normalizer.ts +54 -56
- package/src/permission-event-rpc.ts +21 -15
- package/src/permission-manager.ts +3 -6
- package/src/permission-session.ts +0 -5
- package/src/permissions-service.ts +33 -12
- package/test/access-intent/bash/program.test.ts +1 -1
- package/test/composition-root.test.ts +22 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +73 -0
- package/test/handlers/gates/tool.test.ts +25 -16
- package/test/helpers/gate-fixtures.ts +0 -3
- package/test/input-normalizer.test.ts +163 -270
- package/test/path-utils.test.ts +1 -1
- package/test/permission-event-rpc.test.ts +80 -65
- package/test/permission-manager-unified.test.ts +134 -145
- package/test/permissions-service.test.ts +84 -72
- package/test/service.test.ts +56 -103
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-deprecated -- tests the deprecated RPC channel implementation */
|
|
2
2
|
import { createEventBus } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
4
5
|
import {
|
|
5
6
|
type PermissionRpcDeps,
|
|
6
7
|
registerPermissionRpcHandlers,
|
|
@@ -16,6 +17,15 @@ import {
|
|
|
16
17
|
PERMISSIONS_UI_PROMPT_CHANNEL,
|
|
17
18
|
} from "#src/permission-events";
|
|
18
19
|
|
|
20
|
+
// Mock node:fs so realpathSync (the canonical alias) is controllable.
|
|
21
|
+
const realpathSync = vi.hoisted(() =>
|
|
22
|
+
vi.fn<(path: string) => string>((p) => p),
|
|
23
|
+
);
|
|
24
|
+
vi.mock("node:fs", () => ({
|
|
25
|
+
realpathSync,
|
|
26
|
+
default: { realpathSync },
|
|
27
|
+
}));
|
|
28
|
+
|
|
19
29
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
20
30
|
|
|
21
31
|
function makeCheckResult(
|
|
@@ -36,17 +46,31 @@ function makeDeps(
|
|
|
36
46
|
overrides: Partial<PermissionRpcDeps> = {},
|
|
37
47
|
): PermissionRpcDeps {
|
|
38
48
|
return {
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
resolver: {
|
|
50
|
+
resolve: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
51
|
+
},
|
|
52
|
+
session: {
|
|
53
|
+
getRuntimeContext: vi.fn().mockReturnValue(null),
|
|
54
|
+
getPathNormalizer: vi
|
|
55
|
+
.fn()
|
|
56
|
+
.mockReturnValue(new PathNormalizer("linux", "/test/project")),
|
|
41
57
|
},
|
|
42
|
-
sessionRules: { getRuleset: vi.fn().mockReturnValue([]) },
|
|
43
|
-
session: { getRuntimeContext: vi.fn().mockReturnValue(null) },
|
|
44
58
|
requestPermissionDecisionFromUi: vi.fn(),
|
|
45
59
|
logger: { review: vi.fn() },
|
|
46
60
|
...overrides,
|
|
47
61
|
};
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
/** Build the narrow RPC session view from a runtime context. */
|
|
65
|
+
function makeSession(ctx: unknown): PermissionRpcDeps["session"] {
|
|
66
|
+
return {
|
|
67
|
+
getRuntimeContext: vi.fn().mockReturnValue(ctx),
|
|
68
|
+
getPathNormalizer: vi
|
|
69
|
+
.fn()
|
|
70
|
+
.mockReturnValue(new PathNormalizer("linux", "/test/project")),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
/** Wait for a single event on the bus reply channel. */
|
|
51
75
|
function waitForReply<T>(
|
|
52
76
|
bus: ReturnType<typeof createEventBus>,
|
|
@@ -60,6 +84,11 @@ function waitForReply<T>(
|
|
|
60
84
|
});
|
|
61
85
|
}
|
|
62
86
|
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
realpathSync.mockReset();
|
|
89
|
+
realpathSync.mockImplementation((p: string) => p);
|
|
90
|
+
});
|
|
91
|
+
|
|
63
92
|
// ── registerPermissionRpcHandlers — check RPC ──────────────────────────────
|
|
64
93
|
|
|
65
94
|
describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
@@ -73,8 +102,8 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
73
102
|
it("replies allow for an allowed surface/value", async () => {
|
|
74
103
|
const bus = createEventBus();
|
|
75
104
|
const deps = makeDeps({
|
|
76
|
-
|
|
77
|
-
|
|
105
|
+
resolver: {
|
|
106
|
+
resolve: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
78
107
|
},
|
|
79
108
|
});
|
|
80
109
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -100,8 +129,8 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
100
129
|
it("replies deny for a denied surface/value", async () => {
|
|
101
130
|
const bus = createEventBus();
|
|
102
131
|
const deps = makeDeps({
|
|
103
|
-
|
|
104
|
-
|
|
132
|
+
resolver: {
|
|
133
|
+
resolve: vi.fn().mockReturnValue(
|
|
105
134
|
makeCheckResult("deny", {
|
|
106
135
|
origin: "project",
|
|
107
136
|
matchedPattern: "rm *",
|
|
@@ -131,8 +160,8 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
131
160
|
it("replies ask for an ask surface/value", async () => {
|
|
132
161
|
const bus = createEventBus();
|
|
133
162
|
const deps = makeDeps({
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
resolver: {
|
|
164
|
+
resolve: vi
|
|
136
165
|
.fn()
|
|
137
166
|
.mockReturnValue(
|
|
138
167
|
makeCheckResult("ask", { matchedPattern: undefined }),
|
|
@@ -157,11 +186,11 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
157
186
|
}
|
|
158
187
|
});
|
|
159
188
|
|
|
160
|
-
it("passes agentName to
|
|
161
|
-
const
|
|
189
|
+
it("passes agentName through to the resolver when provided", async () => {
|
|
190
|
+
const resolve = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
162
191
|
const bus = createEventBus();
|
|
163
192
|
const deps = makeDeps({
|
|
164
|
-
|
|
193
|
+
resolver: { resolve },
|
|
165
194
|
});
|
|
166
195
|
registerPermissionRpcHandlers(bus, deps);
|
|
167
196
|
|
|
@@ -177,52 +206,42 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
177
206
|
});
|
|
178
207
|
await replyPromise;
|
|
179
208
|
|
|
180
|
-
expect(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
expect.anything(),
|
|
187
|
-
);
|
|
209
|
+
expect(resolve).toHaveBeenCalledWith({
|
|
210
|
+
kind: "tool",
|
|
211
|
+
surface: "bash",
|
|
212
|
+
input: { command: "git push" },
|
|
213
|
+
agentName: "Worker",
|
|
214
|
+
});
|
|
188
215
|
});
|
|
189
216
|
|
|
190
|
-
it("
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
action: "allow" as const,
|
|
196
|
-
origin: "session" as const,
|
|
197
|
-
},
|
|
198
|
-
];
|
|
199
|
-
const check = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
217
|
+
it("resolves a path-surface query through an access-path intent matching the canonical alias", async () => {
|
|
218
|
+
realpathSync.mockImplementation((p: string) =>
|
|
219
|
+
p === "/test/project/link" ? "/test/project/real" : p,
|
|
220
|
+
);
|
|
221
|
+
const resolve = vi.fn().mockReturnValue(makeCheckResult("deny"));
|
|
200
222
|
const bus = createEventBus();
|
|
201
|
-
|
|
202
|
-
permissionManager: { check },
|
|
203
|
-
sessionRules: { getRuleset: vi.fn().mockReturnValue(sessionRules) },
|
|
204
|
-
});
|
|
205
|
-
registerPermissionRpcHandlers(bus, deps);
|
|
223
|
+
registerPermissionRpcHandlers(bus, makeDeps({ resolver: { resolve } }));
|
|
206
224
|
|
|
207
225
|
const replyPromise = waitForReply(
|
|
208
226
|
bus,
|
|
209
|
-
`${PERMISSIONS_RPC_CHECK_CHANNEL}:reply:req-
|
|
227
|
+
`${PERMISSIONS_RPC_CHECK_CHANNEL}:reply:req-path`,
|
|
210
228
|
);
|
|
211
229
|
bus.emit(PERMISSIONS_RPC_CHECK_CHANNEL, {
|
|
212
|
-
requestId: "req-
|
|
213
|
-
surface: "
|
|
214
|
-
value: "
|
|
230
|
+
requestId: "req-path",
|
|
231
|
+
surface: "external_directory",
|
|
232
|
+
value: "link",
|
|
215
233
|
});
|
|
216
234
|
await replyPromise;
|
|
217
235
|
|
|
218
|
-
expect(
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
236
|
+
expect(resolve).toHaveBeenCalledTimes(1);
|
|
237
|
+
const intent = resolve.mock.calls[0][0] as {
|
|
238
|
+
kind: string;
|
|
239
|
+
surface: string;
|
|
240
|
+
path: { matchValues(): string[] };
|
|
241
|
+
};
|
|
242
|
+
expect(intent.kind).toBe("access-path");
|
|
243
|
+
expect(intent.surface).toBe("external_directory");
|
|
244
|
+
expect(intent.path.matchValues()).toContain("/test/project/real");
|
|
226
245
|
});
|
|
227
246
|
|
|
228
247
|
it("replies with error envelope when requestId is missing", async () => {
|
|
@@ -247,10 +266,10 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
247
266
|
});
|
|
248
267
|
|
|
249
268
|
it("unsubCheck stops the handler from firing", async () => {
|
|
250
|
-
const
|
|
269
|
+
const resolve = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
251
270
|
const bus = createEventBus();
|
|
252
271
|
const deps = makeDeps({
|
|
253
|
-
|
|
272
|
+
resolver: { resolve },
|
|
254
273
|
});
|
|
255
274
|
const handles = registerPermissionRpcHandlers(bus, deps);
|
|
256
275
|
handles.unsubCheck();
|
|
@@ -261,8 +280,8 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
261
280
|
});
|
|
262
281
|
|
|
263
282
|
// Give async handlers a chance to fire
|
|
264
|
-
await new Promise((
|
|
265
|
-
expect(
|
|
283
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
284
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
266
285
|
});
|
|
267
286
|
});
|
|
268
287
|
|
|
@@ -294,7 +313,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
294
313
|
const ctx = makeCtxWithUi();
|
|
295
314
|
const approvedDecision = { approved: true, state: "approved" as const };
|
|
296
315
|
const deps = makeDeps({
|
|
297
|
-
session:
|
|
316
|
+
session: makeSession(ctx),
|
|
298
317
|
requestPermissionDecisionFromUi: vi
|
|
299
318
|
.fn()
|
|
300
319
|
.mockResolvedValue(approvedDecision),
|
|
@@ -329,7 +348,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
329
348
|
.fn()
|
|
330
349
|
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
331
350
|
const deps = makeDeps({
|
|
332
|
-
session:
|
|
351
|
+
session: makeSession(ctx),
|
|
333
352
|
requestPermissionDecisionFromUi: requestUi,
|
|
334
353
|
});
|
|
335
354
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -367,7 +386,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
367
386
|
.fn()
|
|
368
387
|
.mockResolvedValue({ approved: true, state: "approved" as const });
|
|
369
388
|
const deps = makeDeps({
|
|
370
|
-
session:
|
|
389
|
+
session: makeSession(ctx),
|
|
371
390
|
requestPermissionDecisionFromUi: requestUi,
|
|
372
391
|
});
|
|
373
392
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -403,7 +422,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
403
422
|
denialReason: "Too risky",
|
|
404
423
|
};
|
|
405
424
|
const deps = makeDeps({
|
|
406
|
-
session:
|
|
425
|
+
session: makeSession(ctx),
|
|
407
426
|
requestPermissionDecisionFromUi: vi
|
|
408
427
|
.fn()
|
|
409
428
|
.mockResolvedValue(deniedDecision),
|
|
@@ -434,7 +453,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
434
453
|
it("replies with no_ui error when context has no UI", async () => {
|
|
435
454
|
const bus = createEventBus();
|
|
436
455
|
const deps = makeDeps({
|
|
437
|
-
session:
|
|
456
|
+
session: makeSession(null),
|
|
438
457
|
});
|
|
439
458
|
registerPermissionRpcHandlers(bus, deps);
|
|
440
459
|
|
|
@@ -457,11 +476,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
457
476
|
it("replies with no_ui error when context hasUI is false", async () => {
|
|
458
477
|
const bus = createEventBus();
|
|
459
478
|
const deps = makeDeps({
|
|
460
|
-
session: {
|
|
461
|
-
getRuntimeContext: vi
|
|
462
|
-
.fn()
|
|
463
|
-
.mockReturnValue({ hasUI: false, ui: makeUi() }),
|
|
464
|
-
},
|
|
479
|
+
session: makeSession({ hasUI: false, ui: makeUi() }),
|
|
465
480
|
});
|
|
466
481
|
registerPermissionRpcHandlers(bus, deps);
|
|
467
482
|
|
|
@@ -486,7 +501,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
486
501
|
const ctx = makeCtxWithUi();
|
|
487
502
|
const logger = { review: vi.fn() };
|
|
488
503
|
const deps = makeDeps({
|
|
489
|
-
session:
|
|
504
|
+
session: makeSession(ctx),
|
|
490
505
|
requestPermissionDecisionFromUi: vi
|
|
491
506
|
.fn()
|
|
492
507
|
.mockResolvedValue({ approved: true, state: "approved" as const }),
|
|
@@ -526,7 +541,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:prompt", () => {
|
|
|
526
541
|
const bus = createEventBus();
|
|
527
542
|
const ctx = makeCtxWithUi();
|
|
528
543
|
const deps = makeDeps({
|
|
529
|
-
session:
|
|
544
|
+
session: makeSession(ctx),
|
|
530
545
|
requestPermissionDecisionFromUi: requestUi,
|
|
531
546
|
});
|
|
532
547
|
const handles = registerPermissionRpcHandlers(bus, deps);
|