@gotgenes/pi-permission-system 3.5.0 → 3.7.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/src/index.ts CHANGED
@@ -11,18 +11,11 @@ import {
11
11
  type ExtensionCommandContext,
12
12
  type ExtensionContext,
13
13
  getAgentDir,
14
- isToolCallEventType,
15
14
  } from "@mariozechner/pi-coding-agent";
16
15
  import {
17
16
  getActiveAgentName,
18
17
  getActiveAgentNameFromSystemPrompt,
19
18
  } from "./active-agent";
20
- import {
21
- createActiveToolsCacheKey,
22
- createBeforeAgentStartPromptStateKey,
23
- shouldApplyCachedAgentStartState,
24
- } from "./before-agent-start-cache";
25
- import { getNonEmptyString, toRecord } from "./common";
26
19
  import { loadAndMergeConfigs, loadUnifiedConfig } from "./config-loader";
27
20
  import { registerPermissionSystemCommand } from "./config-modal";
28
21
  import {
@@ -43,63 +36,36 @@ import {
43
36
  normalizePermissionSystemConfig,
44
37
  type PermissionSystemExtensionConfig,
45
38
  } from "./extension-config";
46
- import {
47
- extractExternalPathsFromBashCommand,
48
- formatBashExternalDirectoryAskPrompt,
49
- formatBashExternalDirectoryDenyReason,
50
- formatExternalDirectoryAskPrompt,
51
- formatExternalDirectoryDenyReason,
52
- formatExternalDirectoryHardStopHint,
53
- formatExternalDirectoryUserDeniedReason,
54
- getPathBearingToolPath,
55
- isPathOutsideWorkingDirectory,
56
- normalizePathForComparison,
57
- PATH_BEARING_TOOLS,
58
- } from "./external-directory";
59
39
  import { setForwardedPermissionLogger } from "./forwarded-permissions/io";
60
40
  import {
61
41
  confirmPermission,
62
42
  type PermissionForwardingDeps,
63
43
  processForwardedPermissionRequests,
64
44
  } from "./forwarded-permissions/polling";
45
+ import {
46
+ type HandlerDeps,
47
+ handleBeforeAgentStart,
48
+ handleInput,
49
+ handleResourcesDiscover,
50
+ handleSessionShutdown,
51
+ handleSessionStart,
52
+ handleToolCall,
53
+ } from "./handlers";
54
+ import type { PromptPermissionDetails } from "./handlers/types";
65
55
  import { createPermissionSystemLogger } from "./logging";
66
56
  import {
67
57
  type PermissionPromptDecision,
68
58
  requestPermissionDecisionFromUi,
69
59
  } from "./permission-dialog";
70
60
  import { PERMISSION_FORWARDING_POLL_INTERVAL_MS } from "./permission-forwarding";
71
- import { applyPermissionGate } from "./permission-gate";
72
61
  import { PermissionManager } from "./permission-manager";
73
- import {
74
- formatAskPrompt,
75
- formatDenyReason,
76
- formatMissingToolNameReason,
77
- formatSkillAskPrompt,
78
- formatSkillPathAskPrompt,
79
- formatSkillPathDenyReason,
80
- formatUnknownToolReason,
81
- formatUserDeniedReason,
82
- } from "./permission-prompts";
83
- import {
84
- deriveApprovalPrefix,
85
- SessionApprovalCache,
86
- } from "./session-approval-cache";
87
- import {
88
- findSkillPathMatch,
89
- resolveSkillPromptEntries,
90
- type SkillPromptEntry,
91
- } from "./skill-prompt-sanitizer";
62
+ import { SessionApprovalCache } from "./session-approval-cache";
63
+ import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
92
64
  import {
93
65
  PERMISSION_SYSTEM_STATUS_KEY,
94
66
  syncPermissionSystemStatus,
95
67
  } from "./status";
96
68
  import { isSubagentExecutionContext } from "./subagent-context";
97
- import { sanitizeAvailableToolsSection } from "./system-prompt-sanitizer";
98
- import { getPermissionLogContext } from "./tool-input-preview";
99
- import {
100
- checkRequestedToolRegistration,
101
- getToolNameFromValue,
102
- } from "./tool-registry";
103
69
  import {
104
70
  canResolveAskPermissionRequest,
105
71
  shouldAutoApprovePermissionState,
@@ -110,8 +76,6 @@ const SESSIONS_DIR = join(PI_AGENT_DIR, "sessions");
110
76
  const SUBAGENT_SESSIONS_DIR = join(PI_AGENT_DIR, "subagent-sessions");
111
77
  const PERMISSION_FORWARDING_DIR = join(SESSIONS_DIR, "permission-forwarding");
112
78
 
113
- type PermissionReviewSource = "tool_call" | "skill_input" | "skill_read";
114
-
115
79
  let extensionConfig: PermissionSystemExtensionConfig = {
116
80
  ...DEFAULT_EXTENSION_CONFIG,
117
81
  };
@@ -140,7 +104,6 @@ function reportLoggingWarning(message: string): void {
140
104
  if (!loggingWarningReporter || reportedLoggingWarnings.has(message)) {
141
105
  return;
142
106
  }
143
-
144
107
  reportedLoggingWarnings.add(message);
145
108
  loggingWarningReporter(message);
146
109
  }
@@ -165,50 +128,6 @@ function writeReviewLog(
165
128
  }
166
129
  }
167
130
 
168
- function extractSkillNameFromInput(text: string): string | null {
169
- const trimmed = text.trim();
170
- if (!trimmed.startsWith("/skill:")) {
171
- return null;
172
- }
173
-
174
- const afterPrefix = trimmed.slice("/skill:".length);
175
- if (!afterPrefix) {
176
- return null;
177
- }
178
-
179
- const firstWhitespace = afterPrefix.search(/\s/);
180
- const skillName = (
181
- firstWhitespace === -1 ? afterPrefix : afterPrefix.slice(0, firstWhitespace)
182
- ).trim();
183
- return skillName || null;
184
- }
185
-
186
- function getEventToolName(event: unknown): string | null {
187
- return getToolNameFromValue(event);
188
- }
189
-
190
- function getEventInput(event: unknown): unknown {
191
- const record = toRecord(event);
192
-
193
- if (record.input !== undefined) {
194
- return record.input;
195
- }
196
-
197
- if (record.arguments !== undefined) {
198
- return record.arguments;
199
- }
200
-
201
- return {};
202
- }
203
-
204
- function canRequestPermissionConfirmation(ctx: ExtensionContext): boolean {
205
- return canResolveAskPermissionRequest({
206
- config: extensionConfig,
207
- hasUI: ctx.hasUI,
208
- isSubagent: isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR),
209
- });
210
- }
211
-
212
131
  function derivePiProjectPaths(cwd: string | undefined | null): {
213
132
  projectGlobalConfigPath: string;
214
133
  projectAgentsDir: string;
@@ -216,7 +135,6 @@ function derivePiProjectPaths(cwd: string | undefined | null): {
216
135
  if (!cwd) {
217
136
  return null;
218
137
  }
219
-
220
138
  return {
221
139
  projectGlobalConfigPath: getProjectConfigPath(cwd),
222
140
  projectAgentsDir: join(cwd, ".pi", "agent", "agents"),
@@ -228,7 +146,6 @@ function createPermissionManagerForCwd(
228
146
  ): PermissionManager {
229
147
  const agentDir = getAgentDir();
230
148
  const projectPaths = derivePiProjectPaths(cwd);
231
-
232
149
  return new PermissionManager({
233
150
  globalConfigPath: getGlobalConfigPath(agentDir),
234
151
  projectGlobalConfigPath: projectPaths?.projectGlobalConfigPath,
@@ -249,17 +166,10 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
249
166
  let runtimeContext: ExtensionContext | null = null;
250
167
  let lastConfigWarning: string | null = null;
251
168
 
252
- const invalidateAgentStartCache = (): void => {
253
- activeSkillEntries = [];
254
- lastActiveToolsCacheKey = null;
255
- lastPromptStateCacheKey = null;
256
- };
257
-
258
169
  const notifyWarning = (message: string): void => {
259
170
  if (!runtimeContext?.hasUI) {
260
171
  return;
261
172
  }
262
-
263
173
  runtimeContext.ui.notify(message, "warning");
264
174
  };
265
175
 
@@ -267,7 +177,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
267
177
  if (ctx) {
268
178
  runtimeContext = ctx;
269
179
  }
270
-
271
180
  const cwd = runtimeContext?.cwd ?? null;
272
181
  const agentDir = getAgentDir();
273
182
  const mergeResult = loadAndMergeConfigs(
@@ -306,7 +215,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
306
215
  const normalized = normalizePermissionSystemConfig(next);
307
216
  const globalPath = getGlobalConfigPath(getAgentDir());
308
217
 
309
- // Load existing global config and merge runtime knobs into it
310
218
  const existing = loadUnifiedConfig(globalPath);
311
219
  const merged = {
312
220
  ...existing.config,
@@ -366,24 +274,44 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
366
274
  getConfigPath: () => getGlobalConfigPath(getAgentDir()),
367
275
  });
368
276
 
369
- const createPermissionRequestId = (prefix: string): string => {
370
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
277
+ const stopForwardedPermissionPolling = (): void => {
278
+ if (permissionForwardingTimer) {
279
+ clearInterval(permissionForwardingTimer);
280
+ permissionForwardingTimer = null;
281
+ }
282
+ permissionForwardingContext = null;
283
+ isProcessingForwardedRequests = false;
284
+ };
285
+
286
+ const startForwardedPermissionPolling = (ctx: ExtensionContext): void => {
287
+ if (!ctx.hasUI || isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR)) {
288
+ stopForwardedPermissionPolling();
289
+ return;
290
+ }
291
+ permissionForwardingContext = ctx;
292
+ if (permissionForwardingTimer) {
293
+ return;
294
+ }
295
+ permissionForwardingTimer = setInterval(() => {
296
+ if (!permissionForwardingContext || isProcessingForwardedRequests) {
297
+ return;
298
+ }
299
+ isProcessingForwardedRequests = true;
300
+ void processForwardedPermissionRequests(
301
+ permissionForwardingContext,
302
+ forwardingDeps,
303
+ ).finally(() => {
304
+ isProcessingForwardedRequests = false;
305
+ });
306
+ }, PERMISSION_FORWARDING_POLL_INTERVAL_MS);
371
307
  };
372
308
 
309
+ const createPermissionRequestId = (prefix: string): string =>
310
+ `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}-${process.pid}`;
311
+
373
312
  const reviewPermissionDecision = (
374
313
  event: string,
375
- details: {
376
- requestId: string;
377
- source: PermissionReviewSource;
378
- agentName: string | null;
379
- message: string;
380
- toolCallId?: string;
381
- toolName?: string;
382
- skillName?: string;
383
- path?: string;
384
- command?: string;
385
- target?: string;
386
- toolInputPreview?: string;
314
+ details: PromptPermissionDetails & {
387
315
  resolution?: string;
388
316
  denialReason?: string;
389
317
  },
@@ -407,27 +335,13 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
407
335
 
408
336
  const promptPermission = async (
409
337
  ctx: ExtensionContext,
410
- details: {
411
- requestId: string;
412
- source: PermissionReviewSource;
413
- agentName: string | null;
414
- message: string;
415
- toolCallId?: string;
416
- toolName?: string;
417
- skillName?: string;
418
- path?: string;
419
- command?: string;
420
- target?: string;
421
- toolInputPreview?: string;
422
- },
338
+ details: PromptPermissionDetails,
423
339
  ): Promise<PermissionPromptDecision> => {
424
340
  if (shouldAutoApprovePermissionState("ask", extensionConfig)) {
425
341
  reviewPermissionDecision("permission_request.auto_approved", details);
426
342
  return { approved: true, state: "approved" };
427
343
  }
428
-
429
344
  reviewPermissionDecision("permission_request.waiting", details);
430
-
431
345
  const decision = await confirmPermission(
432
346
  ctx,
433
347
  details.message,
@@ -446,42 +360,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
446
360
  return decision;
447
361
  };
448
362
 
449
- const stopForwardedPermissionPolling = (): void => {
450
- if (permissionForwardingTimer) {
451
- clearInterval(permissionForwardingTimer);
452
- permissionForwardingTimer = null;
453
- }
454
-
455
- permissionForwardingContext = null;
456
- isProcessingForwardedRequests = false;
457
- };
458
-
459
- const startForwardedPermissionPolling = (ctx: ExtensionContext): void => {
460
- if (!ctx.hasUI || isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR)) {
461
- stopForwardedPermissionPolling();
462
- return;
463
- }
464
-
465
- permissionForwardingContext = ctx;
466
- if (permissionForwardingTimer) {
467
- return;
468
- }
469
-
470
- permissionForwardingTimer = setInterval(() => {
471
- if (!permissionForwardingContext || isProcessingForwardedRequests) {
472
- return;
473
- }
474
-
475
- isProcessingForwardedRequests = true;
476
- void processForwardedPermissionRequests(
477
- permissionForwardingContext,
478
- forwardingDeps,
479
- ).finally(() => {
480
- isProcessingForwardedRequests = false;
481
- });
482
- }, PERMISSION_FORWARDING_POLL_INTERVAL_MS);
483
- };
484
-
485
363
  const resolveAgentName = (
486
364
  ctx: ExtensionContext,
487
365
  systemPrompt?: string,
@@ -491,35 +369,17 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
491
369
  lastKnownActiveAgentName = fromSession;
492
370
  return fromSession;
493
371
  }
494
-
495
372
  const fromSystemPrompt = getActiveAgentNameFromSystemPrompt(systemPrompt);
496
373
  if (fromSystemPrompt) {
497
374
  lastKnownActiveAgentName = fromSystemPrompt;
498
375
  return fromSystemPrompt;
499
376
  }
500
-
501
377
  return lastKnownActiveAgentName;
502
378
  };
503
379
 
504
- const shouldExposeTool = (
505
- toolName: string,
506
- agentName: string | null,
507
- ): boolean => {
508
- // Use tool-level permission check for tool injection decisions
509
- // This ensures that agent-specific tool deny rules (e.g., bash: deny) are respected
510
- // before any command-level permissions are considered
511
- const toolPermission = permissionManager.getToolPermission(
512
- toolName,
513
- agentName ?? undefined,
514
- );
515
- return toolPermission !== "deny";
516
- };
517
-
518
380
  const logResolvedConfigPaths = (): void => {
519
381
  const policyPaths = permissionManager.getResolvedPolicyPaths();
520
382
  const cwd = runtimeContext?.cwd ?? null;
521
-
522
- // Detect legacy files for the log entry
523
383
  const agentDir = getAgentDir();
524
384
  const legacyGlobalPolicyDetected = existsSync(
525
385
  getLegacyGlobalPolicyPath(agentDir),
@@ -532,7 +392,6 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
532
392
  const legacyExtensionConfigDetected =
533
393
  normalize(legacyExtConfigPath) !== normalize(newGlobalPath) &&
534
394
  existsSync(legacyExtConfigPath);
535
-
536
395
  const entry = buildResolvedConfigLogEntry({
537
396
  policyPaths,
538
397
  legacyGlobalPolicyDetected,
@@ -549,518 +408,59 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
549
408
  );
550
409
  };
551
410
 
552
- pi.on("session_start", async (event, ctx) => {
553
- runtimeContext = ctx;
554
- refreshExtensionConfig(ctx);
555
- permissionManager = createPermissionManagerForCwd(ctx.cwd);
556
- invalidateAgentStartCache();
557
- lastKnownActiveAgentName = getActiveAgentName(ctx);
558
- startForwardedPermissionPolling(ctx);
559
- logResolvedConfigPaths();
560
-
561
- const policyIssues = permissionManager.getConfigIssues(
562
- lastKnownActiveAgentName,
563
- );
564
- for (const issue of policyIssues) {
565
- notifyWarning(issue);
566
- }
567
-
568
- if (event.reason === "reload") {
569
- writeDebugLog("lifecycle.reload", {
570
- triggeredBy: "session_start",
571
- reason: event.reason,
572
- cwd: ctx.cwd,
573
- });
574
- }
575
- });
576
-
577
- pi.on("resources_discover", async (event, _ctx) => {
578
- if (event.reason === "reload") {
579
- permissionManager = runtimeContext
580
- ? createPermissionManagerForCwd(runtimeContext.cwd)
581
- : new PermissionManager();
582
- invalidateAgentStartCache();
583
- writeDebugLog("lifecycle.reload", {
584
- triggeredBy: "resources_discover",
585
- reason: event.reason,
586
- cwd: runtimeContext?.cwd ?? null,
587
- });
588
- }
589
- });
590
-
591
- pi.on("session_shutdown", async () => {
592
- runtimeContext?.ui.setStatus(PERMISSION_SYSTEM_STATUS_KEY, undefined);
593
- runtimeContext = null;
594
- invalidateAgentStartCache();
595
- sessionApprovalCache.clear();
596
- stopForwardedPermissionPolling();
597
- });
598
-
599
- pi.on("before_agent_start", async (event, ctx) => {
600
- runtimeContext = ctx;
601
- refreshExtensionConfig(ctx);
602
- startForwardedPermissionPolling(ctx);
603
- const agentName = resolveAgentName(ctx, event.systemPrompt);
604
- const allTools = pi.getAllTools();
605
- const allowedTools: string[] = [];
606
-
607
- for (const tool of allTools) {
608
- const toolName = getEventToolName(tool);
609
- if (!toolName) {
610
- continue;
611
- }
612
-
613
- if (shouldExposeTool(toolName, agentName)) {
614
- allowedTools.push(toolName);
615
- }
616
- }
617
-
618
- const activeToolsCacheKey = createActiveToolsCacheKey(allowedTools);
619
- if (
620
- shouldApplyCachedAgentStartState(
621
- lastActiveToolsCacheKey,
622
- activeToolsCacheKey,
623
- )
624
- ) {
625
- pi.setActiveTools(allowedTools);
626
- lastActiveToolsCacheKey = activeToolsCacheKey;
627
- }
628
-
629
- const promptStateCacheKey = createBeforeAgentStartPromptStateKey({
630
- agentName,
631
- cwd: ctx.cwd,
632
- permissionStamp: permissionManager.getPolicyCacheStamp(
633
- agentName ?? undefined,
634
- ),
635
- systemPrompt: event.systemPrompt,
636
- allowedToolNames: allowedTools,
637
- });
638
-
639
- if (
640
- !shouldApplyCachedAgentStartState(
641
- lastPromptStateCacheKey,
642
- promptStateCacheKey,
643
- )
644
- ) {
645
- return {};
646
- }
647
-
648
- lastPromptStateCacheKey = promptStateCacheKey;
649
- const toolPromptResult = sanitizeAvailableToolsSection(
650
- event.systemPrompt,
651
- allowedTools,
652
- );
653
- const skillPromptResult = resolveSkillPromptEntries(
654
- toolPromptResult.prompt,
655
- permissionManager,
656
- agentName,
657
- ctx.cwd,
658
- );
659
- activeSkillEntries = skillPromptResult.entries;
660
-
661
- if (skillPromptResult.prompt !== event.systemPrompt) {
662
- return { systemPrompt: skillPromptResult.prompt };
663
- }
664
-
665
- return {};
666
- });
667
-
668
- pi.on("input", async (event, ctx) => {
669
- runtimeContext = ctx;
670
- startForwardedPermissionPolling(ctx);
671
- const skillName = extractSkillNameFromInput(event.text);
672
- if (!skillName) {
673
- return { action: "continue" };
674
- }
675
-
676
- const agentName = resolveAgentName(ctx);
677
- const check = permissionManager.checkPermission(
678
- "skill",
679
- { name: skillName },
680
- agentName ?? undefined,
681
- );
682
-
683
- if (check.state === "deny" && ctx.hasUI) {
684
- const notifyMessage = agentName
685
- ? `Skill '${skillName}' is not permitted for agent '${agentName}'.`
686
- : `Skill '${skillName}' is not permitted by the current skill policy.`;
687
- ctx.ui.notify(notifyMessage, "warning");
688
- }
689
-
690
- const skillInputMessage = formatSkillAskPrompt(
691
- skillName,
692
- agentName ?? undefined,
693
- );
694
- const skillInputGate = await applyPermissionGate({
695
- state: check.state,
696
- canConfirm: canRequestPermissionConfirmation(ctx),
697
- promptForApproval: () =>
698
- promptPermission(ctx, {
699
- requestId: createPermissionRequestId("skill-input"),
700
- source: "skill_input",
701
- agentName,
702
- message: skillInputMessage,
703
- skillName,
704
- }),
705
- writeLog: writeReviewLog,
706
- logContext: {
707
- source: "skill_input",
708
- skillName,
709
- agentName,
710
- message: skillInputMessage,
711
- },
712
- messages: {
713
- denyReason: skillInputMessage,
714
- unavailableReason:
715
- "Skill requires approval, but no interactive UI is available.",
716
- userDeniedReason: () => "User denied skill.",
717
- },
718
- });
719
- if (skillInputGate.action === "block") {
720
- return { action: "handled" };
721
- }
722
-
723
- return { action: "continue" };
724
- });
725
-
726
- pi.on("tool_call", async (event, ctx) => {
727
- runtimeContext = ctx;
728
- startForwardedPermissionPolling(ctx);
729
- const agentName = resolveAgentName(ctx);
730
- const toolName = getEventToolName(event);
731
-
732
- if (!toolName) {
733
- return { block: true, reason: formatMissingToolNameReason() };
734
- }
735
-
736
- const registrationCheck = checkRequestedToolRegistration(
737
- toolName,
738
- pi.getAllTools(),
739
- );
740
- if (registrationCheck.status === "missing-tool-name") {
741
- return { block: true, reason: formatMissingToolNameReason() };
742
- }
743
-
744
- if (registrationCheck.status === "unregistered") {
745
- return {
746
- block: true,
747
- reason: formatUnknownToolReason(
748
- registrationCheck.requestedToolName,
749
- registrationCheck.availableToolNames,
750
- ),
751
- };
752
- }
753
-
754
- if (isToolCallEventType("read", event) && activeSkillEntries.length > 0) {
755
- const normalizedReadPath = normalizePathForComparison(
756
- event.input.path,
757
- ctx.cwd,
758
- );
759
- const matchedSkill = findSkillPathMatch(
760
- normalizedReadPath,
761
- activeSkillEntries,
762
- );
763
-
764
- if (matchedSkill) {
765
- const skillReadMessage = formatSkillPathAskPrompt(
766
- matchedSkill,
767
- event.input.path,
768
- agentName ?? undefined,
769
- );
770
- const skillReadGate = await applyPermissionGate({
771
- state: matchedSkill.state,
772
- canConfirm: canRequestPermissionConfirmation(ctx),
773
- promptForApproval: () =>
774
- promptPermission(ctx, {
775
- requestId: event.toolCallId,
776
- source: "skill_read",
777
- agentName,
778
- message: skillReadMessage,
779
- toolCallId: event.toolCallId,
780
- toolName: toolName,
781
- skillName: matchedSkill.name,
782
- path: event.input.path,
783
- }),
784
- writeLog: writeReviewLog,
785
- logContext: {
786
- source: "skill_read",
787
- skillName: matchedSkill.name,
788
- agentName,
789
- path: event.input.path,
790
- message: skillReadMessage,
791
- },
792
- messages: {
793
- denyReason: formatSkillPathDenyReason(
794
- matchedSkill,
795
- event.input.path,
796
- agentName ?? undefined,
797
- ),
798
- unavailableReason: `Accessing skill '${matchedSkill.name}' requires approval, but no interactive UI is available.`,
799
- userDeniedReason: (decision) => {
800
- const denialReason = decision.denialReason
801
- ? ` Reason: ${decision.denialReason}.`
802
- : "";
803
- return `User denied access to skill '${matchedSkill.name}'.${denialReason}`;
804
- },
805
- },
806
- });
807
- if (skillReadGate.action === "block") {
808
- return { block: true, reason: skillReadGate.reason };
809
- }
810
- }
811
- }
812
-
813
- const input = getEventInput(event);
814
- const externalDirectoryPath = ctx.cwd
815
- ? getPathBearingToolPath(toolName, input)
816
- : null;
817
-
818
- if (
819
- ctx.cwd &&
820
- externalDirectoryPath &&
821
- isPathOutsideWorkingDirectory(externalDirectoryPath, ctx.cwd)
822
- ) {
823
- const normalizedExtPath = normalizePathForComparison(
824
- externalDirectoryPath,
825
- ctx.cwd,
826
- );
827
- const sessionPrefix = sessionApprovalCache.findMatchingPrefix(
828
- "external_directory",
829
- normalizedExtPath,
830
- );
831
-
832
- if (sessionPrefix) {
833
- writeReviewLog("permission_request.session_approved", {
834
- source: "tool_call",
835
- toolCallId: event.toolCallId,
836
- toolName,
837
- agentName,
838
- path: externalDirectoryPath,
839
- resolution: "session_approved",
840
- sessionApprovalPrefix: sessionPrefix,
841
- });
842
- // Fall through to normal permission check
843
- } else {
844
- const extCheck = permissionManager.checkPermission(
845
- "external_directory",
846
- {},
847
- agentName ?? undefined,
848
- );
849
-
850
- let extDirDecision: PermissionPromptDecision | null = null;
851
- const extDirMessage = formatExternalDirectoryAskPrompt(
852
- toolName,
853
- externalDirectoryPath,
854
- ctx.cwd,
855
- agentName ?? undefined,
856
- );
857
- const extDirGate = await applyPermissionGate({
858
- state: extCheck.state,
859
- canConfirm: canRequestPermissionConfirmation(ctx),
860
- promptForApproval: async () => {
861
- const decision = await promptPermission(ctx, {
862
- requestId: event.toolCallId,
863
- source: "tool_call",
864
- agentName,
865
- message: extDirMessage,
866
- toolCallId: event.toolCallId,
867
- toolName,
868
- path: externalDirectoryPath,
869
- });
870
- extDirDecision = decision;
871
- return decision;
872
- },
873
- writeLog: writeReviewLog,
874
- logContext: {
875
- source: "tool_call",
876
- toolCallId: event.toolCallId,
877
- toolName,
878
- agentName,
879
- path: externalDirectoryPath,
880
- message: extDirMessage,
881
- },
882
- messages: {
883
- denyReason: formatExternalDirectoryDenyReason(
884
- toolName,
885
- externalDirectoryPath,
886
- ctx.cwd,
887
- agentName ?? undefined,
888
- ),
889
- unavailableReason: `Accessing '${externalDirectoryPath}' outside the working directory requires approval, but no interactive UI is available.`,
890
- userDeniedReason: (decision) =>
891
- formatExternalDirectoryUserDeniedReason(
892
- toolName,
893
- externalDirectoryPath,
894
- decision.denialReason,
895
- ),
896
- },
897
- });
898
- if (extDirGate.action === "block") {
899
- return { block: true, reason: extDirGate.reason };
900
- }
901
-
902
- if (extDirDecision?.state === "approved_for_session") {
903
- const prefix = deriveApprovalPrefix(normalizedExtPath);
904
- sessionApprovalCache.approve("external_directory", prefix);
905
- }
906
- }
907
- // Fall through to normal permission check
908
- }
909
-
910
- // Bash external directory gate: extract paths from bash commands
911
- if (ctx.cwd && toolName === "bash") {
912
- const command = getNonEmptyString(toRecord(input).command);
913
- if (command) {
914
- const externalPaths = extractExternalPathsFromBashCommand(
915
- command,
916
- ctx.cwd,
917
- );
918
- if (externalPaths.length > 0) {
919
- // Filter out paths already covered by session approvals
920
- const uncoveredPaths = externalPaths.filter(
921
- (p) => !sessionApprovalCache.has("external_directory", p),
922
- );
923
-
924
- if (uncoveredPaths.length === 0) {
925
- // All external paths are session-approved
926
- writeReviewLog("permission_request.session_approved", {
927
- source: "tool_call",
928
- toolCallId: event.toolCallId,
929
- toolName,
930
- agentName,
931
- command,
932
- externalPaths,
933
- resolution: "session_approved",
934
- });
935
- // Fall through to normal bash permission check
936
- } else {
937
- const extCheck = permissionManager.checkPermission(
938
- "external_directory",
939
- {},
940
- agentName ?? undefined,
941
- );
942
-
943
- let bashExtDecision: PermissionPromptDecision | null = null;
944
- const bashExtMessage = formatBashExternalDirectoryAskPrompt(
945
- command,
946
- uncoveredPaths,
947
- ctx.cwd,
948
- agentName ?? undefined,
949
- );
950
- const bashExtGate = await applyPermissionGate({
951
- state: extCheck.state,
952
- canConfirm: canRequestPermissionConfirmation(ctx),
953
- promptForApproval: async () => {
954
- const decision = await promptPermission(ctx, {
955
- requestId: event.toolCallId,
956
- source: "tool_call",
957
- agentName,
958
- message: bashExtMessage,
959
- toolCallId: event.toolCallId,
960
- toolName,
961
- command,
962
- });
963
- bashExtDecision = decision;
964
- return decision;
965
- },
966
- writeLog: writeReviewLog,
967
- logContext: {
968
- source: "tool_call",
969
- toolCallId: event.toolCallId,
970
- toolName,
971
- agentName,
972
- command,
973
- externalPaths: uncoveredPaths,
974
- message: bashExtMessage,
975
- },
976
- messages: {
977
- denyReason: formatBashExternalDirectoryDenyReason(
978
- command,
979
- uncoveredPaths,
980
- ctx.cwd,
981
- agentName ?? undefined,
982
- ),
983
- unavailableReason: `Bash command '${command}' references path(s) outside the working directory and requires approval, but no interactive UI is available.`,
984
- userDeniedReason: (decision) => {
985
- const reasonSuffix = decision.denialReason
986
- ? ` Reason: ${decision.denialReason}.`
987
- : "";
988
- return `User denied external directory access for bash command '${command}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
989
- },
990
- },
991
- });
992
- if (bashExtGate.action === "block") {
993
- return { block: true, reason: bashExtGate.reason };
994
- }
995
-
996
- if (bashExtDecision?.state === "approved_for_session") {
997
- for (const extPath of uncoveredPaths) {
998
- const prefix = deriveApprovalPrefix(extPath);
999
- sessionApprovalCache.approve("external_directory", prefix);
1000
- }
1001
- }
1002
- }
1003
- // Fall through to normal bash permission check
1004
- }
1005
- }
1006
- }
1007
-
1008
- const check = permissionManager.checkPermission(
1009
- toolName,
1010
- input,
1011
- agentName ?? undefined,
1012
- );
1013
- const permissionLogContext = getPermissionLogContext(
1014
- check,
1015
- input,
1016
- PATH_BEARING_TOOLS,
1017
- );
1018
-
1019
- const toolUnavailableReason =
1020
- toolName === "bash" && isToolCallEventType("bash", event)
1021
- ? `Running bash command '${event.input.command}' requires approval, but no interactive UI is available.`
1022
- : toolName === "mcp"
1023
- ? "Using tool 'mcp' requires approval, but no interactive UI is available."
1024
- : `Using tool '${toolName}' requires approval, but no interactive UI is available.`;
1025
-
1026
- const toolAskMessage = formatAskPrompt(
1027
- check,
1028
- agentName ?? undefined,
1029
- input,
1030
- );
1031
- const toolGate = await applyPermissionGate({
1032
- state: check.state,
1033
- canConfirm: canRequestPermissionConfirmation(ctx),
1034
- promptForApproval: () =>
1035
- promptPermission(ctx, {
1036
- requestId: event.toolCallId,
1037
- source: "tool_call",
1038
- agentName,
1039
- message: toolAskMessage,
1040
- toolCallId: event.toolCallId,
1041
- toolName,
1042
- ...permissionLogContext,
1043
- }),
1044
- writeLog: writeReviewLog,
1045
- logContext: {
1046
- source: "tool_call",
1047
- toolCallId: event.toolCallId,
1048
- toolName,
1049
- agentName,
1050
- message: toolAskMessage,
1051
- ...permissionLogContext,
1052
- },
1053
- messages: {
1054
- denyReason: formatDenyReason(check, agentName ?? undefined),
1055
- unavailableReason: toolUnavailableReason,
1056
- userDeniedReason: (decision) =>
1057
- formatUserDeniedReason(check, decision.denialReason),
1058
- },
1059
- });
1060
- if (toolGate.action === "block") {
1061
- return { block: true, reason: toolGate.reason };
1062
- }
411
+ const deps: HandlerDeps = {
412
+ getPermissionManager: () => permissionManager,
413
+ setPermissionManager: (pm) => {
414
+ permissionManager = pm;
415
+ },
416
+ getRuntimeContext: () => runtimeContext,
417
+ setRuntimeContext: (ctx) => {
418
+ runtimeContext = ctx;
419
+ },
420
+ getActiveSkillEntries: () => activeSkillEntries,
421
+ setActiveSkillEntries: (entries) => {
422
+ activeSkillEntries = entries;
423
+ },
424
+ getLastKnownActiveAgentName: () => lastKnownActiveAgentName,
425
+ setLastKnownActiveAgentName: (name) => {
426
+ lastKnownActiveAgentName = name;
427
+ },
428
+ getLastActiveToolsCacheKey: () => lastActiveToolsCacheKey,
429
+ setLastActiveToolsCacheKey: (key) => {
430
+ lastActiveToolsCacheKey = key;
431
+ },
432
+ getLastPromptStateCacheKey: () => lastPromptStateCacheKey,
433
+ setLastPromptStateCacheKey: (key) => {
434
+ lastPromptStateCacheKey = key;
435
+ },
436
+ sessionApprovalCache,
437
+ createPermissionManagerForCwd,
438
+ refreshExtensionConfig,
439
+ notifyWarning,
440
+ logResolvedConfigPaths,
441
+ resolveAgentName,
442
+ canRequestPermissionConfirmation: (ctx) =>
443
+ canResolveAskPermissionRequest({
444
+ config: extensionConfig,
445
+ hasUI: ctx.hasUI,
446
+ isSubagent: isSubagentExecutionContext(ctx, SUBAGENT_SESSIONS_DIR),
447
+ }),
448
+ promptPermission,
449
+ createPermissionRequestId,
450
+ startForwardedPermissionPolling,
451
+ stopForwardedPermissionPolling,
452
+ writeReviewLog,
453
+ writeDebugLog,
454
+ getAllTools: () => pi.getAllTools(),
455
+ setActiveTools: (names) => pi.setActiveTools(names),
456
+ };
1063
457
 
1064
- return {};
1065
- });
458
+ pi.on("session_start", (event, ctx) => handleSessionStart(deps, event, ctx));
459
+ pi.on("resources_discover", (event) => handleResourcesDiscover(deps, event));
460
+ pi.on("session_shutdown", () => handleSessionShutdown(deps));
461
+ pi.on("before_agent_start", (event, ctx) =>
462
+ handleBeforeAgentStart(deps, event, ctx),
463
+ );
464
+ pi.on("input", (event, ctx) => handleInput(deps, event, ctx));
465
+ pi.on("tool_call", (event, ctx) => handleToolCall(deps, event, ctx));
1066
466
  }