@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.
@@ -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
- permissionManager: {
40
- check: vi.fn().mockReturnValue(makeCheckResult("allow")),
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
- permissionManager: {
77
- check: vi.fn().mockReturnValue(makeCheckResult("allow")),
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
- permissionManager: {
104
- check: vi.fn().mockReturnValue(
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
- permissionManager: {
135
- check: vi
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 check when provided", async () => {
161
- const check = vi.fn().mockReturnValue(makeCheckResult("allow"));
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
- permissionManager: { check },
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(check).toHaveBeenCalledWith(
181
- expect.objectContaining({
182
- kind: "tool",
183
- surface: "bash",
184
- agentName: "Worker",
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("includes session rules in the check", async () => {
191
- const sessionRules = [
192
- {
193
- surface: "bash",
194
- pattern: "git *",
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
- const deps = makeDeps({
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-session`,
227
+ `${PERMISSIONS_RPC_CHECK_CHANNEL}:reply:req-path`,
210
228
  );
211
229
  bus.emit(PERMISSIONS_RPC_CHECK_CHANNEL, {
212
- requestId: "req-session",
213
- surface: "bash",
214
- value: "git status",
230
+ requestId: "req-path",
231
+ surface: "external_directory",
232
+ value: "link",
215
233
  });
216
234
  await replyPromise;
217
235
 
218
- expect(check).toHaveBeenCalledWith(
219
- expect.objectContaining({
220
- kind: "tool",
221
- surface: "bash",
222
- agentName: undefined,
223
- }),
224
- sessionRules,
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 check = vi.fn().mockReturnValue(makeCheckResult("allow"));
269
+ const resolve = vi.fn().mockReturnValue(makeCheckResult("allow"));
251
270
  const bus = createEventBus();
252
271
  const deps = makeDeps({
253
- permissionManager: { check },
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((resolve) => setTimeout(resolve, 10));
265
- expect(check).not.toHaveBeenCalled();
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: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
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: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
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: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
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: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
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: { getRuntimeContext: vi.fn().mockReturnValue(null) },
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: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
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: { getRuntimeContext: vi.fn().mockReturnValue(ctx) },
544
+ session: makeSession(ctx),
530
545
  requestPermissionDecisionFromUi: requestUi,
531
546
  });
532
547
  const handles = registerPermissionRpcHandlers(bus, deps);