@gotgenes/pi-permission-system 16.0.2 → 16.2.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 +20 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +57 -0
- package/src/access-intent/access-path.ts +88 -0
- package/src/access-intent/bash/cwd-projection.ts +9 -4
- package/src/access-intent/bash/program.ts +8 -8
- package/src/handlers/gates/bash-command.ts +18 -3
- package/src/handlers/gates/bash-external-directory.ts +16 -30
- package/src/handlers/gates/bash-path-extractor.ts +9 -3
- package/src/handlers/gates/bash-path.ts +6 -4
- package/src/handlers/gates/external-directory-policy.ts +66 -0
- package/src/handlers/gates/external-directory.ts +8 -9
- package/src/handlers/gates/path.ts +6 -5
- package/src/handlers/gates/runner.ts +6 -5
- package/src/handlers/gates/tool-call-gate-pipeline.ts +6 -5
- package/src/path-utils.ts +0 -20
- package/src/permission-event-rpc.ts +4 -6
- package/src/permission-manager.ts +39 -55
- package/src/permission-resolver.ts +45 -57
- package/src/permissions-service.ts +2 -4
- package/src/skill-prompt-sanitizer.ts +2 -2
- package/test/access-intent/access-path.test.ts +107 -0
- package/test/access-intent/bash/program.test.ts +30 -12
- package/test/handlers/before-agent-start.test.ts +6 -9
- package/test/handlers/external-directory-session-dedup.test.ts +16 -40
- package/test/handlers/gates/bash-command-metamorphic.test.ts +5 -3
- package/test/handlers/gates/bash-command.test.ts +26 -23
- package/test/handlers/gates/bash-external-directory.test.ts +36 -32
- package/test/handlers/gates/bash-path.test.ts +12 -8
- package/test/handlers/gates/external-directory-policy.test.ts +124 -0
- package/test/handlers/gates/external-directory.test.ts +11 -5
- package/test/handlers/gates/path.test.ts +30 -25
- package/test/handlers/gates/runner.test.ts +6 -1
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +4 -3
- package/test/handlers/input.test.ts +1 -1
- package/test/helpers/gate-fixtures.ts +12 -24
- package/test/helpers/handler-fixtures.ts +21 -15
- package/test/helpers/session-fixtures.ts +3 -18
- package/test/path-utils.test.ts +0 -34
- package/test/permission-event-rpc.test.ts +24 -20
- package/test/permission-manager-unified.test.ts +445 -209
- package/test/permission-resolver.test.ts +112 -89
- package/test/permissions-service.test.ts +10 -7
- package/test/skill-prompt-sanitizer.test.ts +30 -7
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { ResolvedAccessIntent } from "#src/access-intent/access-intent";
|
|
3
|
+
import { AccessPath } from "#src/access-intent/access-path";
|
|
2
4
|
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
3
5
|
import { PermissionResolver } from "#src/permission-resolver";
|
|
4
6
|
import type { Ruleset } from "#src/rule";
|
|
@@ -9,12 +11,10 @@ import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
|
9
11
|
function makePermissionManager() {
|
|
10
12
|
return {
|
|
11
13
|
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
12
|
-
|
|
14
|
+
check: vi
|
|
13
15
|
.fn<
|
|
14
16
|
(
|
|
15
|
-
|
|
16
|
-
input: unknown,
|
|
17
|
-
agentName?: string,
|
|
17
|
+
intent: ResolvedAccessIntent,
|
|
18
18
|
sessionRules?: Ruleset,
|
|
19
19
|
) => PermissionCheckResult
|
|
20
20
|
>()
|
|
@@ -24,20 +24,6 @@ function makePermissionManager() {
|
|
|
24
24
|
source: "tool",
|
|
25
25
|
origin: "builtin",
|
|
26
26
|
}),
|
|
27
|
-
checkPathPolicy: vi
|
|
28
|
-
.fn<
|
|
29
|
-
(
|
|
30
|
-
values: readonly string[],
|
|
31
|
-
agentName?: string,
|
|
32
|
-
sessionRules?: Ruleset,
|
|
33
|
-
) => PermissionCheckResult
|
|
34
|
-
>()
|
|
35
|
-
.mockReturnValue({
|
|
36
|
-
state: "allow",
|
|
37
|
-
toolName: "path",
|
|
38
|
-
source: "special",
|
|
39
|
-
origin: "builtin",
|
|
40
|
-
}),
|
|
41
27
|
getToolPermission: vi
|
|
42
28
|
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
43
29
|
.mockReturnValue("allow"),
|
|
@@ -62,29 +48,24 @@ beforeEach(() => {
|
|
|
62
48
|
});
|
|
63
49
|
|
|
64
50
|
describe("PermissionResolver", () => {
|
|
65
|
-
describe("resolve", () => {
|
|
66
|
-
it("forwards
|
|
67
|
-
const { resolver, permissionManager } = makeResolver();
|
|
68
|
-
|
|
69
|
-
resolver.resolve("bash", { command: "ls" }, "agent-x");
|
|
70
|
-
|
|
71
|
-
expect(permissionManager.checkPermission).toHaveBeenCalledWith(
|
|
72
|
-
"bash",
|
|
73
|
-
{ command: "ls" },
|
|
74
|
-
"agent-x",
|
|
75
|
-
[],
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("defaults agentName to undefined when omitted", () => {
|
|
51
|
+
describe("resolve — tool intent", () => {
|
|
52
|
+
it("forwards a tool intent with the empty session ruleset", () => {
|
|
80
53
|
const { resolver, permissionManager } = makeResolver();
|
|
81
54
|
|
|
82
|
-
resolver.resolve(
|
|
55
|
+
resolver.resolve({
|
|
56
|
+
kind: "tool",
|
|
57
|
+
surface: "bash",
|
|
58
|
+
input: { command: "ls" },
|
|
59
|
+
agentName: "agent-x",
|
|
60
|
+
});
|
|
83
61
|
|
|
84
|
-
expect(permissionManager.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
62
|
+
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
63
|
+
{
|
|
64
|
+
kind: "tool",
|
|
65
|
+
surface: "bash",
|
|
66
|
+
input: { command: "ls" },
|
|
67
|
+
agentName: "agent-x",
|
|
68
|
+
},
|
|
88
69
|
[],
|
|
89
70
|
);
|
|
90
71
|
});
|
|
@@ -94,13 +75,16 @@ describe("PermissionResolver", () => {
|
|
|
94
75
|
const sessionRules = new SessionRules();
|
|
95
76
|
const { resolver } = makeResolver(pm, sessionRules);
|
|
96
77
|
|
|
97
|
-
// Record an approval directly into the shared SessionRules instance.
|
|
98
78
|
sessionRules.recordSessionApproval(
|
|
99
79
|
SessionApproval.single("bash", "git *"),
|
|
100
80
|
);
|
|
101
|
-
resolver.resolve(
|
|
81
|
+
resolver.resolve({
|
|
82
|
+
kind: "tool",
|
|
83
|
+
surface: "bash",
|
|
84
|
+
input: { command: "git status" },
|
|
85
|
+
});
|
|
102
86
|
|
|
103
|
-
const passedRules = vi.mocked(pm.
|
|
87
|
+
const passedRules = vi.mocked(pm.check).mock.calls[0][1];
|
|
104
88
|
expect(passedRules).toHaveLength(1);
|
|
105
89
|
expect(passedRules?.[0]).toMatchObject({
|
|
106
90
|
surface: "bash",
|
|
@@ -109,9 +93,9 @@ describe("PermissionResolver", () => {
|
|
|
109
93
|
});
|
|
110
94
|
});
|
|
111
95
|
|
|
112
|
-
it("returns the
|
|
96
|
+
it("returns the manager's check result", () => {
|
|
113
97
|
const pm = makePermissionManager();
|
|
114
|
-
vi.mocked(pm.
|
|
98
|
+
vi.mocked(pm.check).mockReturnValue({
|
|
115
99
|
state: "deny",
|
|
116
100
|
toolName: "bash",
|
|
117
101
|
source: "bash",
|
|
@@ -120,7 +104,11 @@ describe("PermissionResolver", () => {
|
|
|
120
104
|
});
|
|
121
105
|
const { resolver } = makeResolver(pm);
|
|
122
106
|
|
|
123
|
-
const result = resolver.resolve(
|
|
107
|
+
const result = resolver.resolve({
|
|
108
|
+
kind: "tool",
|
|
109
|
+
surface: "bash",
|
|
110
|
+
input: { command: "rm -rf /" },
|
|
111
|
+
});
|
|
124
112
|
|
|
125
113
|
expect(result).toEqual({
|
|
126
114
|
state: "deny",
|
|
@@ -132,30 +120,25 @@ describe("PermissionResolver", () => {
|
|
|
132
120
|
});
|
|
133
121
|
});
|
|
134
122
|
|
|
135
|
-
describe("
|
|
136
|
-
it("forwards values
|
|
123
|
+
describe("resolve — path-values intent", () => {
|
|
124
|
+
it("forwards a path-values intent with the current session ruleset", () => {
|
|
137
125
|
const { resolver, permissionManager } = makeResolver();
|
|
138
126
|
|
|
139
|
-
resolver.
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
["/proj/src/a.ts", "src/a.ts"],
|
|
143
|
-
"agent-x",
|
|
144
|
-
|
|
145
|
-
"path",
|
|
146
|
-
);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("forwards an explicit surface to checkPathPolicy", () => {
|
|
150
|
-
const { resolver, permissionManager } = makeResolver();
|
|
151
|
-
|
|
152
|
-
resolver.resolvePathPolicy(["/tmp/x"], "agent-x", "external_directory");
|
|
127
|
+
resolver.resolve({
|
|
128
|
+
kind: "path-values",
|
|
129
|
+
surface: "path",
|
|
130
|
+
values: ["/proj/src/a.ts", "src/a.ts"],
|
|
131
|
+
agentName: "agent-x",
|
|
132
|
+
});
|
|
153
133
|
|
|
154
|
-
expect(permissionManager.
|
|
155
|
-
|
|
156
|
-
|
|
134
|
+
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
135
|
+
{
|
|
136
|
+
kind: "path-values",
|
|
137
|
+
surface: "path",
|
|
138
|
+
values: ["/proj/src/a.ts", "src/a.ts"],
|
|
139
|
+
agentName: "agent-x",
|
|
140
|
+
},
|
|
157
141
|
[],
|
|
158
|
-
"external_directory",
|
|
159
142
|
);
|
|
160
143
|
});
|
|
161
144
|
|
|
@@ -167,9 +150,13 @@ describe("PermissionResolver", () => {
|
|
|
167
150
|
sessionRules.recordSessionApproval(
|
|
168
151
|
SessionApproval.single("path", "src/*"),
|
|
169
152
|
);
|
|
170
|
-
resolver.
|
|
153
|
+
resolver.resolve({
|
|
154
|
+
kind: "path-values",
|
|
155
|
+
surface: "path",
|
|
156
|
+
values: ["src/a.ts"],
|
|
157
|
+
});
|
|
171
158
|
|
|
172
|
-
const passedRules = vi.mocked(pm.
|
|
159
|
+
const passedRules = vi.mocked(pm.check).mock.calls[0][1];
|
|
173
160
|
expect(passedRules).toHaveLength(1);
|
|
174
161
|
expect(passedRules?.[0]).toMatchObject({
|
|
175
162
|
surface: "path",
|
|
@@ -177,45 +164,78 @@ describe("PermissionResolver", () => {
|
|
|
177
164
|
action: "allow",
|
|
178
165
|
});
|
|
179
166
|
});
|
|
167
|
+
});
|
|
180
168
|
|
|
181
|
-
|
|
169
|
+
describe("resolve — access-path intent", () => {
|
|
170
|
+
it("unwraps the AccessPath via matchValues() into a path-values intent", () => {
|
|
171
|
+
const { resolver, permissionManager } = makeResolver();
|
|
172
|
+
const accessPath = AccessPath.forExternalDirectory(
|
|
173
|
+
"/tmp/x",
|
|
174
|
+
"/workspace",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
resolver.resolve({
|
|
178
|
+
kind: "access-path",
|
|
179
|
+
surface: "external_directory",
|
|
180
|
+
path: accessPath,
|
|
181
|
+
agentName: "agent-x",
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
185
|
+
{
|
|
186
|
+
kind: "path-values",
|
|
187
|
+
surface: "external_directory",
|
|
188
|
+
values: accessPath.matchValues(),
|
|
189
|
+
agentName: "agent-x",
|
|
190
|
+
},
|
|
191
|
+
[],
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("returns the manager's check result for an access-path intent", () => {
|
|
182
196
|
const pm = makePermissionManager();
|
|
183
|
-
vi.mocked(pm.
|
|
197
|
+
vi.mocked(pm.check).mockReturnValue({
|
|
184
198
|
state: "deny",
|
|
185
|
-
toolName: "
|
|
199
|
+
toolName: "external_directory",
|
|
186
200
|
source: "special",
|
|
187
201
|
origin: "global",
|
|
188
|
-
matchedPattern: "
|
|
202
|
+
matchedPattern: "/tmp/*",
|
|
189
203
|
});
|
|
190
204
|
const { resolver } = makeResolver(pm);
|
|
205
|
+
const accessPath = AccessPath.forExternalDirectory(
|
|
206
|
+
"/tmp/x",
|
|
207
|
+
"/workspace",
|
|
208
|
+
);
|
|
191
209
|
|
|
192
|
-
const result = resolver.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
toolName: "path",
|
|
197
|
-
source: "special",
|
|
198
|
-
origin: "global",
|
|
199
|
-
matchedPattern: "src/*",
|
|
210
|
+
const result = resolver.resolve({
|
|
211
|
+
kind: "access-path",
|
|
212
|
+
surface: "external_directory",
|
|
213
|
+
path: accessPath,
|
|
200
214
|
});
|
|
215
|
+
|
|
216
|
+
expect(result.state).toBe("deny");
|
|
217
|
+
expect(result.matchedPattern).toBe("/tmp/*");
|
|
201
218
|
});
|
|
202
219
|
});
|
|
203
220
|
|
|
204
|
-
describe("checkPermission", () => {
|
|
205
|
-
it("delegates to
|
|
221
|
+
describe("checkPermission (raw, off-interface)", () => {
|
|
222
|
+
it("delegates to manager.check as a tool intent without session rules", () => {
|
|
206
223
|
const { resolver, permissionManager } = makeResolver();
|
|
207
224
|
|
|
208
225
|
resolver.checkPermission("bash", { command: "ls" }, "agent-1");
|
|
209
226
|
|
|
210
|
-
expect(permissionManager.
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
227
|
+
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
228
|
+
{
|
|
229
|
+
kind: "tool",
|
|
230
|
+
surface: "bash",
|
|
231
|
+
input: { command: "ls" },
|
|
232
|
+
agentName: "agent-1",
|
|
233
|
+
},
|
|
214
234
|
undefined,
|
|
215
235
|
);
|
|
216
236
|
});
|
|
217
237
|
|
|
218
|
-
it("passes optional sessionRules
|
|
238
|
+
it("passes optional sessionRules as the second arg to check", () => {
|
|
219
239
|
const { resolver, permissionManager } = makeResolver();
|
|
220
240
|
const extraRules: Ruleset = [
|
|
221
241
|
{ surface: "bash", pattern: "*", action: "allow", origin: "session" },
|
|
@@ -228,10 +248,13 @@ describe("PermissionResolver", () => {
|
|
|
228
248
|
extraRules,
|
|
229
249
|
);
|
|
230
250
|
|
|
231
|
-
expect(permissionManager.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
252
|
+
{
|
|
253
|
+
kind: "tool",
|
|
254
|
+
surface: "bash",
|
|
255
|
+
input: { command: "ls" },
|
|
256
|
+
agentName: undefined,
|
|
257
|
+
},
|
|
235
258
|
extraRules,
|
|
236
259
|
);
|
|
237
260
|
});
|
|
@@ -96,7 +96,7 @@ describe("checkPermission", () => {
|
|
|
96
96
|
expect(mockBuildInputForSurface).toHaveBeenCalledWith("read", undefined);
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
-
it("calls permissionManager.
|
|
99
|
+
it("calls permissionManager.check with a tool intent, built input, agentName, and current ruleset", () => {
|
|
100
100
|
const ruleset: Ruleset = [
|
|
101
101
|
{ surface: "bash", pattern: "*", action: "allow", origin: "global" },
|
|
102
102
|
];
|
|
@@ -106,19 +106,22 @@ describe("checkPermission", () => {
|
|
|
106
106
|
sessionRules: makeSessionRules(ruleset),
|
|
107
107
|
});
|
|
108
108
|
service.checkPermission("bash", "echo hi", "my-agent");
|
|
109
|
-
expect(permissionManager.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
expect(permissionManager.check).toHaveBeenCalledWith(
|
|
110
|
+
{
|
|
111
|
+
kind: "tool",
|
|
112
|
+
surface: "bash",
|
|
113
|
+
input: builtInput,
|
|
114
|
+
agentName: "my-agent",
|
|
115
|
+
},
|
|
113
116
|
ruleset,
|
|
114
117
|
);
|
|
115
118
|
void sessionRules; // used indirectly
|
|
116
119
|
});
|
|
117
120
|
|
|
118
|
-
it("returns the result from permissionManager.
|
|
121
|
+
it("returns the result from permissionManager.check", () => {
|
|
119
122
|
const expected = makeCheckResult({ state: "deny", toolName: "bash" });
|
|
120
123
|
const { service, permissionManager } = makeService();
|
|
121
|
-
vi.mocked(permissionManager.
|
|
124
|
+
vi.mocked(permissionManager.check).mockReturnValue(expected);
|
|
122
125
|
const result = service.checkPermission("bash", "rm -rf /");
|
|
123
126
|
expect(result).toBe(expected);
|
|
124
127
|
});
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
4
4
|
import {
|
|
5
5
|
findSkillPathMatch,
|
|
6
6
|
parseAllSkillPromptSections,
|
|
7
7
|
resolveSkillPromptEntries,
|
|
8
|
+
type SkillPermissionChecker,
|
|
8
9
|
} from "#src/skill-prompt-sanitizer";
|
|
9
10
|
import type { PermissionCheckResult } from "#src/types";
|
|
10
11
|
import { createManager } from "#test/helpers/manager-harness";
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Adapt a real `PermissionManager` to the raw `SkillPermissionChecker`
|
|
15
|
+
* contract, mirroring how `PermissionResolver.checkPermission` delegates to
|
|
16
|
+
* `manager.check` with a tool intent (#478).
|
|
17
|
+
*/
|
|
18
|
+
function asChecker(manager: ScopedPermissionManager): SkillPermissionChecker {
|
|
19
|
+
return {
|
|
20
|
+
checkPermission: (surface, input, agentName) =>
|
|
21
|
+
manager.check({ kind: "tool", surface, input, agentName }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
12
25
|
afterEach(() => {
|
|
13
26
|
vi.restoreAllMocks();
|
|
14
27
|
});
|
|
@@ -20,16 +33,16 @@ const CWD = "/projects/my-app";
|
|
|
20
33
|
function makeManager(
|
|
21
34
|
defaultState: "allow" | "deny" | "ask" = "allow",
|
|
22
35
|
overrides: Record<string, "allow" | "deny" | "ask"> = {},
|
|
23
|
-
):
|
|
36
|
+
): SkillPermissionChecker {
|
|
24
37
|
return {
|
|
25
38
|
checkPermission: vi.fn(
|
|
26
|
-
(_surface: string, input:
|
|
27
|
-
const name = input.name ?? "";
|
|
39
|
+
(_surface: string, input: unknown): PermissionCheckResult => {
|
|
40
|
+
const name = (input as { name?: string }).name ?? "";
|
|
28
41
|
const state = overrides[name] ?? defaultState;
|
|
29
42
|
return { toolName: "skill", state, source: "tool", origin: "builtin" };
|
|
30
43
|
},
|
|
31
44
|
),
|
|
32
|
-
}
|
|
45
|
+
};
|
|
33
46
|
}
|
|
34
47
|
|
|
35
48
|
function skillBlock(
|
|
@@ -312,7 +325,12 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
312
325
|
"System prompt end",
|
|
313
326
|
].join("\n");
|
|
314
327
|
|
|
315
|
-
const result = resolveSkillPromptEntries(
|
|
328
|
+
const result = resolveSkillPromptEntries(
|
|
329
|
+
prompt,
|
|
330
|
+
asChecker(manager),
|
|
331
|
+
null,
|
|
332
|
+
"/cwd",
|
|
333
|
+
);
|
|
316
334
|
|
|
317
335
|
expect(result.prompt).not.toContain("denied-skill");
|
|
318
336
|
expect(result.prompt).toContain("visible-skill");
|
|
@@ -354,7 +372,12 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
|
|
|
354
372
|
"System prompt end",
|
|
355
373
|
].join("\n");
|
|
356
374
|
|
|
357
|
-
const result = resolveSkillPromptEntries(
|
|
375
|
+
const result = resolveSkillPromptEntries(
|
|
376
|
+
prompt,
|
|
377
|
+
asChecker(manager),
|
|
378
|
+
null,
|
|
379
|
+
"/cwd",
|
|
380
|
+
);
|
|
358
381
|
const visiblePath = resolve("/cwd", "./skills/visible/file.ts");
|
|
359
382
|
const blockedPath = resolve("/cwd", "./skills/blocked/file.ts");
|
|
360
383
|
const matchedVisibleSkill = findSkillPathMatch(
|