@gotgenes/pi-permission-system 5.5.1 → 5.6.1

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,13 +1,17 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
 
3
- import { evaluateExternalDirectoryGate } from "../../../src/handlers/gates/external-directory";
4
3
  import type {
5
- ExternalDirectoryGateDeps,
6
- ToolCallContext,
7
- } from "../../../src/handlers/gates/types";
8
- import type { PermissionCheckResult } from "../../../src/types";
9
-
10
- // ── helpers ────────────────────────────────────────────────────────────────
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";
12
+ import type { ToolCallContext } from "../../../src/handlers/gates/types";
13
+
14
+ // ── helpers ───────────────────────────��────────────────────────────��───────
11
15
 
12
16
  function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
13
17
  return {
@@ -20,226 +24,151 @@ function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
20
24
  };
21
25
  }
22
26
 
23
- function makeCheckResult(
24
- state: "allow" | "deny" | "ask",
25
- overrides: Partial<PermissionCheckResult> = {},
26
- ): PermissionCheckResult {
27
- return {
28
- state,
29
- toolName: "external_directory",
30
- source: "special",
31
- origin: "builtin",
32
- ...overrides,
33
- };
34
- }
35
-
36
- function makeExtDirGateDeps(
37
- overrides: Partial<ExternalDirectoryGateDeps> = {},
38
- ): ExternalDirectoryGateDeps {
39
- return {
40
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
41
- getSessionRuleset: vi.fn().mockReturnValue([]),
42
- approveSessionRule: vi.fn(),
43
- writeReviewLog: vi.fn(),
44
- emitDecision: vi.fn(),
45
- canConfirm: vi.fn().mockReturnValue(true),
46
- promptPermission: vi
47
- .fn()
48
- .mockResolvedValue({ approved: true, state: "approved" }),
49
- getInfrastructureDirs: vi
50
- .fn()
51
- .mockReturnValue(["/test/agent", "/test/agent/git"]),
52
- ...overrides,
53
- };
54
- }
27
+ // ── tests ────────────────────��────────────────────────────────────��────────
55
28
 
