@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.
- package/CHANGELOG.md +23 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/handlers/gates/bash-external-directory.ts +70 -65
- package/src/handlers/gates/descriptor.ts +115 -0
- package/src/handlers/gates/external-directory.ts +63 -122
- package/src/handlers/gates/index.ts +12 -4
- package/src/handlers/gates/runner.ts +144 -0
- package/src/handlers/gates/skill-read.ts +37 -54
- package/src/handlers/gates/tool.ts +35 -97
- package/src/handlers/gates/types.ts +0 -77
- package/src/handlers/tool-call.ts +89 -58
- package/tests/handlers/gates/bash-external-directory.test.ts +128 -126
- package/tests/handlers/gates/external-directory.test.ts +117 -188
- package/tests/handlers/gates/runner.test.ts +361 -0
- package/tests/handlers/gates/skill-read.test.ts +87 -123
- package/tests/handlers/gates/tool.test.ts +119 -112
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
} from "../../../src/handlers/gates/
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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",
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
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",
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
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("
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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("
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
-
// ──
|
|
100
|
+
// ── GateDescriptor for external paths ─────────────────────────────────��
|
|
176
101
|
|
|
177
|
-
it("
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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("
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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("
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
});
|