@gotgenes/pi-permission-system 10.5.1 → 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 +13 -0
- package/package.json +1 -1
- package/src/input-normalizer.ts +20 -8
- package/src/path-utils.ts +1 -10
- package/test/before-agent-start-cache.test.ts +89 -0
- package/test/handlers/external-directory-session-dedup.test.ts +96 -0
- package/test/handlers/gates/bash-path.test.ts +57 -0
- package/test/handlers/gates/path.test.ts +58 -0
- package/test/handlers/tool-call.test.ts +103 -0
- package/test/helpers/manager-harness.ts +61 -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/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/test/permission-system.test.ts +0 -2785
|
@@ -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
|
+
});
|