56
- // ── tests ──────────────────────────────────────────────────────────────────
57
-
58
- describe("evaluateExternalDirectoryGate", () => {
59
- it("returns null when no CWD", async () => {
60
- const tcc = makeTcc({ cwd: undefined });
61
- const result = await evaluateExternalDirectoryGate(
62
- tcc,
63
- makeExtDirGateDeps(),
64
- );
29
+ describe("describeExternalDirectoryGate", () => {
30
+ it("returns null when no CWD", () => {
31
+ const result = describeExternalDirectoryGate(makeTcc({ cwd: undefined }), [
32
+ "/test/agent",
33
+ ]);
65
34
  expect(result).toBeNull();
66
35
  });
67
36
 
68
- it("returns null when tool is not path-bearing", async () => {
69
- const tcc = makeTcc({ toolName: "bash", input: { command: "ls" } });
70
- const result = await evaluateExternalDirectoryGate(
71
- tcc,
72
- makeExtDirGateDeps(),
37
+ it("returns null when tool is not path-bearing", () => {
38
+ const result = describeExternalDirectoryGate(
39
+ makeTcc({ toolName: "bash", input: { command: "ls" } }),
40
+ ["/test/agent"],
73
41
  );
74
42
  expect(result).toBeNull();
75
43
  });
76
44
 
77
- it("returns null when path is inside CWD", async () => {
78
- const tcc = makeTcc({ input: { path: "/test/project/src/index.ts" } });
79
- const result = await evaluateExternalDirectoryGate(
80
- tcc,
81
- makeExtDirGateDeps(),
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"],
82
49
  );
83
50
  expect(result).toBeNull();
84
51
  });
85
52
 
86
- // ── Pi infrastructure read bypass ──────────────────────────────────────
53
+ // ── Pi infrastructure read bypass ─────────────────���────────────────────
87
54
 
88
- it("allows and emits infrastructure_auto_allowed for read targeting infra dir", async () => {
89
- const deps = makeExtDirGateDeps();
90
- const tcc = makeTcc({
91
- toolName: "read",
92
- input: { path: "/test/agent/git/some-package/SKILL.md" },
93
- });
94
- const result = await evaluateExternalDirectoryGate(tcc, deps);
95
- expect(result).toEqual({ action: "allow" });
96
- expect(deps.emitDecision).toHaveBeenCalledWith(
97
- expect.objectContaining({
98
- resolution: "infrastructure_auto_allowed",
99
- 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" },
100
60
  }),
61
+ ["/test/agent", "/test/agent/git"],
101
62
  );
102
- });
103
-
104
- it("respects config.piInfrastructureReadPaths for bypass", async () => {
105
- const deps = makeExtDirGateDeps({
106
- getInfrastructureDirs: vi.fn().mockReturnValue(["/custom/infra"]),
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",
107
70
  });
108
- const tcc = makeTcc({
109
- toolName: "read",
110
- input: { path: "/custom/infra/SKILL.md" },
71
+ expect(bypass.log).toMatchObject({
72
+ event: "permission_request.infrastructure_auto_allowed",
111
73
  });
112
- const result = await evaluateExternalDirectoryGate(tcc, deps);
113
- expect(result).toEqual({ action: "allow" });
114
74
  });
115
75
 
116
- it("does NOT bypass for write tools targeting infra dirs", async () => {
117
- const deps = makeExtDirGateDeps({
118
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
119
- });
120
- const tcc = makeTcc({
121
- toolName: "write",
122
- input: { path: "/test/agent/git/some-file.ts", content: "x" },
123
- });
124
- const result = await evaluateExternalDirectoryGate(tcc, deps);
125
- expect(result).toMatchObject({ action: "block" });
126
- });
127
-
128
- // ── Session-rule hit ─────────────────────────────────────────────────────
129
-
130
- it("allows and emits session_approved when session rule covers the path", async () => {
131
- const deps = makeExtDirGateDeps({
132
- checkPermission: vi.fn().mockReturnValue(
133
- makeCheckResult("allow", {
134
- source: "session",
135
- matchedPattern: "/outside/project/*",
136
- }),
137
- ),
138
- getSessionRuleset: vi.fn().mockReturnValue([
139
- {
140
- surface: "external_directory",
141
- pattern: "/outside/project/*",
142
- action: "allow",
143
- },
144
- ]),
145
- });
146
- const tcc = makeTcc();
147
- const result = await evaluateExternalDirectoryGate(tcc, deps);
148
- expect(result).toEqual({ action: "allow" });
149
- expect(deps.emitDecision).toHaveBeenCalledWith(
150
- expect.objectContaining({
151
- resolution: "session_approved",
152
- 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" },
153
81
  }),
82
+ ["/custom/infra"],
154
83
  );
84
+ expect(isGateBypass(result)).toBe(true);
155
85
  });
156
86
 
157
- // ── Policy deny ──────────────────────────────────────────────────────────
158
-
159
- it("blocks and emits policy_deny when policy is deny", async () => {
160
- const deps = makeExtDirGateDeps({
161
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
162
- });
163
- const tcc = makeTcc();
164
- const result = await evaluateExternalDirectoryGate(tcc, deps);
165
- expect(result).toMatchObject({ action: "block" });
166
- expect(deps.emitDecision).toHaveBeenCalledWith(
167
- expect.objectContaining({
168
- surface: "external_directory",
169
- result: "deny",
170
- 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" },
171
92
  }),
93
+ ["/test/agent", "/test/agent/git"],
172
94
  );
95
+ // Should be a GateDescriptor (needs permission check), not a bypass
96
+ expect(result).not.toBeNull();
97
+ expect(isGateDescriptor(result)).toBe(true);
173
98
  });
174
99
 
175
- // ── Policy ask user approves once ──────────────────────────────────────
100
+ // ── GateDescriptor for external paths ─────────────────────────────────��
176
101
 
177
- it("allows without recording session rule when user approves once", async () => {
178
- const deps = makeExtDirGateDeps({
179
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
180
- promptPermission: vi
181
- .fn()
182
- .mockResolvedValue({ approved: true, state: "approved" }),
183
- });
184
- const tcc = makeTcc();
185
- const result = await evaluateExternalDirectoryGate(tcc, deps);
186
- expect(result).toEqual({ action: "allow" });
187
- expect(deps.approveSessionRule).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");
188
107
  });
189
108
 
190
- // ── 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
+ });
191
117
 
192
- it("records session rule when user approves for session", async () => {
193
- const deps = makeExtDirGateDeps({
194
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
195
- promptPermission: vi
196
- .fn()
197
- .mockResolvedValue({ approved: true, state: "approved_for_session" }),
198
- });
199
- const tcc = makeTcc();
200
- const result = await evaluateExternalDirectoryGate(tcc, deps);
201
- expect(result).toEqual({ action: "allow" });
202
- expect(deps.approveSessionRule).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",
203
134
  "external_directory",
204
- expect.any(String),
205
135
  );
136
+ expect(result.sessionApproval).toHaveProperty("pattern");
206
137
  });
207
138
 
208
- // ── Policy ask user denies ─────────────────────────────────────────────
209
-
210
- it("blocks and emits user_denied when user denies", async () => {
211
- const deps = makeExtDirGateDeps({
212
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
213
- promptPermission: vi
214
- .fn()
215
- .mockResolvedValue({ approved: false, state: "denied" }),
216
- });
217
- const tcc = makeTcc();
218
- const result = await evaluateExternalDirectoryGate(tcc, deps);
219
- expect(result).toMatchObject({ action: "block" });
220
- expect(deps.emitDecision).toHaveBeenCalledWith(
221
- expect.objectContaining({
222
- result: "deny",
223
- resolution: "user_denied",
224
- }),
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",
225
147
  );
226
148
  });
227
149
 
228
- // ── 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
+ });
229
163
 
230
- it("blocks and emits confirmation_unavailable when no UI", async () => {
231
- const deps = makeExtDirGateDeps({
232
- checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
233
- canConfirm: 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",
234
171
  });
235
- const tcc = makeTcc();
236
- const result = await evaluateExternalDirectoryGate(tcc, deps);
237
- expect(result).toMatchObject({ action: "block" });
238
- expect(deps.emitDecision).toHaveBeenCalledWith(
239
- expect.objectContaining({
240
- result: "deny",
241
- resolution: "confirmation_unavailable",
242
- }),
243
- );
172
+ expect(result.logContext.message).toBeDefined();
244
173
  });
245
174
  });