@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/package.json +1 -1
  3. package/src/handlers/before-agent-start.ts +11 -6
  4. package/src/handlers/lifecycle.ts +7 -4
  5. package/src/handlers/permission-gate-handler.ts +3 -3
  6. package/src/index.ts +8 -3
  7. package/src/input-normalizer.ts +20 -8
  8. package/src/path-utils.ts +1 -10
  9. package/src/permission-resolver.ts +0 -3
  10. package/src/permission-session.ts +8 -52
  11. package/src/session-rules.ts +3 -2
  12. package/src/skill-prompt-sanitizer.ts +1 -1
  13. package/test/before-agent-start-cache.test.ts +89 -0
  14. package/test/handlers/before-agent-start.test.ts +56 -86
  15. package/test/handlers/external-directory-session-dedup.test.ts +175 -159
  16. package/test/handlers/gates/bash-path.test.ts +57 -0
  17. package/test/handlers/gates/path.test.ts +58 -0
  18. package/test/handlers/input.test.ts +5 -4
  19. package/test/handlers/lifecycle.test.ts +79 -85
  20. package/test/handlers/tool-call.test.ts +106 -2
  21. package/test/helpers/handler-fixtures.ts +99 -102
  22. package/test/helpers/manager-harness.ts +61 -0
  23. package/test/helpers/session-fixtures.ts +192 -0
  24. package/test/input-normalizer.test.ts +77 -1
  25. package/test/logging.test.ts +51 -0
  26. package/test/path-utils.test.ts +10 -0
  27. package/test/permission-forwarding.test.ts +73 -0
  28. package/test/permission-manager-unified.test.ts +1577 -3
  29. package/test/permission-resolver.test.ts +3 -1
  30. package/test/permission-session.test.ts +14 -198
  31. package/test/session-rules.test.ts +13 -5
  32. package/test/skill-prompt-sanitizer.test.ts +130 -0
  33. package/test/status.test.ts +10 -0
  34. package/test/system-prompt-sanitizer.test.ts +68 -0
  35. package/test/tool-registry.test.ts +42 -0
  36. package/test/yolo-mode.test.ts +78 -0
  37. package/src/agent-prep-session.ts +0 -28
  38. package/src/gate-handler-session.ts +0 -13
  39. package/src/session-lifecycle-session.ts +0 -24
  40. 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.record(SessionApproval.single("bash", "git *"));
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 { SessionConfigStore } from "#src/config-store";
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
- session.recordSessionApproval(SessionApproval.single("bash", "*"));
340
- expect(session.getSessionRuleset()).toHaveLength(1);
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(session.getSessionRuleset()).toEqual([]);
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("record", () => {
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.record(SessionApproval.single("bash", "git *"));
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.record(
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.record(
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.record(SessionApproval.multiple("external_directory", []));
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
+ });
@@ -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
+ });