@gotgenes/pi-permission-system 10.5.0 → 10.5.2
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/handlers/before-agent-start.ts +11 -6
- package/src/handlers/lifecycle.ts +7 -4
- package/src/handlers/permission-gate-handler.ts +3 -3
- package/src/index.ts +8 -3
- package/src/input-normalizer.ts +20 -8
- package/src/path-utils.ts +1 -10
- package/src/permission-resolver.ts +0 -3
- package/src/permission-session.ts +8 -52
- package/src/session-rules.ts +3 -2
- package/src/skill-prompt-sanitizer.ts +1 -1
- package/test/before-agent-start-cache.test.ts +89 -0
- package/test/handlers/before-agent-start.test.ts +56 -86
- package/test/handlers/external-directory-session-dedup.test.ts +175 -159
- package/test/handlers/gates/bash-path.test.ts +57 -0
- package/test/handlers/gates/path.test.ts +58 -0
- package/test/handlers/input.test.ts +5 -4
- package/test/handlers/lifecycle.test.ts +79 -85
- package/test/handlers/tool-call.test.ts +106 -2
- package/test/helpers/handler-fixtures.ts +99 -102
- package/test/helpers/manager-harness.ts +61 -0
- package/test/helpers/session-fixtures.ts +192 -0
- package/test/input-normalizer.test.ts +77 -1
- package/test/logging.test.ts +51 -0
- package/test/path-utils.test.ts +10 -0
- package/test/permission-forwarding.test.ts +73 -0
- package/test/permission-manager-unified.test.ts +1577 -3
- package/test/permission-resolver.test.ts +3 -1
- package/test/permission-session.test.ts +14 -198
- package/test/session-rules.test.ts +13 -5
- package/test/skill-prompt-sanitizer.test.ts +130 -0
- package/test/status.test.ts +10 -0
- package/test/system-prompt-sanitizer.test.ts +68 -0
- package/test/tool-registry.test.ts +42 -0
- package/test/yolo-mode.test.ts +78 -0
- package/src/agent-prep-session.ts +0 -28
- package/src/gate-handler-session.ts +0 -13
- package/src/session-lifecycle-session.ts +0 -24
- package/test/permission-system.test.ts +0 -2785
|
@@ -82,7 +82,9 @@ describe("PermissionResolver", () => {
|
|
|
82
82
|
const { resolver } = makeResolver(pm, sessionRules);
|
|
83
83
|
|
|
84
84
|
// Record an approval directly into the shared SessionRules instance.
|
|
85
|
-
sessionRules.
|
|
85
|
+
sessionRules.recordSessionApproval(
|
|
86
|
+
SessionApproval.single("bash", "git *"),
|
|
87
|
+
);
|
|
86
88
|
resolver.resolve("bash", { command: "git status" });
|
|
87
89
|
|
|
88
90
|
const passedRules = vi.mocked(pm.checkPermission).mock.calls[0][3];
|
|
@@ -17,20 +17,19 @@ vi.mock("../src/active-agent", () => ({
|
|
|
17
17
|
|
|
18
18
|
// ── Test helpers ───────────────────────────────────────────────────────────
|
|
19
19
|
|
|
20
|
-
import type {
|
|
21
|
-
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
22
|
-
import type { ExtensionPaths } from "#src/extension-paths";
|
|
23
|
-
import type { ForwardingController } from "#src/forwarding-manager";
|
|
24
|
-
import type { ScopedPermissionManager } from "#src/permission-manager";
|
|
25
|
-
import { PermissionSession } from "#src/permission-session";
|
|
26
|
-
import type { PromptingGatewayLifecycle } from "#src/prompting-gateway";
|
|
27
|
-
import type { Ruleset } from "#src/rule";
|
|
20
|
+
import type { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
28
21
|
import { SessionApproval } from "#src/session-approval";
|
|
29
|
-
import type { SessionLogger } from "#src/session-logger";
|
|
30
|
-
import { SessionRules } from "#src/session-rules";
|
|
31
22
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
32
|
-
import type { PermissionCheckResult, PermissionState } from "#src/types";
|
|
33
23
|
import { makeCtx } from "#test/helpers/handler-fixtures";
|
|
24
|
+
import {
|
|
25
|
+
makeConfigStore,
|
|
26
|
+
makeFakePermissionManager,
|
|
27
|
+
makeRealSession,
|
|
28
|
+
} from "#test/helpers/session-fixtures";
|
|
29
|
+
|
|
30
|
+
// Alias so the existing tests read naturally.
|
|
31
|
+
const createSession = makeRealSession;
|
|
32
|
+
const makePermissionManager = makeFakePermissionManager;
|
|
34
33
|
|
|
35
34
|
function makeSkillEntry(
|
|
36
35
|
name: string,
|
|
@@ -47,125 +46,6 @@ function makeSkillEntry(
|
|
|
47
46
|
};
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
function makePaths(overrides: Partial<ExtensionPaths> = {}): ExtensionPaths {
|
|
51
|
-
return {
|
|
52
|
-
agentDir: "/test/agent",
|
|
53
|
-
sessionsDir: "/test/agent/sessions",
|
|
54
|
-
subagentSessionsDir: "/test/agent/subagent-sessions",
|
|
55
|
-
forwardingDir: "/test/agent/sessions/permission-forwarding",
|
|
56
|
-
globalLogsDir: "/test/agent/logs",
|
|
57
|
-
piInfrastructureDirs: ["/test/agent", "/test/agent/git"],
|
|
58
|
-
...overrides,
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function makeLogger(): SessionLogger {
|
|
63
|
-
return {
|
|
64
|
-
debug: vi.fn(),
|
|
65
|
-
review: vi.fn(),
|
|
66
|
-
warn: vi.fn(),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function makeConfigStore(
|
|
71
|
-
overrides: Partial<SessionConfigStore> = {},
|
|
72
|
-
): SessionConfigStore {
|
|
73
|
-
return {
|
|
74
|
-
current:
|
|
75
|
-
overrides.current ??
|
|
76
|
-
vi
|
|
77
|
-
.fn<() => typeof DEFAULT_EXTENSION_CONFIG>()
|
|
78
|
-
.mockReturnValue({ ...DEFAULT_EXTENSION_CONFIG }),
|
|
79
|
-
refresh: overrides.refresh ?? vi.fn<(ctx?: ExtensionContext) => void>(),
|
|
80
|
-
logResolvedPaths: overrides.logResolvedPaths ?? vi.fn<() => void>(),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function makeGateway(): PromptingGatewayLifecycle {
|
|
85
|
-
return {
|
|
86
|
-
activate: vi.fn<PromptingGatewayLifecycle["activate"]>(),
|
|
87
|
-
deactivate: vi.fn<PromptingGatewayLifecycle["deactivate"]>(),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function makeForwarding(): ForwardingController {
|
|
92
|
-
return {
|
|
93
|
-
start: vi.fn(),
|
|
94
|
-
stop: vi.fn(),
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function makePermissionManager() {
|
|
99
|
-
return {
|
|
100
|
-
configureForCwd: vi.fn<(cwd: string | undefined | null) => void>(),
|
|
101
|
-
checkPermission: vi
|
|
102
|
-
.fn<
|
|
103
|
-
(
|
|
104
|
-
toolName: string,
|
|
105
|
-
input: unknown,
|
|
106
|
-
agentName?: string,
|
|
107
|
-
sessionRules?: Ruleset,
|
|
108
|
-
) => PermissionCheckResult
|
|
109
|
-
>()
|
|
110
|
-
.mockReturnValue({
|
|
111
|
-
state: "allow",
|
|
112
|
-
toolName: "read",
|
|
113
|
-
source: "tool",
|
|
114
|
-
origin: "builtin",
|
|
115
|
-
}),
|
|
116
|
-
getToolPermission: vi
|
|
117
|
-
.fn<(toolName: string, agentName?: string) => PermissionState>()
|
|
118
|
-
.mockReturnValue("allow"),
|
|
119
|
-
getConfigIssues: vi.fn((): string[] => []),
|
|
120
|
-
getPolicyCacheStamp: vi.fn((): string => "stamp-1"),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function createSession(overrides?: {
|
|
125
|
-
paths?: Partial<ExtensionPaths>;
|
|
126
|
-
logger?: SessionLogger;
|
|
127
|
-
forwarding?: ForwardingController;
|
|
128
|
-
permissionManager?: ScopedPermissionManager;
|
|
129
|
-
sessionRules?: SessionRules;
|
|
130
|
-
configStore?: SessionConfigStore;
|
|
131
|
-
gateway?: PromptingGatewayLifecycle;
|
|
132
|
-
}): {
|
|
133
|
-
session: PermissionSession;
|
|
134
|
-
paths: ExtensionPaths;
|
|
135
|
-
logger: SessionLogger;
|
|
136
|
-
forwarding: ForwardingController;
|
|
137
|
-
sessionRules: SessionRules;
|
|
138
|
-
configStore: SessionConfigStore;
|
|
139
|
-
gateway: PromptingGatewayLifecycle;
|
|
140
|
-
} {
|
|
141
|
-
const paths = makePaths(overrides?.paths);
|
|
142
|
-
const logger = overrides?.logger ?? makeLogger();
|
|
143
|
-
const forwarding = overrides?.forwarding ?? makeForwarding();
|
|
144
|
-
const permissionManager =
|
|
145
|
-
overrides?.permissionManager ?? makePermissionManager();
|
|
146
|
-
const sessionRules = overrides?.sessionRules ?? new SessionRules();
|
|
147
|
-
const configStore = overrides?.configStore ?? makeConfigStore();
|
|
148
|
-
const gateway = overrides?.gateway ?? makeGateway();
|
|
149
|
-
const session = new PermissionSession(
|
|
150
|
-
paths,
|
|
151
|
-
logger,
|
|
152
|
-
forwarding,
|
|
153
|
-
permissionManager,
|
|
154
|
-
sessionRules,
|
|
155
|
-
configStore,
|
|
156
|
-
gateway,
|
|
157
|
-
);
|
|
158
|
-
return {
|
|
159
|
-
session,
|
|
160
|
-
paths,
|
|
161
|
-
logger,
|
|
162
|
-
forwarding,
|
|
163
|
-
sessionRules,
|
|
164
|
-
configStore,
|
|
165
|
-
gateway,
|
|
166
|
-
};
|
|
167
|
-
}
|
|
168
|
-
|
|
169
49
|
// ── Tests ──────────────────────────────────────────────────────────────────
|
|
170
50
|
|
|
171
51
|
beforeEach(() => {
|
|
@@ -176,70 +56,6 @@ beforeEach(() => {
|
|
|
176
56
|
});
|
|
177
57
|
|
|
178
58
|
describe("PermissionSession", () => {
|
|
179
|
-
describe("constructor and delegation", () => {
|
|
180
|
-
it("delegates checkPermission to internal PermissionManager", () => {
|
|
181
|
-
const pm = makePermissionManager();
|
|
182
|
-
const { session } = createSession({ permissionManager: pm });
|
|
183
|
-
|
|
184
|
-
const result = session.checkPermission("bash", { command: "ls" });
|
|
185
|
-
|
|
186
|
-
expect(pm.checkPermission).toHaveBeenCalledWith(
|
|
187
|
-
"bash",
|
|
188
|
-
{ command: "ls" },
|
|
189
|
-
undefined,
|
|
190
|
-
undefined,
|
|
191
|
-
);
|
|
192
|
-
expect(result.state).toBe("allow");
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it("delegates getToolPermission to internal PermissionManager", () => {
|
|
196
|
-
const pm = makePermissionManager();
|
|
197
|
-
const { session } = createSession({ permissionManager: pm });
|
|
198
|
-
|
|
199
|
-
const result = session.getToolPermission("read");
|
|
200
|
-
|
|
201
|
-
expect(pm.getToolPermission).toHaveBeenCalledWith("read", undefined);
|
|
202
|
-
expect(result).toBe("allow");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it("delegates getConfigIssues to internal PermissionManager", () => {
|
|
206
|
-
const pm = makePermissionManager();
|
|
207
|
-
vi.mocked(pm.getConfigIssues).mockReturnValue(["issue1"]);
|
|
208
|
-
const { session } = createSession({ permissionManager: pm });
|
|
209
|
-
|
|
210
|
-
expect(session.getConfigIssues("agent1")).toEqual(["issue1"]);
|
|
211
|
-
expect(pm.getConfigIssues).toHaveBeenCalledWith("agent1");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("delegates getPolicyCacheStamp to internal PermissionManager", () => {
|
|
215
|
-
const pm = makePermissionManager();
|
|
216
|
-
const { session } = createSession({ permissionManager: pm });
|
|
217
|
-
|
|
218
|
-
expect(session.getPolicyCacheStamp("agent1")).toBe("stamp-1");
|
|
219
|
-
expect(pm.getPolicyCacheStamp).toHaveBeenCalledWith("agent1");
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("delegates getSessionRuleset to internal SessionRules", () => {
|
|
223
|
-
const { session } = createSession();
|
|
224
|
-
const rules = session.getSessionRuleset();
|
|
225
|
-
expect(rules).toEqual([]);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it("delegates recordSessionApproval to internal SessionRules", () => {
|
|
229
|
-
const { session } = createSession();
|
|
230
|
-
session.recordSessionApproval(
|
|
231
|
-
SessionApproval.single("bash", "/usr/bin/*"),
|
|
232
|
-
);
|
|
233
|
-
const rules = session.getSessionRuleset();
|
|
234
|
-
expect(rules).toHaveLength(1);
|
|
235
|
-
expect(rules[0]).toMatchObject({
|
|
236
|
-
surface: "bash",
|
|
237
|
-
pattern: "/usr/bin/*",
|
|
238
|
-
action: "allow",
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
59
|
describe("activate and deactivate", () => {
|
|
244
60
|
it("stores the context on activate", () => {
|
|
245
61
|
const { session, forwarding } = createSession();
|
|
@@ -335,13 +151,13 @@ describe("PermissionSession", () => {
|
|
|
335
151
|
|
|
336
152
|
describe("shutdown", () => {
|
|
337
153
|
it("clears session rules", () => {
|
|
338
|
-
const { session } = createSession();
|
|
339
|
-
|
|
340
|
-
expect(
|
|
154
|
+
const { session, sessionRules } = createSession();
|
|
155
|
+
sessionRules.recordSessionApproval(SessionApproval.single("bash", "*"));
|
|
156
|
+
expect(sessionRules.getRuleset()).toHaveLength(1);
|
|
341
157
|
|
|
342
158
|
session.shutdown();
|
|
343
159
|
|
|
344
|
-
expect(
|
|
160
|
+
expect(sessionRules.getRuleset()).toEqual([]);
|
|
345
161
|
});
|
|
346
162
|
|
|
347
163
|
it("clears cache keys", () => {
|
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import { evaluate } from "#src/rule";
|
|
4
4
|
import { SessionApproval } from "#src/session-approval";
|
|
5
|
+
import type { SessionApprovalRecorder } from "#src/session-approval-recorder";
|
|
5
6
|
import { deriveApprovalPattern, SessionRules } from "#src/session-rules";
|
|
6
7
|
|
|
7
8
|
// ── SessionRules ───────────────────────────────────────────────────────────
|
|
@@ -67,10 +68,15 @@ describe("SessionRules", () => {
|
|
|
67
68
|
});
|
|
68
69
|
});
|
|
69
70
|
|
|
70
|
-
describe("
|
|
71
|
+
describe("recordSessionApproval", () => {
|
|
72
|
+
it("satisfies the SessionApprovalRecorder interface", () => {
|
|
73
|
+
const rules: SessionApprovalRecorder = new SessionRules();
|
|
74
|
+
expect(typeof rules.recordSessionApproval).toBe("function");
|
|
75
|
+
});
|
|
76
|
+
|
|
71
77
|
it("records a single-pattern approval as one rule", () => {
|
|
72
78
|
const rules = new SessionRules();
|
|
73
|
-
rules.
|
|
79
|
+
rules.recordSessionApproval(SessionApproval.single("bash", "git *"));
|
|
74
80
|
expect(rules.getRuleset()).toEqual([
|
|
75
81
|
{
|
|
76
82
|
surface: "bash",
|
|
@@ -84,7 +90,7 @@ describe("SessionRules", () => {
|
|
|
84
90
|
|
|
85
91
|
it("records a multi-pattern approval as one rule per pattern", () => {
|
|
86
92
|
const rules = new SessionRules();
|
|
87
|
-
rules.
|
|
93
|
+
rules.recordSessionApproval(
|
|
88
94
|
SessionApproval.multiple("external_directory", [
|
|
89
95
|
"/outside/a/*",
|
|
90
96
|
"/outside/b/*",
|
|
@@ -97,7 +103,7 @@ describe("SessionRules", () => {
|
|
|
97
103
|
|
|
98
104
|
it("records each rule with the correct surface", () => {
|
|
99
105
|
const rules = new SessionRules();
|
|
100
|
-
rules.
|
|
106
|
+
rules.recordSessionApproval(
|
|
101
107
|
SessionApproval.multiple("external_directory", [
|
|
102
108
|
"/outside/a/*",
|
|
103
109
|
"/outside/b/*",
|
|
@@ -110,7 +116,9 @@ describe("SessionRules", () => {
|
|
|
110
116
|
|
|
111
117
|
it("records nothing for an empty patterns list", () => {
|
|
112
118
|
const rules = new SessionRules();
|
|
113
|
-
rules.
|
|
119
|
+
rules.recordSessionApproval(
|
|
120
|
+
SessionApproval.multiple("external_directory", []),
|
|
121
|
+
);
|
|
114
122
|
expect(rules.getRuleset()).toEqual([]);
|
|
115
123
|
});
|
|
116
124
|
});
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
3
|
import type { PermissionManager } from "#src/permission-manager";
|
|
3
4
|
import {
|
|
4
5
|
findSkillPathMatch,
|
|
6
|
+
parseAllSkillPromptSections,
|
|
5
7
|
resolveSkillPromptEntries,
|
|
6
8
|
} from "#src/skill-prompt-sanitizer";
|
|
7
9
|
import type { PermissionCheckResult } from "#src/types";
|
|
10
|
+
import { createManager } from "#test/helpers/manager-harness";
|
|
8
11
|
|
|
9
12
|
afterEach(() => {
|
|
10
13
|
vi.restoreAllMocks();
|
|
@@ -242,3 +245,130 @@ describe("findSkillPathMatch", () => {
|
|
|
242
245
|
expect(match?.name).toBe("child");
|
|
243
246
|
});
|
|
244
247
|
});
|
|
248
|
+
|
|
249
|
+
// ---------------------------------------------------------------------------
|
|
250
|
+
// Moved from permission-system.test.ts catch-all (#342)
|
|
251
|
+
// ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
test("parseAllSkillPromptSections finds every available_skills block", () => {
|
|
254
|
+
const prompt = [
|
|
255
|
+
"Some preamble",
|
|
256
|
+
"<available_skills>",
|
|
257
|
+
" <skill>",
|
|
258
|
+
" <name>skill-one</name>",
|
|
259
|
+
" <description>First skill</description>",
|
|
260
|
+
" <location>/path/to/one</location>",
|
|
261
|
+
" </skill>",
|
|
262
|
+
"</available_skills>",
|
|
263
|
+
"Some content between",
|
|
264
|
+
"<available_skills>",
|
|
265
|
+
" <skill>",
|
|
266
|
+
" <name>skill-two</name>",
|
|
267
|
+
" <description>Second skill</description>",
|
|
268
|
+
" <location>/path/to/two</location>",
|
|
269
|
+
" </skill>",
|
|
270
|
+
"</available_skills>",
|
|
271
|
+
"Footer",
|
|
272
|
+
].join("\n");
|
|
273
|
+
|
|
274
|
+
const sections = parseAllSkillPromptSections(prompt);
|
|
275
|
+
|
|
276
|
+
expect(sections.length).toBe(2);
|
|
277
|
+
expect(sections[0].entries[0]?.name).toBe("skill-one");
|
|
278
|
+
expect(sections[1].entries[0]?.name).toBe("skill-two");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("REGRESSION: resolveSkillPromptEntries sanitizes every available_skills block", () => {
|
|
282
|
+
const { manager, cleanup } = createManager({
|
|
283
|
+
permission: {
|
|
284
|
+
"*": "ask",
|
|
285
|
+
skill: { "denied-skill": "deny" },
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const prompt = [
|
|
291
|
+
"System prompt start",
|
|
292
|
+
"<available_skills>",
|
|
293
|
+
" <skill>",
|
|
294
|
+
" <name>visible-skill</name>",
|
|
295
|
+
" <description>Allowed skill</description>",
|
|
296
|
+
" <location>/skills/visible/index.ts</location>",
|
|
297
|
+
" </skill>",
|
|
298
|
+
" <skill>",
|
|
299
|
+
" <name>denied-skill</name>",
|
|
300
|
+
" <description>Denied in first block</description>",
|
|
301
|
+
" <location>/skills/blocked/one.ts</location>",
|
|
302
|
+
" </skill>",
|
|
303
|
+
"</available_skills>",
|
|
304
|
+
"Agent identity section",
|
|
305
|
+
"<available_skills>",
|
|
306
|
+
" <skill>",
|
|
307
|
+
" <name>denied-skill</name>",
|
|
308
|
+
" <description>Denied in second block</description>",
|
|
309
|
+
" <location>/skills/blocked/two.ts</location>",
|
|
310
|
+
" </skill>",
|
|
311
|
+
"</available_skills>",
|
|
312
|
+
"System prompt end",
|
|
313
|
+
].join("\n");
|
|
314
|
+
|
|
315
|
+
const result = resolveSkillPromptEntries(prompt, manager, null, "/cwd");
|
|
316
|
+
|
|
317
|
+
expect(result.prompt).not.toContain("denied-skill");
|
|
318
|
+
expect(result.prompt).toContain("visible-skill");
|
|
319
|
+
expect((result.prompt.match(/<available_skills>/g) ?? []).length).toBe(1);
|
|
320
|
+
expect(result.entries.map((entry) => entry.name)).toEqual([
|
|
321
|
+
"visible-skill",
|
|
322
|
+
]);
|
|
323
|
+
} finally {
|
|
324
|
+
cleanup();
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("REGRESSION: resolveSkillPromptEntries keeps only visible skills available for path matching", () => {
|
|
329
|
+
const { manager, cleanup } = createManager({
|
|
330
|
+
permission: {
|
|
331
|
+
"*": "ask",
|
|
332
|
+
skill: { "blocked-skill": "deny" },
|
|
333
|
+
},
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
const prompt = [
|
|
338
|
+
"System prompt start",
|
|
339
|
+
"<available_skills>",
|
|
340
|
+
" <skill>",
|
|
341
|
+
" <name>blocked-skill</name>",
|
|
342
|
+
" <description>Blocked skill</description>",
|
|
343
|
+
" <location>@./skills/blocked/entry.ts</location>",
|
|
344
|
+
" </skill>",
|
|
345
|
+
"</available_skills>",
|
|
346
|
+
"Middle section",
|
|
347
|
+
"<available_skills>",
|
|
348
|
+
" <skill>",
|
|
349
|
+
" <name>visible-skill</name>",
|
|
350
|
+
" <description>Visible skill</description>",
|
|
351
|
+
" <location>@./skills/visible/entry.ts</location>",
|
|
352
|
+
" </skill>",
|
|
353
|
+
"</available_skills>",
|
|
354
|
+
"System prompt end",
|
|
355
|
+
].join("\n");
|
|
356
|
+
|
|
357
|
+
const result = resolveSkillPromptEntries(prompt, manager, null, "/cwd");
|
|
358
|
+
const visiblePath = resolve("/cwd", "./skills/visible/file.ts");
|
|
359
|
+
const blockedPath = resolve("/cwd", "./skills/blocked/file.ts");
|
|
360
|
+
const matchedVisibleSkill = findSkillPathMatch(
|
|
361
|
+
process.platform === "win32" ? visiblePath.toLowerCase() : visiblePath,
|
|
362
|
+
result.entries,
|
|
363
|
+
);
|
|
364
|
+
const matchedBlockedSkill = findSkillPathMatch(
|
|
365
|
+
process.platform === "win32" ? blockedPath.toLowerCase() : blockedPath,
|
|
366
|
+
result.entries,
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
expect(matchedVisibleSkill?.name).toBe("visible-skill");
|
|
370
|
+
expect(matchedBlockedSkill).toBe(null);
|
|
371
|
+
} finally {
|
|
372
|
+
cleanup();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
3
|
+
import { getPermissionSystemStatus } from "#src/status";
|
|
4
|
+
|
|
5
|
+
test("Permission-system status is only exposed when yolo mode is enabled", () => {
|
|
6
|
+
expect(getPermissionSystemStatus(DEFAULT_EXTENSION_CONFIG)).toBe(undefined);
|
|
7
|
+
expect(
|
|
8
|
+
getPermissionSystemStatus({ ...DEFAULT_EXTENSION_CONFIG, yoloMode: true }),
|
|
9
|
+
).toBe("yolo");
|
|
10
|
+
});
|
|
@@ -225,3 +225,71 @@ describe("sanitizeAvailableToolsSection — findSection boundary edge cases", ()
|
|
|
225
225
|
expect(result.prompt).not.toContain("Available tools:");
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Moved from permission-system.test.ts catch-all (#342)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
test("System prompt sanitizer removes the Available tools section and surrounding boilerplate", () => {
|
|
234
|
+
const prompt = [
|
|
235
|
+
"Available tools:",
|
|
236
|
+
"- read: Read file contents",
|
|
237
|
+
"- mcp: Discover, inspect, and call MCP tools across configured servers",
|
|
238
|
+
"",
|
|
239
|
+
"In addition to the tools above, you may have access to other custom tools depending on the project.",
|
|
240
|
+
"",
|
|
241
|
+
"Guidelines:",
|
|
242
|
+
"- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
|
|
243
|
+
"- Be concise in your responses",
|
|
244
|
+
].join("\n");
|
|
245
|
+
|
|
246
|
+
const result = sanitizeAvailableToolsSection(prompt, ["read", "mcp"]);
|
|
247
|
+
|
|
248
|
+
expect(result.removed).toBe(true);
|
|
249
|
+
expect(result.prompt).not.toContain("Available tools:");
|
|
250
|
+
expect(result.prompt).not.toContain("In addition to the tools above");
|
|
251
|
+
expect(result.prompt).toMatch(/Guidelines:/);
|
|
252
|
+
expect(result.prompt).toMatch(/Use mcp for MCP discovery first/i);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("System prompt sanitizer removes denied tool guidelines while keeping global guidance", () => {
|
|
256
|
+
const prompt = [
|
|
257
|
+
"Guidelines:",
|
|
258
|
+
"- Use task when work SHOULD be delegated to one or more specialized agents instead of handled entirely in the current session.",
|
|
259
|
+
"- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
|
|
260
|
+
"- Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)",
|
|
261
|
+
"- Be concise in your responses",
|
|
262
|
+
"- Show file paths clearly when working with files",
|
|
263
|
+
].join("\n");
|
|
264
|
+
|
|
265
|
+
const result = sanitizeAvailableToolsSection(prompt, ["bash", "grep", "mcp"]);
|
|
266
|
+
|
|
267
|
+
expect(result.removed).toBe(true);
|
|
268
|
+
expect(result.prompt).not.toContain("Use task when work SHOULD");
|
|
269
|
+
expect(result.prompt).toMatch(/Use mcp for MCP discovery first/i);
|
|
270
|
+
expect(result.prompt).toMatch(/Prefer grep\/find\/ls tools over bash/i);
|
|
271
|
+
expect(result.prompt).toMatch(/Be concise in your responses/);
|
|
272
|
+
expect(result.prompt).toMatch(
|
|
273
|
+
/Show file paths clearly when working with files/,
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("System prompt sanitizer removes inactive built-in write guidance", () => {
|
|
278
|
+
const prompt = [
|
|
279
|
+
"Guidelines:",
|
|
280
|
+
"- Use write only for new files or complete rewrites",
|
|
281
|
+
"- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
|
|
282
|
+
"- Be concise in your responses",
|
|
283
|
+
].join("\n");
|
|
284
|
+
|
|
285
|
+
const result = sanitizeAvailableToolsSection(prompt, ["read"]);
|
|
286
|
+
|
|
287
|
+
expect(result.removed).toBe(true);
|
|
288
|
+
expect(result.prompt).not.toContain(
|
|
289
|
+
"Use write only for new files or complete rewrites",
|
|
290
|
+
);
|
|
291
|
+
expect(result.prompt).not.toContain(
|
|
292
|
+
"do NOT use cat or bash to display what you did",
|
|
293
|
+
);
|
|
294
|
+
expect(result.prompt).toMatch(/Be concise in your responses/);
|
|
295
|
+
});
|
|
@@ -153,3 +153,45 @@ describe("checkRequestedToolRegistration", () => {
|
|
|
153
153
|
expect(result.status).toBe("registered");
|
|
154
154
|
});
|
|
155
155
|
});
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Moved from permission-system.test.ts catch-all (#342)
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
test("Tool registry resolves event tool names from string and object payloads", () => {
|
|
162
|
+
expect(getToolNameFromValue(" read ")).toBe("read");
|
|
163
|
+
expect(getToolNameFromValue({ toolName: "write" })).toBe("write");
|
|
164
|
+
expect(getToolNameFromValue({ name: "find" })).toBe("find");
|
|
165
|
+
expect(getToolNameFromValue({ tool: "grep" })).toBe("grep");
|
|
166
|
+
expect(getToolNameFromValue({})).toBe(null);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("Tool registry blocks unregistered tools and handles aliases", () => {
|
|
170
|
+
const registeredTools = [
|
|
171
|
+
{ toolName: "mcp" },
|
|
172
|
+
{ toolName: "read" },
|
|
173
|
+
{ toolName: "bash" },
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
const unknownCheck = checkRequestedToolRegistration(
|
|
177
|
+
"third_party_tool",
|
|
178
|
+
registeredTools,
|
|
179
|
+
);
|
|
180
|
+
expect(unknownCheck.status).toBe("unregistered");
|
|
181
|
+
if (unknownCheck.status === "unregistered") {
|
|
182
|
+
expect(unknownCheck.availableToolNames).toEqual(["bash", "mcp", "read"]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const aliasCheck = checkRequestedToolRegistration(
|
|
186
|
+
"legacy_read",
|
|
187
|
+
registeredTools,
|
|
188
|
+
{ legacy_read: "read" },
|
|
189
|
+
);
|
|
190
|
+
expect(aliasCheck.status).toBe("registered");
|
|
191
|
+
|
|
192
|
+
const missingNameCheck = checkRequestedToolRegistration(
|
|
193
|
+
" ",
|
|
194
|
+
registeredTools,
|
|
195
|
+
);
|
|
196
|
+
expect(missingNameCheck.status).toBe("missing-tool-name");
|
|
197
|
+
});
|
package/test/yolo-mode.test.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
2
2
|
import type { PermissionSystemExtensionConfig } from "#src/extension-config";
|
|
3
|
+
import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
|
|
4
|
+
import { resolvePermissionForwardingTargetSessionId } from "#src/permission-forwarding";
|
|
3
5
|
import {
|
|
4
6
|
canResolveAskPermissionRequest,
|
|
5
7
|
shouldAutoApprovePermissionState,
|
|
@@ -108,3 +110,79 @@ describe("canResolveAskPermissionRequest", () => {
|
|
|
108
110
|
).toBe(true);
|
|
109
111
|
});
|
|
110
112
|
});
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Moved from permission-system.test.ts catch-all (#342)
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
test("Yolo mode only auto-approves ask-state permissions", () => {
|
|
119
|
+
expect(
|
|
120
|
+
shouldAutoApprovePermissionState("ask", DEFAULT_EXTENSION_CONFIG),
|
|
121
|
+
).toBe(false);
|
|
122
|
+
expect(
|
|
123
|
+
shouldAutoApprovePermissionState("ask", {
|
|
124
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
125
|
+
yoloMode: true,
|
|
126
|
+
}),
|
|
127
|
+
).toBe(true);
|
|
128
|
+
expect(
|
|
129
|
+
shouldAutoApprovePermissionState("deny", {
|
|
130
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
131
|
+
yoloMode: true,
|
|
132
|
+
}),
|
|
133
|
+
).toBe(false);
|
|
134
|
+
expect(
|
|
135
|
+
shouldAutoApprovePermissionState("allow", {
|
|
136
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
137
|
+
yoloMode: true,
|
|
138
|
+
}),
|
|
139
|
+
).toBe(false);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("Yolo mode resolves ask permissions without UI or delegation forwarding", () => {
|
|
143
|
+
expect(
|
|
144
|
+
canResolveAskPermissionRequest({
|
|
145
|
+
config: DEFAULT_EXTENSION_CONFIG,
|
|
146
|
+
hasUI: false,
|
|
147
|
+
isSubagent: false,
|
|
148
|
+
}),
|
|
149
|
+
).toBe(false);
|
|
150
|
+
expect(
|
|
151
|
+
canResolveAskPermissionRequest({
|
|
152
|
+
config: { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true },
|
|
153
|
+
hasUI: false,
|
|
154
|
+
isSubagent: false,
|
|
155
|
+
}),
|
|
156
|
+
).toBe(true);
|
|
157
|
+
expect(
|
|
158
|
+
canResolveAskPermissionRequest({
|
|
159
|
+
config: DEFAULT_EXTENSION_CONFIG,
|
|
160
|
+
hasUI: false,
|
|
161
|
+
isSubagent: true,
|
|
162
|
+
}),
|
|
163
|
+
).toBe(true);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("Yolo mode bypasses delegated ask routing when no parent forwarding target is available", () => {
|
|
167
|
+
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
168
|
+
hasUI: false,
|
|
169
|
+
isSubagent: true,
|
|
170
|
+
currentSessionId: "child-session",
|
|
171
|
+
env: {},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(targetSessionId).toBe(null);
|
|
175
|
+
expect(
|
|
176
|
+
canResolveAskPermissionRequest({
|
|
177
|
+
config: { ...DEFAULT_EXTENSION_CONFIG, yoloMode: true },
|
|
178
|
+
hasUI: false,
|
|
179
|
+
isSubagent: true,
|
|
180
|
+
}),
|
|
181
|
+
).toBe(true);
|
|
182
|
+
expect(
|
|
183
|
+
shouldAutoApprovePermissionState("ask", {
|
|
184
|
+
...DEFAULT_EXTENSION_CONFIG,
|
|
185
|
+
yoloMode: true,
|
|
186
|
+
}),
|
|
187
|
+
).toBe(true);
|
|
188
|
+
});
|