@gotgenes/pi-permission-system 17.1.1 → 18.0.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 +26 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/access-intent/access-intent.ts +6 -3
- package/src/access-intent/bash/program.ts +0 -4
- package/src/handlers/gates/tool-call-gate-pipeline.ts +61 -22
- package/src/handlers/gates/tool.ts +10 -14
- package/src/index.ts +8 -6
- package/src/input-normalizer.ts +54 -56
- package/src/permission-event-rpc.ts +21 -15
- package/src/permission-manager.ts +3 -6
- package/src/permission-session.ts +0 -5
- package/src/permissions-service.ts +33 -12
- package/test/access-intent/bash/program.test.ts +1 -1
- package/test/composition-root.test.ts +22 -0
- package/test/handlers/gates/tool-call-gate-pipeline.test.ts +73 -0
- package/test/handlers/gates/tool.test.ts +25 -16
- package/test/helpers/gate-fixtures.ts +0 -3
- package/test/input-normalizer.test.ts +163 -270
- package/test/path-utils.test.ts +1 -1
- package/test/permission-event-rpc.test.ts +80 -65
- package/test/permission-manager-unified.test.ts +134 -145
- package/test/permissions-service.test.ts +84 -72
- package/test/service.test.ts +56 -103
|
@@ -1,34 +1,42 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import type {
|
|
1
|
+
import { beforeEach, describe, expect, it, type Mock, vi } from "vitest";
|
|
2
|
+
import type { AccessIntent } from "#src/access-intent/access-intent";
|
|
3
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
3
4
|
import { LocalPermissionsService } from "#src/permissions-service";
|
|
4
|
-
import type { Ruleset } from "#src/rule";
|
|
5
|
-
import type { SessionRules } from "#src/session-rules";
|
|
6
5
|
import type { ToolAccessExtractorRegistrar } from "#src/tool-access-extractor-registry";
|
|
7
6
|
import type {
|
|
8
7
|
ToolInputFormatter,
|
|
9
8
|
ToolInputFormatterRegistrar,
|
|
10
9
|
} from "#src/tool-input-formatter-registry";
|
|
10
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
11
11
|
|
|
12
12
|
import { makeCheckResult } from "#test/helpers/handler-fixtures";
|
|
13
|
-
import { makeFakePermissionManager } from "#test/helpers/session-fixtures";
|
|
14
13
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
vi.fn<(surface: string, value?: string) => unknown>(),
|
|
14
|
+
// Mock node:fs so realpathSync (the canonical alias) is controllable.
|
|
15
|
+
const realpathSync = vi.hoisted(() =>
|
|
16
|
+
vi.fn<(path: string) => string>((p) => p),
|
|
19
17
|
);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
vi.mock("node:fs", () => ({
|
|
19
|
+
realpathSync,
|
|
20
|
+
default: { realpathSync },
|
|
23
21
|
}));
|
|
24
22
|
|
|
25
23
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
interface FakeResolver {
|
|
26
|
+
resolve: Mock<(intent: AccessIntent) => PermissionCheckResult>;
|
|
27
|
+
getToolPermission: Mock<
|
|
28
|
+
(toolName: string, agentName?: string) => PermissionState
|
|
29
|
+
>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function makeResolver(): FakeResolver {
|
|
30
33
|
return {
|
|
31
|
-
|
|
34
|
+
resolve: vi
|
|
35
|
+
.fn<(intent: AccessIntent) => PermissionCheckResult>()
|
|
36
|
+
.mockReturnValue(makeCheckResult()),
|
|
37
|
+
getToolPermission: vi
|
|
38
|
+
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
39
|
+
.mockReturnValue("ask"),
|
|
32
40
|
};
|
|
33
41
|
}
|
|
34
42
|
|
|
@@ -49,90 +57,97 @@ function makeAccessExtractorRegistry(): ToolAccessExtractorRegistrar {
|
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
function makeService(overrides?: {
|
|
52
|
-
|
|
53
|
-
sessionRules?: Pick<SessionRules, "getRuleset">;
|
|
60
|
+
resolver?: FakeResolver;
|
|
54
61
|
formatterRegistry?: ToolInputFormatterRegistrar;
|
|
55
62
|
accessExtractorRegistry?: ToolAccessExtractorRegistrar;
|
|
56
63
|
}) {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
64
|
+
const resolver = overrides?.resolver ?? makeResolver();
|
|
65
|
+
// The published service always answers against the parent session's cwd.
|
|
66
|
+
const session = { getPathNormalizer: () => normalizer };
|
|
60
67
|
const formatterRegistry =
|
|
61
68
|
overrides?.formatterRegistry ?? makeFormatterRegistry();
|
|
62
69
|
const accessExtractorRegistry =
|
|
63
70
|
overrides?.accessExtractorRegistry ?? makeAccessExtractorRegistry();
|
|
64
71
|
const service = new LocalPermissionsService(
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
resolver,
|
|
73
|
+
session,
|
|
67
74
|
formatterRegistry,
|
|
68
75
|
accessExtractorRegistry,
|
|
69
76
|
);
|
|
70
|
-
return {
|
|
71
|
-
service,
|
|
72
|
-
permissionManager,
|
|
73
|
-
sessionRules,
|
|
74
|
-
formatterRegistry,
|
|
75
|
-
accessExtractorRegistry,
|
|
76
|
-
};
|
|
77
|
+
return { service, resolver, formatterRegistry, accessExtractorRegistry };
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
const normalizer = new PathNormalizer("linux", "/test/project");
|
|
81
|
+
|
|
79
82
|
// ── tests ──────────────────────────────────────────────────────────────────
|
|
80
83
|
|
|
81
84
|
beforeEach(() => {
|
|
82
|
-
|
|
83
|
-
|
|
85
|
+
realpathSync.mockReset();
|
|
86
|
+
realpathSync.mockImplementation((p: string) => p);
|
|
84
87
|
});
|
|
85
88
|
|
|
86
89
|
describe("checkPermission", () => {
|
|
87
|
-
it("
|
|
88
|
-
const { service } = makeService();
|
|
89
|
-
service.checkPermission("bash", "echo hi");
|
|
90
|
-
expect(
|
|
90
|
+
it("resolves a non-path surface through a tool intent", () => {
|
|
91
|
+
const { service, resolver } = makeService();
|
|
92
|
+
service.checkPermission("bash", "echo hi", "my-agent");
|
|
93
|
+
expect(resolver.resolve).toHaveBeenCalledWith({
|
|
94
|
+
kind: "tool",
|
|
95
|
+
surface: "bash",
|
|
96
|
+
input: { command: "echo hi" },
|
|
97
|
+
agentName: "my-agent",
|
|
98
|
+
});
|
|
91
99
|
});
|
|
92
100
|
|
|
93
|
-
it("
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
101
|
+
it("resolves an external_directory path query through an access-path intent matching the canonical alias", () => {
|
|
102
|
+
realpathSync.mockImplementation((p: string) =>
|
|
103
|
+
p === "/test/project/link" ? "/test/project/real" : p,
|
|
104
|
+
);
|
|
105
|
+
const { service, resolver } = makeService();
|
|
106
|
+
service.checkPermission("external_directory", "link");
|
|
107
|
+
expect(resolver.resolve).toHaveBeenCalledTimes(1);
|
|
108
|
+
const intent = resolver.resolve.mock.calls[0][0];
|
|
109
|
+
expect(intent.kind).toBe("access-path");
|
|
110
|
+
if (intent.kind === "access-path") {
|
|
111
|
+
expect(intent.surface).toBe("external_directory");
|
|
112
|
+
expect(intent.path.matchValues()).toContain("/test/project/real");
|
|
113
|
+
}
|
|
97
114
|
});
|
|
98
115
|
|
|
99
|
-
it("
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
];
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
},
|
|
116
|
-
ruleset,
|
|
117
|
-
);
|
|
118
|
-
void sessionRules; // used indirectly
|
|
116
|
+
it("resolves a path-bearing tool query (read) through an access-path intent", () => {
|
|
117
|
+
const { service, resolver } = makeService();
|
|
118
|
+
service.checkPermission("read", "/test/project/.env");
|
|
119
|
+
const intent = resolver.resolve.mock.calls[0][0];
|
|
120
|
+
expect(intent.kind).toBe("access-path");
|
|
121
|
+
if (intent.kind === "access-path") {
|
|
122
|
+
expect(intent.surface).toBe("read");
|
|
123
|
+
expect(intent.path.value()).toBe("/test/project/.env");
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("falls back to a tool intent for a value-less path query", () => {
|
|
128
|
+
const { service, resolver } = makeService();
|
|
129
|
+
service.checkPermission("path");
|
|
130
|
+
const intent = resolver.resolve.mock.calls[0][0];
|
|
131
|
+
expect(intent.kind).toBe("tool");
|
|
119
132
|
});
|
|
120
133
|
|
|
121
|
-
it("returns the result from
|
|
134
|
+
it("returns the result from resolver.resolve", () => {
|
|
122
135
|
const expected = makeCheckResult({ state: "deny", toolName: "bash" });
|
|
123
|
-
const
|
|
124
|
-
|
|
136
|
+
const resolver = makeResolver();
|
|
137
|
+
resolver.resolve.mockReturnValue(expected);
|
|
138
|
+
const { service } = makeService({ resolver });
|
|
125
139
|
const result = service.checkPermission("bash", "rm -rf /");
|
|
126
140
|
expect(result).toBe(expected);
|
|
127
141
|
});
|
|
128
142
|
});
|
|
129
143
|
|
|
130
144
|
describe("getToolPermission", () => {
|
|
131
|
-
it("delegates to
|
|
132
|
-
const
|
|
133
|
-
|
|
145
|
+
it("delegates to resolver.getToolPermission", () => {
|
|
146
|
+
const resolver = makeResolver();
|
|
147
|
+
resolver.getToolPermission.mockReturnValue("deny");
|
|
148
|
+
const { service } = makeService({ resolver });
|
|
134
149
|
const result = service.getToolPermission("write", "my-agent");
|
|
135
|
-
expect(
|
|
150
|
+
expect(resolver.getToolPermission).toHaveBeenCalledWith(
|
|
136
151
|
"write",
|
|
137
152
|
"my-agent",
|
|
138
153
|
);
|
|
@@ -140,12 +155,9 @@ describe("getToolPermission", () => {
|
|
|
140
155
|
});
|
|
141
156
|
|
|
142
157
|
it("omits agentName when not provided", () => {
|
|
143
|
-
const { service,
|
|
158
|
+
const { service, resolver } = makeService();
|
|
144
159
|
service.getToolPermission("read");
|
|
145
|
-
expect(
|
|
146
|
-
"read",
|
|
147
|
-
undefined,
|
|
148
|
-
);
|
|
160
|
+
expect(resolver.getToolPermission).toHaveBeenCalledWith("read", undefined);
|
|
149
161
|
});
|
|
150
162
|
});
|
|
151
163
|
|
package/test/service.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import type { AccessIntent } from "#src/access-intent/access-intent";
|
|
3
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
4
|
+
import { LocalPermissionsService } from "#src/permissions-service";
|
|
3
5
|
import type { PermissionsService } from "#src/service";
|
|
4
6
|
import {
|
|
5
7
|
getPermissionsService,
|
|
@@ -8,7 +10,7 @@ import {
|
|
|
8
10
|
} from "#src/service";
|
|
9
11
|
import { ToolAccessExtractorRegistry } from "#src/tool-access-extractor-registry";
|
|
10
12
|
import { ToolInputFormatterRegistry } from "#src/tool-input-formatter-registry";
|
|
11
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
13
|
+
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
12
14
|
|
|
13
15
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
14
16
|
|
|
@@ -77,7 +79,7 @@ describe("globalThis accessor", () => {
|
|
|
77
79
|
|
|
78
80
|
// ── service adapter delegation ─────────────────────────────────────────────
|
|
79
81
|
|
|
80
|
-
describe("service
|
|
82
|
+
describe("service round-trip through the global slot", () => {
|
|
81
83
|
afterEach(() => {
|
|
82
84
|
const current = getPermissionsService();
|
|
83
85
|
if (current) {
|
|
@@ -93,117 +95,68 @@ describe("service adapter delegation", () => {
|
|
|
93
95
|
origin: "global",
|
|
94
96
|
};
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const retrieved = getPermissionsService()!;
|
|
118
|
-
const result = retrieved.checkPermission("bash", "git push");
|
|
119
|
-
|
|
120
|
-
expect(result).toBe(fakeResult);
|
|
121
|
-
expect(checkPermission).toHaveBeenCalledWith(
|
|
122
|
-
"bash",
|
|
123
|
-
{ command: "git push" },
|
|
124
|
-
undefined,
|
|
125
|
-
sessionRules,
|
|
98
|
+
function makeResolver() {
|
|
99
|
+
return {
|
|
100
|
+
resolve: vi
|
|
101
|
+
.fn<(intent: AccessIntent) => PermissionCheckResult>()
|
|
102
|
+
.mockReturnValue(fakeResult),
|
|
103
|
+
getToolPermission: vi
|
|
104
|
+
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
105
|
+
.mockReturnValue("ask"),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function publishLocalService(resolver: ReturnType<typeof makeResolver>) {
|
|
110
|
+
publishPermissionsService(
|
|
111
|
+
new LocalPermissionsService(
|
|
112
|
+
resolver,
|
|
113
|
+
{
|
|
114
|
+
getPathNormalizer: () => new PathNormalizer("linux", "/test/project"),
|
|
115
|
+
},
|
|
116
|
+
new ToolInputFormatterRegistry(),
|
|
117
|
+
new ToolAccessExtractorRegistry(),
|
|
118
|
+
),
|
|
126
119
|
);
|
|
127
|
-
}
|
|
120
|
+
}
|
|
128
121
|
|
|
129
|
-
it("
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return checkPermission(surface, input, agentName, []);
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
publishPermissionsService(service);
|
|
140
|
-
getPermissionsService()!.checkPermission("skill", "my-skill", "Explore");
|
|
141
|
-
|
|
142
|
-
expect(checkPermission).toHaveBeenCalledWith(
|
|
143
|
-
"skill",
|
|
144
|
-
{ name: "my-skill" },
|
|
122
|
+
it("resolves a non-path query via a tool intent", () => {
|
|
123
|
+
const resolver = makeResolver();
|
|
124
|
+
publishLocalService(resolver);
|
|
125
|
+
const result = getPermissionsService()!.checkPermission(
|
|
126
|
+
"bash",
|
|
127
|
+
"git push",
|
|
145
128
|
"Explore",
|
|
146
|
-
[],
|
|
147
129
|
);
|
|
130
|
+
expect(result).toBe(fakeResult);
|
|
131
|
+
expect(resolver.resolve).toHaveBeenCalledWith({
|
|
132
|
+
kind: "tool",
|
|
133
|
+
surface: "bash",
|
|
134
|
+
input: { command: "git push" },
|
|
135
|
+
agentName: "Explore",
|
|
136
|
+
});
|
|
148
137
|
});
|
|
149
138
|
|
|
150
|
-
it("
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
registerToolAccessExtractor: vi.fn(),
|
|
161
|
-
};
|
|
139
|
+
it("resolves a path-surface query via an access-path intent", () => {
|
|
140
|
+
const resolver = makeResolver();
|
|
141
|
+
publishLocalService(resolver);
|
|
142
|
+
getPermissionsService()!.checkPermission("read", "/test/project/.env");
|
|
143
|
+
const intent = resolver.resolve.mock.calls[0][0];
|
|
144
|
+
expect(intent.kind).toBe("access-path");
|
|
145
|
+
if (intent.kind === "access-path") {
|
|
146
|
+
expect(intent.surface).toBe("read");
|
|
147
|
+
}
|
|
148
|
+
});
|
|
162
149
|
|
|
163
|
-
|
|
150
|
+
it("delegates getToolPermission through the resolver", () => {
|
|
151
|
+
const resolver = makeResolver();
|
|
152
|
+
resolver.getToolPermission.mockReturnValue("deny");
|
|
153
|
+
publishLocalService(resolver);
|
|
164
154
|
const result = getPermissionsService()!.getToolPermission(
|
|
165
|
-
"
|
|
155
|
+
"write",
|
|
166
156
|
"Explore",
|
|
167
157
|
);
|
|
168
|
-
|
|
169
158
|
expect(result).toBe("deny");
|
|
170
|
-
expect(
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it("getToolPermission works without agentName", () => {
|
|
174
|
-
const getToolPermissionFn = vi.fn(
|
|
175
|
-
(_t: string, _a?: string): "ask" => "ask",
|
|
176
|
-
);
|
|
177
|
-
const service: PermissionsService = {
|
|
178
|
-
checkPermission: vi.fn(),
|
|
179
|
-
getToolPermission(toolName, agentName) {
|
|
180
|
-
return getToolPermissionFn(toolName, agentName);
|
|
181
|
-
},
|
|
182
|
-
registerToolInputFormatter: vi.fn(),
|
|
183
|
-
registerToolAccessExtractor: vi.fn(),
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
publishPermissionsService(service);
|
|
187
|
-
const result = getPermissionsService()!.getToolPermission("write");
|
|
188
|
-
|
|
189
|
-
expect(result).toBe("ask");
|
|
190
|
-
expect(getToolPermissionFn).toHaveBeenCalledWith("write", undefined);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it("checkPermission uses empty object for unknown surfaces", () => {
|
|
194
|
-
const checkPermission = vi.fn().mockReturnValue(fakeResult);
|
|
195
|
-
|
|
196
|
-
const service = makeService({
|
|
197
|
-
checkPermission(surface, value, agentName) {
|
|
198
|
-
const input = buildInputForSurface(surface, value);
|
|
199
|
-
return checkPermission(surface, input, agentName, []);
|
|
200
|
-
},
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
publishPermissionsService(service);
|
|
204
|
-
getPermissionsService()!.checkPermission("read", "/tmp/file");
|
|
205
|
-
|
|
206
|
-
expect(checkPermission).toHaveBeenCalledWith("read", {}, undefined, []);
|
|
159
|
+
expect(resolver.getToolPermission).toHaveBeenCalledWith("write", "Explore");
|
|
207
160
|
});
|
|
208
161
|
});
|
|
209
162
|
|