@gotgenes/pi-permission-system 17.1.0 → 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 +33 -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/bash-path-resolver.ts +16 -12
- package/src/access-intent/bash/program.ts +0 -4
- package/src/access-intent/bash/token-classification.ts +23 -0
- package/src/handlers/before-agent-start.ts +1 -2
- package/src/handlers/gates/external-directory.ts +2 -12
- package/src/handlers/gates/skill-read.ts +4 -8
- package/src/handlers/gates/tool-call-gate-pipeline.ts +62 -24
- package/src/handlers/gates/tool.ts +10 -14
- package/src/index.ts +8 -6
- package/src/input-normalizer.ts +54 -56
- package/src/path-normalizer.ts +30 -0
- 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/src/skill-prompt-sanitizer.ts +8 -21
- package/test/access-intent/bash/program.test.ts +1 -1
- package/test/access-intent/bash/token-classification.test.ts +75 -0
- package/test/bash-external-directory.test.ts +38 -0
- package/test/composition-root.test.ts +22 -0
- package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -3
- package/test/handlers/gates/external-directory.test.ts +0 -1
- package/test/handlers/gates/skill-read.test.ts +16 -12
- 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-normalizer.test.ts +43 -0
- 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
- package/test/skill-prompt-sanitizer.test.ts +31 -65
|
@@ -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
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
2
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
3
|
+
import { PathNormalizer } from "#src/path-normalizer";
|
|
3
4
|
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
4
5
|
import {
|
|
5
6
|
findSkillPathMatch,
|
|
@@ -30,6 +31,11 @@ afterEach(() => {
|
|
|
30
31
|
|
|
31
32
|
const CWD = "/projects/my-app";
|
|
32
33
|
|
|
34
|
+
// `findSkillPathMatch` only uses the normalizer's platform (it compares two
|
|
35
|
+
// already-absolute paths via `isWithinDirectory`), so this CWD-baked instance
|
|
36
|
+
// serves every call regardless of the entries' cwd.
|
|
37
|
+
const normalizer = new PathNormalizer("linux", CWD);
|
|
38
|
+
|
|
33
39
|
function makeManager(
|
|
34
40
|
defaultState: "allow" | "deny" | "ask" = "allow",
|
|
35
41
|
overrides: Record<string, "allow" | "deny" | "ask"> = {},
|
|
@@ -72,13 +78,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
72
78
|
test("returns unchanged prompt and empty entries when no skills section present", () => {
|
|
73
79
|
const input = "You are a helpful assistant.";
|
|
74
80
|
const manager = makeManager("allow");
|
|
75
|
-
const result = resolveSkillPromptEntries(
|
|
76
|
-
input,
|
|
77
|
-
manager,
|
|
78
|
-
null,
|
|
79
|
-
CWD,
|
|
80
|
-
"linux",
|
|
81
|
-
);
|
|
81
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
82
82
|
expect(result.prompt).toBe(input);
|
|
83
83
|
expect(result.entries).toEqual([]);
|
|
84
84
|
expect(manager.checkPermission).not.toHaveBeenCalled();
|
|
@@ -87,13 +87,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
87
87
|
test("keeps all skills when all are allowed", () => {
|
|
88
88
|
const input = availableSkillsSection("librarian", "ask-user");
|
|
89
89
|
const manager = makeManager("allow");
|
|
90
|
-
const result = resolveSkillPromptEntries(
|
|
91
|
-
input,
|
|
92
|
-
manager,
|
|
93
|
-
null,
|
|
94
|
-
CWD,
|
|
95
|
-
"linux",
|
|
96
|
-
);
|
|
90
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
97
91
|
expect(result.prompt).toContain("librarian");
|
|
98
92
|
expect(result.prompt).toContain("ask-user");
|
|
99
93
|
expect(result.entries).toHaveLength(2);
|
|
@@ -102,13 +96,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
102
96
|
test("removes denied skill from section", () => {
|
|
103
97
|
const input = availableSkillsSection("librarian", "dangerous");
|
|
104
98
|
const manager = makeManager("allow", { dangerous: "deny" });
|
|
105
|
-
const result = resolveSkillPromptEntries(
|
|
106
|
-
input,
|
|
107
|
-
manager,
|
|
108
|
-
null,
|
|
109
|
-
CWD,
|
|
110
|
-
"linux",
|
|
111
|
-
);
|
|
99
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
112
100
|
expect(result.prompt).toContain("librarian");
|
|
113
101
|
expect(result.prompt).not.toContain("dangerous");
|
|
114
102
|
// denied skill is excluded from returned entries
|
|
@@ -118,13 +106,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
118
106
|
test("removes entire section when all skills are denied", () => {
|
|
119
107
|
const input = `Intro\n${availableSkillsSection("dangerous")}\nOutro`;
|
|
120
108
|
const manager = makeManager("deny");
|
|
121
|
-
const result = resolveSkillPromptEntries(
|
|
122
|
-
input,
|
|
123
|
-
manager,
|
|
124
|
-
null,
|
|
125
|
-
CWD,
|
|
126
|
-
"linux",
|
|
127
|
-
);
|
|
109
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
128
110
|
expect(result.prompt).not.toContain("<available_skills>");
|
|
129
111
|
expect(result.prompt).toContain("Intro");
|
|
130
112
|
expect(result.prompt).toContain("Outro");
|
|
@@ -134,13 +116,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
134
116
|
test("keeps ask-state skills in section and entries", () => {
|
|
135
117
|
const input = availableSkillsSection("librarian");
|
|
136
118
|
const manager = makeManager("ask");
|
|
137
|
-
const result = resolveSkillPromptEntries(
|
|
138
|
-
input,
|
|
139
|
-
manager,
|
|
140
|
-
null,
|
|
141
|
-
CWD,
|
|
142
|
-
"linux",
|
|
143
|
-
);
|
|
119
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
144
120
|
expect(result.prompt).toContain("librarian");
|
|
145
121
|
expect(result.entries).toHaveLength(1);
|
|
146
122
|
expect(result.entries[0].state).toBe("ask");
|
|
@@ -149,7 +125,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
149
125
|
test("delegates permission check to permissionManager for each skill", () => {
|
|
150
126
|
const input = availableSkillsSection("alpha", "beta");
|
|
151
127
|
const manager = makeManager("allow");
|
|
152
|
-
resolveSkillPromptEntries(input, manager, null,
|
|
128
|
+
resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
153
129
|
expect(manager.checkPermission).toHaveBeenCalledWith(
|
|
154
130
|
"skill",
|
|
155
131
|
{ name: "alpha" },
|
|
@@ -165,7 +141,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
165
141
|
test("passes agentName to permissionManager", () => {
|
|
166
142
|
const input = availableSkillsSection("librarian");
|
|
167
143
|
const manager = makeManager("allow");
|
|
168
|
-
resolveSkillPromptEntries(input, manager, "my-agent",
|
|
144
|
+
resolveSkillPromptEntries(input, manager, "my-agent", normalizer);
|
|
169
145
|
expect(manager.checkPermission).toHaveBeenCalledWith(
|
|
170
146
|
"skill",
|
|
171
147
|
{ name: "librarian" },
|
|
@@ -180,7 +156,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
180
156
|
availableSkillsSection("librarian"),
|
|
181
157
|
].join("\n");
|
|
182
158
|
const manager = makeManager("allow");
|
|
183
|
-
resolveSkillPromptEntries(input, manager, null,
|
|
159
|
+
resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
184
160
|
// Should only be called once despite appearing twice.
|
|
185
161
|
expect(manager.checkPermission).toHaveBeenCalledTimes(1);
|
|
186
162
|
});
|
|
@@ -189,13 +165,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
189
165
|
const location = "/skills/librarian/SKILL.md";
|
|
190
166
|
const input = availableSkillsSection("librarian");
|
|
191
167
|
const manager = makeManager("allow");
|
|
192
|
-
const result = resolveSkillPromptEntries(
|
|
193
|
-
input,
|
|
194
|
-
manager,
|
|
195
|
-
null,
|
|
196
|
-
CWD,
|
|
197
|
-
"linux",
|
|
198
|
-
);
|
|
168
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
199
169
|
expect(result.entries[0].normalizedLocation).toBe(location);
|
|
200
170
|
expect(result.entries[0].normalizedBaseDir).toBe("/skills/librarian");
|
|
201
171
|
});
|
|
@@ -205,13 +175,7 @@ describe("resolveSkillPromptEntries", () => {
|
|
|
205
175
|
const section2 = availableSkillsSection("beta");
|
|
206
176
|
const input = `${section1}\n${section2}`;
|
|
207
177
|
const manager = makeManager("allow", { beta: "deny" });
|
|
208
|
-
const result = resolveSkillPromptEntries(
|
|
209
|
-
input,
|
|
210
|
-
manager,
|
|
211
|
-
null,
|
|
212
|
-
CWD,
|
|
213
|
-
"linux",
|
|
214
|
-
);
|
|
178
|
+
const result = resolveSkillPromptEntries(input, manager, null, normalizer);
|
|
215
179
|
expect(result.entries.map((e) => e.name)).toContain("alpha");
|
|
216
180
|
expect(result.entries.map((e) => e.name)).not.toContain("beta");
|
|
217
181
|
});
|
|
@@ -240,12 +204,12 @@ describe("findSkillPathMatch", () => {
|
|
|
240
204
|
];
|
|
241
205
|
|
|
242
206
|
test("returns null for empty normalized path", () => {
|
|
243
|
-
expect(findSkillPathMatch("", entries,
|
|
207
|
+
expect(findSkillPathMatch("", entries, normalizer)).toBeNull();
|
|
244
208
|
});
|
|
245
209
|
|
|
246
210
|
test("returns null for empty entries array", () => {
|
|
247
211
|
expect(
|
|
248
|
-
findSkillPathMatch("/skills/librarian/SKILL.md", [],
|
|
212
|
+
findSkillPathMatch("/skills/librarian/SKILL.md", [], normalizer),
|
|
249
213
|
).toBeNull();
|
|
250
214
|
});
|
|
251
215
|
|
|
@@ -253,7 +217,7 @@ describe("findSkillPathMatch", () => {
|
|
|
253
217
|
const match = findSkillPathMatch(
|
|
254
218
|
"/skills/librarian/SKILL.md",
|
|
255
219
|
entries,
|
|
256
|
-
|
|
220
|
+
normalizer,
|
|
257
221
|
);
|
|
258
222
|
expect(match?.name).toBe("librarian");
|
|
259
223
|
});
|
|
@@ -262,13 +226,17 @@ describe("findSkillPathMatch", () => {
|
|
|
262
226
|
const match = findSkillPathMatch(
|
|
263
227
|
"/skills/librarian/extra/helper.md",
|
|
264
228
|
entries,
|
|
265
|
-
|
|
229
|
+
normalizer,
|
|
266
230
|
);
|
|
267
231
|
expect(match?.name).toBe("librarian");
|
|
268
232
|
});
|
|
269
233
|
|
|
270
234
|
test("returns null for path not within any skill directory", () => {
|
|
271
|
-
const match = findSkillPathMatch(
|
|
235
|
+
const match = findSkillPathMatch(
|
|
236
|
+
"/other/path/file.md",
|
|
237
|
+
entries,
|
|
238
|
+
normalizer,
|
|
239
|
+
);
|
|
272
240
|
expect(match).toBeNull();
|
|
273
241
|
});
|
|
274
242
|
|
|
@@ -277,7 +245,7 @@ describe("findSkillPathMatch", () => {
|
|
|
277
245
|
const match = findSkillPathMatch(
|
|
278
246
|
"/skills/librarian-extra/SKILL.md",
|
|
279
247
|
entries,
|
|
280
|
-
|
|
248
|
+
normalizer,
|
|
281
249
|
);
|
|
282
250
|
expect(match).toBeNull();
|
|
283
251
|
});
|
|
@@ -304,7 +272,7 @@ describe("findSkillPathMatch", () => {
|
|
|
304
272
|
const match = findSkillPathMatch(
|
|
305
273
|
"/skills/parent/child/helper.md",
|
|
306
274
|
nestedEntries,
|
|
307
|
-
|
|
275
|
+
normalizer,
|
|
308
276
|
);
|
|
309
277
|
expect(match?.name).toBe("child");
|
|
310
278
|
});
|
|
@@ -380,8 +348,7 @@ test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills blo
|
|
|
380
348
|
prompt,
|
|
381
349
|
asChecker(manager),
|
|
382
350
|
null,
|
|
383
|
-
"/cwd",
|
|
384
|
-
"linux",
|
|
351
|
+
new PathNormalizer("linux", "/cwd"),
|
|
385
352
|
);
|
|
386
353
|
|
|
387
354
|
expect(result.prompt).not.toContain("denied-skill");
|
|
@@ -428,20 +395,19 @@ test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available
|
|
|
428
395
|
prompt,
|
|
429
396
|
asChecker(manager),
|
|
430
397
|
null,
|
|
431
|
-
"/cwd",
|
|
432
|
-
"linux",
|
|
398
|
+
new PathNormalizer("linux", "/cwd"),
|
|
433
399
|
);
|
|
434
400
|
const visiblePath = resolve("/cwd", "./skills/visible/file.ts");
|
|
435
401
|
const blockedPath = resolve("/cwd", "./skills/blocked/file.ts");
|
|
436
402
|
const matchedVisibleSkill = findSkillPathMatch(
|
|
437
403
|
process.platform === "win32" ? visiblePath.toLowerCase() : visiblePath,
|
|
438
404
|
result.entries,
|
|
439
|
-
|
|
405
|
+
normalizer,
|
|
440
406
|
);
|
|
441
407
|
const matchedBlockedSkill = findSkillPathMatch(
|
|
442
408
|
process.platform === "win32" ? blockedPath.toLowerCase() : blockedPath,
|
|
443
409
|
result.entries,
|
|
444
|
-
|
|
410
|
+
normalizer,
|
|
445
411
|
);
|
|
446
412
|
|
|
447
413
|
expect(matchedVisibleSkill?.name).toBe("visible-skill");
|