@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
|
@@ -26,13 +26,10 @@ import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
|
26
26
|
*/
|
|
27
27
|
export function makeResolver(defaultCheck?: PermissionCheckResult) {
|
|
28
28
|
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
29
|
-
const resolvePathPolicy =
|
|
30
|
-
vi.fn<ScopedPermissionResolver["resolvePathPolicy"]>();
|
|
31
29
|
if (defaultCheck) {
|
|
32
30
|
resolve.mockReturnValue(defaultCheck);
|
|
33
|
-
resolvePathPolicy.mockReturnValue(defaultCheck);
|
|
34
31
|
}
|
|
35
|
-
return { resolve
|
|
32
|
+
return { resolve };
|
|
36
33
|
}
|
|
37
34
|
|
|
38
35
|
/**
|
|
@@ -95,7 +92,6 @@ export function makeGateRunner(
|
|
|
95
92
|
overrides: {
|
|
96
93
|
resolveResult?: PermissionCheckResult;
|
|
97
94
|
resolve?: ScopedPermissionResolver["resolve"];
|
|
98
|
-
resolvePathPolicy?: ScopedPermissionResolver["resolvePathPolicy"];
|
|
99
95
|
recordSessionApproval?: SessionApprovalRecorder["recordSessionApproval"];
|
|
100
96
|
canConfirm?: GatePrompter["canConfirm"];
|
|
101
97
|
prompt?: GatePrompter["prompt"];
|
|
@@ -110,13 +106,6 @@ export function makeGateRunner(
|
|
|
110
106
|
.mockReturnValue(
|
|
111
107
|
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
112
108
|
);
|
|
113
|
-
const resolvePathPolicy =
|
|
114
|
-
overrides.resolvePathPolicy ??
|
|
115
|
-
vi
|
|
116
|
-
.fn<ScopedPermissionResolver["resolvePathPolicy"]>()
|
|
117
|
-
.mockReturnValue(
|
|
118
|
-
overrides.resolveResult ?? makeCheckResult({ matchedPattern: "*" }),
|
|
119
|
-
);
|
|
120
109
|
const recordSessionApproval =
|
|
121
110
|
overrides.recordSessionApproval ??
|
|
122
111
|
(vi.fn() as SessionApprovalRecorder["recordSessionApproval"]);
|
|
@@ -129,7 +118,7 @@ export function makeGateRunner(
|
|
|
129
118
|
.fn<GatePrompter["prompt"]>()
|
|
130
119
|
.mockResolvedValue({ approved: true, state: "approved" });
|
|
131
120
|
const runner = new GateRunner(
|
|
132
|
-
{ resolve
|
|
121
|
+
{ resolve },
|
|
133
122
|
{ recordSessionApproval },
|
|
134
123
|
{ canConfirm, prompt },
|
|
135
124
|
reporter,
|
|
@@ -138,7 +127,6 @@ export function makeGateRunner(
|
|
|
138
127
|
runner,
|
|
139
128
|
deps: {
|
|
140
129
|
resolve,
|
|
141
|
-
resolvePathPolicy,
|
|
142
130
|
recordSessionApproval,
|
|
143
131
|
canConfirm,
|
|
144
132
|
prompt,
|
|
@@ -217,22 +205,22 @@ export function makePathDispatchResolver(
|
|
|
217
205
|
defaultResult: PermissionCheckResult,
|
|
218
206
|
) {
|
|
219
207
|
const resolve = vi.fn<ScopedPermissionResolver["resolve"]>();
|
|
220
|
-
resolve.mockImplementation((
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
208
|
+
resolve.mockImplementation((intent) => {
|
|
209
|
+
if (intent.kind === "tool") {
|
|
210
|
+
const path = (intent.input as Record<string, unknown>).path;
|
|
211
|
+
if (typeof path === "string" && path in byPath) {
|
|
212
|
+
return byPath[path];
|
|
213
|
+
}
|
|
214
|
+
return defaultResult;
|
|
224
215
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const resolvePathPolicy =
|
|
228
|
-
vi.fn<ScopedPermissionResolver["resolvePathPolicy"]>();
|
|
229
|
-
resolvePathPolicy.mockImplementation((values) => {
|
|
216
|
+
const values =
|
|
217
|
+
intent.kind === "access-path" ? intent.path.matchValues() : intent.values;
|
|
230
218
|
for (const value of values) {
|
|
231
219
|
if (value in byPath) return byPath[value];
|
|
232
220
|
}
|
|
233
221
|
return defaultResult;
|
|
234
222
|
});
|
|
235
|
-
return { resolve
|
|
223
|
+
return { resolve };
|
|
236
224
|
}
|
|
237
225
|
|
|
238
226
|
/**
|
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
* `makeHandler` builds a real PermissionSession + PermissionResolver and wires
|
|
5
5
|
* them into the handler and pipelines exactly as `index.ts` does.
|
|
6
6
|
* Call-site overrides for permission results flow through
|
|
7
|
-
* `permissionManager.
|
|
7
|
+
* `permissionManager.check`; session state overrides are applied
|
|
8
8
|
* via vi.spyOn on the real session instance.
|
|
9
9
|
*/
|
|
10
10
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import { vi } from "vitest";
|
|
12
12
|
|
|
13
|
+
import type { ResolvedAccessIntent } from "#src/access-intent/access-intent";
|
|
13
14
|
import { GateDecisionReporter } from "#src/decision-reporter";
|
|
14
15
|
import type { GatePrompter } from "#src/gate-prompter";
|
|
15
16
|
import { GateRunner } from "#src/handlers/gates/runner";
|
|
@@ -238,21 +239,26 @@ export function makeHandler(overrides?: {
|
|
|
238
239
|
const so = overrides?.session;
|
|
239
240
|
const surfaceCheck = so?.checkPermission;
|
|
240
241
|
if (surfaceCheck) {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
242
|
+
// Route the unified check(intent) through the surface dispatcher so
|
|
243
|
+
// makeSurfaceCheck / makeBashCommandCheck overrides apply to all gate
|
|
244
|
+
// paths via the single manager entry point (#478).
|
|
245
|
+
vi.mocked(permissionManager.check).mockImplementation(
|
|
246
|
+
(intent: ResolvedAccessIntent, sessionRules) => {
|
|
247
|
+
if (intent.kind === "path-values") {
|
|
248
|
+
return surfaceCheck(
|
|
249
|
+
intent.surface,
|
|
250
|
+
{ path: intent.values[0] ?? "*" },
|
|
251
|
+
intent.agentName,
|
|
252
|
+
sessionRules,
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
return surfaceCheck(
|
|
256
|
+
intent.surface,
|
|
257
|
+
intent.input,
|
|
258
|
+
intent.agentName,
|
|
254
259
|
sessionRules,
|
|
255
|
-
)
|
|
260
|
+
);
|
|
261
|
+
},
|
|
256
262
|
);
|
|
257
263
|
}
|
|
258
264
|
if (so?.getActiveSkillEntries) {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { vi } from "vitest";
|
|
15
15
|
|
|
16
|
+
import type { ResolvedAccessIntent } from "#src/access-intent/access-intent";
|
|
16
17
|
import type { SessionConfigStore } from "#src/config-store";
|
|
17
18
|
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
18
19
|
import type { ExtensionPaths } from "#src/extension-paths";
|
|
@@ -87,12 +88,10 @@ export function makeForwarding(): ForwardingController {
|
|
|
87
88
|
export function makeFakePermissionManager() {
|
|
88
89
|
return {
|
|
89
90
|
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
90
|
-
|
|
91
|
+
check: vi
|
|
91
92
|
.fn<
|
|
92
93
|
(
|
|
93
|
-
|
|
94
|
-
input: unknown,
|
|
95
|
-
agentName?: string,
|
|
94
|
+
intent: ResolvedAccessIntent,
|
|
96
95
|
sessionRules?: Ruleset,
|
|
97
96
|
) => PermissionCheckResult
|
|
98
97
|
>()
|
|
@@ -102,20 +101,6 @@ export function makeFakePermissionManager() {
|
|
|
102
101
|
source: "tool",
|
|
103
102
|
origin: "builtin",
|
|
104
103
|
}),
|
|
105
|
-
checkPathPolicy: vi
|
|
106
|
-
.fn<
|
|
107
|
-
(
|
|
108
|
-
values: readonly string[],
|
|
109
|
-
agentName?: string,
|
|
110
|
-
sessionRules?: Ruleset,
|
|
111
|
-
) => PermissionCheckResult
|
|
112
|
-
>()
|
|
113
|
-
.mockReturnValue({
|
|
114
|
-
state: "allow",
|
|
115
|
-
toolName: "path",
|
|
116
|
-
source: "special",
|
|
117
|
-
origin: "builtin",
|
|
118
|
-
}),
|
|
119
104
|
getToolPermission: vi
|
|
120
105
|
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
121
106
|
.mockReturnValue("allow"),
|
package/test/path-utils.test.ts
CHANGED
|
@@ -22,7 +22,6 @@ vi.mock("node:fs", () => ({
|
|
|
22
22
|
|
|
23
23
|
import {
|
|
24
24
|
canonicalNormalizePathForComparison,
|
|
25
|
-
getExternalDirectoryPolicyValues,
|
|
26
25
|
getPathBearingToolPath,
|
|
27
26
|
getPathPolicyValues,
|
|
28
27
|
getToolInputPath,
|
|
@@ -615,36 +614,3 @@ describe("getPathPolicyValues", () => {
|
|
|
615
614
|
expect(getPathPolicyValues(" ", { cwd })).toEqual([]);
|
|
616
615
|
});
|
|
617
616
|
});
|
|
618
|
-
|
|
619
|
-
describe("getExternalDirectoryPolicyValues", () => {
|
|
620
|
-
const cwd = "/projects/my-app";
|
|
621
|
-
|
|
622
|
-
beforeEach(() => {
|
|
623
|
-
realpathSync.mockReset();
|
|
624
|
-
realpathSync.mockImplementation((p: string) => p);
|
|
625
|
-
});
|
|
626
|
-
|
|
627
|
-
test("adds the symlink-resolved alias alongside the typed path", () => {
|
|
628
|
-
// /tmp -> /private/tmp (the macOS symlink from the bug report).
|
|
629
|
-
realpathSync.mockImplementation((p: string) =>
|
|
630
|
-
p.startsWith("/tmp") ? `/private${p}` : p,
|
|
631
|
-
);
|
|
632
|
-
expect(getExternalDirectoryPolicyValues("/tmp/x", cwd)).toEqual([
|
|
633
|
-
"/tmp/x",
|
|
634
|
-
"/private/tmp/x",
|
|
635
|
-
]);
|
|
636
|
-
});
|
|
637
|
-
|
|
638
|
-
test("dedups when the canonical form equals the lexical form", () => {
|
|
639
|
-
expect(getExternalDirectoryPolicyValues("/etc/hosts", cwd)).toEqual([
|
|
640
|
-
"/etc/hosts",
|
|
641
|
-
]);
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
test("keeps the relative aliases for an in-cwd token without duplicating", () => {
|
|
645
|
-
expect(getExternalDirectoryPolicyValues("src/foo.ts", cwd)).toEqual([
|
|
646
|
-
"/projects/my-app/src/foo.ts",
|
|
647
|
-
"src/foo.ts",
|
|
648
|
-
]);
|
|
649
|
-
});
|
|
650
|
-
});
|
|
@@ -37,7 +37,7 @@ function makeDeps(
|
|
|
37
37
|
): PermissionRpcDeps {
|
|
38
38
|
return {
|
|
39
39
|
permissionManager: {
|
|
40
|
-
|
|
40
|
+
check: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
41
41
|
},
|
|
42
42
|
sessionRules: { getRuleset: vi.fn().mockReturnValue([]) },
|
|
43
43
|
session: { getRuntimeContext: vi.fn().mockReturnValue(null) },
|
|
@@ -74,7 +74,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
74
74
|
const bus = createEventBus();
|
|
75
75
|
const deps = makeDeps({
|
|
76
76
|
permissionManager: {
|
|
77
|
-
|
|
77
|
+
check: vi.fn().mockReturnValue(makeCheckResult("allow")),
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
80
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -101,7 +101,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
101
101
|
const bus = createEventBus();
|
|
102
102
|
const deps = makeDeps({
|
|
103
103
|
permissionManager: {
|
|
104
|
-
|
|
104
|
+
check: vi.fn().mockReturnValue(
|
|
105
105
|
makeCheckResult("deny", {
|
|
106
106
|
origin: "project",
|
|
107
107
|
matchedPattern: "rm *",
|
|
@@ -132,7 +132,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
132
132
|
const bus = createEventBus();
|
|
133
133
|
const deps = makeDeps({
|
|
134
134
|
permissionManager: {
|
|
135
|
-
|
|
135
|
+
check: vi
|
|
136
136
|
.fn()
|
|
137
137
|
.mockReturnValue(
|
|
138
138
|
makeCheckResult("ask", { matchedPattern: undefined }),
|
|
@@ -157,11 +157,11 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
157
157
|
}
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
it("passes agentName to
|
|
161
|
-
const
|
|
160
|
+
it("passes agentName to check when provided", async () => {
|
|
161
|
+
const check = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
162
162
|
const bus = createEventBus();
|
|
163
163
|
const deps = makeDeps({
|
|
164
|
-
permissionManager: {
|
|
164
|
+
permissionManager: { check },
|
|
165
165
|
});
|
|
166
166
|
registerPermissionRpcHandlers(bus, deps);
|
|
167
167
|
|
|
@@ -177,10 +177,12 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
177
177
|
});
|
|
178
178
|
await replyPromise;
|
|
179
179
|
|
|
180
|
-
expect(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
180
|
+
expect(check).toHaveBeenCalledWith(
|
|
181
|
+
expect.objectContaining({
|
|
182
|
+
kind: "tool",
|
|
183
|
+
surface: "bash",
|
|
184
|
+
agentName: "Worker",
|
|
185
|
+
}),
|
|
184
186
|
expect.anything(),
|
|
185
187
|
);
|
|
186
188
|
});
|
|
@@ -194,10 +196,10 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
194
196
|
origin: "session" as const,
|
|
195
197
|
},
|
|
196
198
|
];
|
|
197
|
-
const
|
|
199
|
+
const check = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
198
200
|
const bus = createEventBus();
|
|
199
201
|
const deps = makeDeps({
|
|
200
|
-
permissionManager: {
|
|
202
|
+
permissionManager: { check },
|
|
201
203
|
sessionRules: { getRuleset: vi.fn().mockReturnValue(sessionRules) },
|
|
202
204
|
});
|
|
203
205
|
registerPermissionRpcHandlers(bus, deps);
|
|
@@ -213,10 +215,12 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
213
215
|
});
|
|
214
216
|
await replyPromise;
|
|
215
217
|
|
|
216
|
-
expect(
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
expect(check).toHaveBeenCalledWith(
|
|
219
|
+
expect.objectContaining({
|
|
220
|
+
kind: "tool",
|
|
221
|
+
surface: "bash",
|
|
222
|
+
agentName: undefined,
|
|
223
|
+
}),
|
|
220
224
|
sessionRules,
|
|
221
225
|
);
|
|
222
226
|
});
|
|
@@ -243,10 +247,10 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
243
247
|
});
|
|
244
248
|
|
|
245
249
|
it("unsubCheck stops the handler from firing", async () => {
|
|
246
|
-
const
|
|
250
|
+
const check = vi.fn().mockReturnValue(makeCheckResult("allow"));
|
|
247
251
|
const bus = createEventBus();
|
|
248
252
|
const deps = makeDeps({
|
|
249
|
-
permissionManager: {
|
|
253
|
+
permissionManager: { check },
|
|
250
254
|
});
|
|
251
255
|
const handles = registerPermissionRpcHandlers(bus, deps);
|
|
252
256
|
handles.unsubCheck();
|
|
@@ -258,7 +262,7 @@ describe("registerPermissionRpcHandlers — permissions:rpc:check", () => {
|
|
|
258
262
|
|
|
259
263
|
// Give async handlers a chance to fire
|
|
260
264
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
261
|
-
expect(
|
|
265
|
+
expect(check).not.toHaveBeenCalled();
|
|
262
266
|
});
|
|
263
267
|
});
|
|
264
268
|
|