@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.
@@ -1,12 +1,17 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { evaluateExternalDirectoryGate } from "../../../src/handlers/gates/external-directory";
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
- function makeCheckResult(
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
- function makeRuntime(
40
- overrides: Record<string, unknown> = {},
41
- ): HandlerDeps["runtime"] {
42
- return {
43
- piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
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", async () => {
82
- const tcc = makeTcc({ toolName: "bash", input: { command: "ls" } });
83
- const result = await evaluateExternalDirectoryGate(tcc, makeDeps());
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", async () => {
88
- const tcc = makeTcc({ input: { path: "/test/project/src/index.ts" } });
89
- const result = await evaluateExternalDirectoryGate(tcc, makeDeps());
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("allows and emits infrastructure_auto_allowed for read targeting infra dir", async () => {
96
- const events = makeEvents();
97
- const deps = makeDeps({ events });
98
- const tcc = makeTcc({
99
- toolName: "read",
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
- it("respects config.piInfrastructureReadPaths for bypass", async () => {
114
- const events = makeEvents();
115
- const deps = makeDeps({
116
- runtime: {
117
- piInfrastructureDirs: [],
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
- const tcc = makeTcc({
144
- toolName: "write",
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
- // ── Session-rule hit ─────────────────────────────────────────────────────
152
-
153
- it("allows and emits session_approved when session rule covers the path", async () => {
154
- const events = makeEvents();
155
- const deps = makeDeps({
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
- // ── Policy deny ──────────────────────────────────────────────────────────
192
-
193
- it("blocks and emits policy_deny when policy is deny", async () => {
194
- const events = makeEvents();
195
- const deps = makeDeps({
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
- // ── Policy ask user approves once ──────────────────────────────────────
100
+ // ── GateDescriptor for external paths ─────────────────────────────────��
217
101
 
218
- it("allows without recording session rule when user approves once", async () => {
219
- const sessionRules = {
220
- approve: vi.fn(),
221
- getRuleset: vi.fn().mockReturnValue([]),
222
- clear: vi.fn(),
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
- // ── Policy ask user approves for session ───────────────────────────────
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("records session rule when user approves for session", async () => {
244
- const sessionRules = {
245
- approve: vi.fn(),
246
- getRuleset: vi.fn().mockReturnValue([]),
247
- clear: vi.fn(),
248
- };
249
- const deps = makeDeps({
250
- runtime: {
251
- permissionManager: {
252
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
253
- },
254
- sessionRules,
255
- },
256
- promptPermission: vi
257
- .fn()
258
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
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
- // ── Policy ask user denies ─────────────────────────────────────────────
270
-
271
- it("blocks and emits user_denied when user denies", async () => {
272
- const events = makeEvents();
273
- const deps = makeDeps({
274
- runtime: {
275
- permissionManager: {
276
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
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
- // ── Policy ask no UI ───────────────────────────────────────────────────
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("blocks and emits confirmation_unavailable when no UI", async () => {
299
- const events = makeEvents();
300
- const deps = makeDeps({
301
- runtime: {
302
- permissionManager: {
303
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
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
- const tcc = makeTcc();
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
  });