@gotgenes/pi-permission-system 25.2.2 → 25.3.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -1
  3. package/config/config.example.json +3 -0
  4. package/dist/public.d.ts +156 -41
  5. package/docs/configuration.md +17 -2
  6. package/package.json +1 -1
  7. package/schemas/permissions.schema.json +16 -0
  8. package/src/access-intent/bash/command-enumeration.ts +45 -117
  9. package/src/access-intent/bash/wrapper-analysis.ts +335 -0
  10. package/src/authority/forwarded-request-server.ts +5 -14
  11. package/src/authority/local-user-authorizer.ts +2 -3
  12. package/src/authority/permission-prompt-component.ts +87 -47
  13. package/src/authority/permission-prompter.ts +9 -0
  14. package/src/config-loader.ts +2 -0
  15. package/src/config-schema.ts +14 -0
  16. package/src/extension-config.ts +10 -0
  17. package/src/handlers/gates/bash-command.ts +7 -2
  18. package/src/handlers/gates/bash-external-directory.ts +11 -6
  19. package/src/handlers/gates/bash-path.ts +10 -6
  20. package/src/handlers/gates/external-directory.ts +13 -8
  21. package/src/handlers/gates/path.ts +11 -14
  22. package/src/handlers/gates/skill-input.ts +5 -2
  23. package/src/handlers/gates/skill-read.ts +5 -6
  24. package/src/handlers/gates/tool.ts +10 -5
  25. package/src/index.ts +2 -0
  26. package/src/permission-prompts.ts +4 -72
  27. package/src/presentation/dialog-renderer.ts +404 -0
  28. package/src/presentation/forwarded-ask-payload.ts +45 -0
  29. package/src/presentation/legacy-message.ts +117 -0
  30. package/src/presentation/line-fitting.ts +27 -0
  31. package/src/presentation/path-ask-payload.ts +128 -0
  32. package/src/presentation/prompt-payload.ts +137 -0
  33. package/src/presentation/skill-ask-payload.ts +50 -0
  34. package/src/presentation/tool-ask-payload.ts +104 -0
  35. package/src/tool-preview-formatter.ts +1 -1
  36. package/src/types.ts +6 -0
  37. package/src/handlers/gates/external-directory-messages.ts +0 -28
@@ -194,6 +194,20 @@ export const unifiedConfigSchema = z
194
194
  "How long a subagent waits for the parent session to answer a forwarded permission request, in milliseconds.\n\nOmit to use the default (`600000`, ten minutes). A child whose in-process parent is not draining its inbox at all gives up in a couple of seconds regardless of this value, so lower it only to bound how long you are willing to leave an *unanswered* prompt pending.",
195
195
  default: 600000,
196
196
  }),
197
+ promptMaxRows: z.number().int().min(1).optional().meta({
198
+ description:
199
+ "Maximum rows a permission prompt renders before eliding its evidence. Omit to use the default (24).",
200
+ markdownDescription:
201
+ "Maximum rows a permission prompt renders before eliding its evidence.\n\nOmit to use the default (24). The request's own facts — the requesting agent, the tool, the matched rule, the decision-relevant value — are never elided by this budget; what gives way is the supporting evidence, and `Ctrl+O` expands the prompt to the complete request.",
202
+ default: 24,
203
+ }),
204
+ promptFieldMaxWidth: z.number().int().min(1).optional().meta({
205
+ description:
206
+ "Maximum characters of any one field shown in a permission prompt. Omit to use the default (400).",
207
+ markdownDescription:
208
+ "Maximum characters of any one field shown in a permission prompt.\n\nOmit to use the default (400). This is what bounds a single pathological field — a long here-string command, say — that would otherwise fill the prompt through wrapping. A shortened field is marked with an ellipsis, and `Ctrl+O` shows it in full.",
209
+ default: 400,
210
+ }),
197
211
  toolInputPreviewMaxLength: z.number().int().min(1).optional().meta({
198
212
  description:
199
213
  "Maximum character length of the inline-JSON tool-input preview shown in permission prompts. Omit to use the default (200). Set to a large value to disable truncation.",
@@ -22,6 +22,10 @@ export interface PermissionSystemExtensionConfig {
22
22
  piInfrastructureReadPaths?: string[];
23
23
  /** How long a subagent waits for the parent's answer to a forwarded ask, in ms. Defaults to 600000. */
24
24
  forwardingTimeoutMs?: number;
25
+ /** Max rows a permission prompt renders before eliding its evidence. Defaults to 24. */
26
+ promptMaxRows?: number;
27
+ /** Max characters of any one field shown in a permission prompt. Defaults to 400. */
28
+ promptFieldMaxWidth?: number;
25
29
  /** Max length of the inline-JSON input preview shown in permission prompts. Defaults to 200. */
26
30
  toolInputPreviewMaxLength?: number;
27
31
  /** Max length of inline pattern/path summaries (grep/find/ls) in permission prompts. Defaults to 80. */
@@ -76,6 +80,12 @@ export function normalizePermissionSystemConfig(
76
80
  if (raw.forwardingTimeoutMs !== undefined) {
77
81
  result.forwardingTimeoutMs = raw.forwardingTimeoutMs;
78
82
  }
83
+ if (raw.promptMaxRows !== undefined) {
84
+ result.promptMaxRows = raw.promptMaxRows;
85
+ }
86
+ if (raw.promptFieldMaxWidth !== undefined) {
87
+ result.promptFieldMaxWidth = raw.promptFieldMaxWidth;
88
+ }
79
89
  if (raw.toolInputPreviewMaxLength !== undefined) {
80
90
  result.toolInputPreviewMaxLength = raw.toolInputPreviewMaxLength;
81
91
  }
@@ -83,7 +83,7 @@ export function resolveBashCommandCheck(
83
83
  input: { command: cmd.text },
84
84
  agentName,
85
85
  });
86
- const result =
86
+ const floored =
87
87
  cmd.wrapperKind && base.state === "allow"
88
88
  ? {
89
89
  ...base,
@@ -91,7 +91,12 @@ export function resolveBashCommandCheck(
91
91
  matchedPattern: WRAPPER_SENTINEL[cmd.wrapperKind],
92
92
  }
93
93
  : base;
94
- return cmd.context ? { ...result, commandContext: cmd.context } : result;
94
+ const result = cmd.context
95
+ ? { ...floored, commandContext: cmd.context }
96
+ : floored;
97
+ return cmd.executedUnit === undefined
98
+ ? result
99
+ : { ...result, executedUnit: cmd.executedUnit };
95
100
  });
96
101
  return (
97
102
  pickMostRestrictive(results) ??
@@ -1,9 +1,10 @@
1
1
  import type { BashProgram } from "#src/access-intent/bash/program";
2
2
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
3
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
4
+ import { buildBashExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
3
5
  import { SessionApproval } from "#src/session-approval";
4
6
  import { deriveApprovalPattern } from "#src/session-rules";
5
7
  import type { GateResult } from "./descriptor";
6
- import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
7
8
  import { selectUncoveredExternalPaths } from "./external-directory-policy";
8
9
  import { accessFactsFromPath } from "./helpers";
9
10
  import type { ToolCallContext } from "./types";
@@ -76,12 +77,15 @@ export function describeBashExternalDirectoryGate(
76
77
  resolvedPath: path.resolvedAlias(),
77
78
  }));
78
79
 
79
- const bashExtMessage = formatBashExternalDirectoryAskPrompt(
80
+ const payload = buildBashExternalDirectoryAskPayload({
80
81
  command,
81
- disclosures,
82
- tcc.cwd,
83
- tcc.agentName ?? undefined,
84
- );
82
+ externalPaths: disclosures,
83
+ cwd: tcc.cwd,
84
+ agentName: tcc.agentName,
85
+ toolName: tcc.toolName,
86
+ matchedPattern: preCheck.matchedPattern,
87
+ });
88
+ const bashExtMessage = renderLegacyMessage(payload);
85
89
 
86
90
  const patterns = uncoveredPaths.map((p) => deriveApprovalPattern(p));
87
91
 
@@ -100,6 +104,7 @@ export function describeBashExternalDirectoryGate(
100
104
  source: "tool_call",
101
105
  agentName: tcc.agentName,
102
106
  message: bashExtMessage,
107
+ payload,
103
108
  toolCallId: tcc.toolCallId,
104
109
  toolName: tcc.toolName,
105
110
  command,
@@ -1,13 +1,14 @@
1
1
  import type { AccessPath } from "#src/access-intent/access-path";
2
2
  import type { BashProgram } from "#src/access-intent/bash/program";
3
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
5
+ import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
4
6
  import { SessionApproval } from "#src/session-approval";
5
7
  import { deriveApprovalPattern } from "#src/session-rules";
6
8
  import type { PermissionCheckResult } from "#src/types";
7
9
  import { pickMostRestrictive } from "./candidate-check";
8
10
  import type { GateResult } from "./descriptor";
9
11
  import { accessFactsFromPath } from "./helpers";
10
- import { formatPathAskPrompt } from "./path";
11
12
  import type { ToolCallContext } from "./types";
12
13
 
13
14
  /**
@@ -113,11 +114,13 @@ export function describeBashPathGate(
113
114
  // path), so it matches the values a later call produces. For an unknown base
114
115
  // (`forLiteral`) `value()` is the raw token.
115
116
  const pattern = deriveApprovalPattern(worstEntry.path.value());
116
- const askMessage = formatPathAskPrompt(
117
- tcc.toolName,
118
- worstToken,
119
- tcc.agentName ?? undefined,
120
- );
117
+ const payload = buildPathAskPayload({
118
+ toolName: tcc.toolName,
119
+ pathValue: worstToken,
120
+ agentName: tcc.agentName,
121
+ matchedPattern: worstCheck.matchedPattern,
122
+ });
123
+ const askMessage = renderLegacyMessage(payload);
121
124
 
122
125
  return {
123
126
  surface: "path",
@@ -133,6 +136,7 @@ export function describeBashPathGate(
133
136
  source: "tool_call",
134
137
  agentName: tcc.agentName,
135
138
  message: askMessage,
139
+ payload,
136
140
  toolCallId: tcc.toolCallId,
137
141
  toolName: tcc.toolName,
138
142
  command,
@@ -1,11 +1,12 @@
1
1
  import { getToolInputPath } from "#src/access-intent/tool-input-path";
2
2
  import type { PathNormalizer } from "#src/path-normalizer";
3
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
5
+ import { buildExternalDirectoryAskPayload } from "#src/presentation/path-ask-payload";
4
6
  import { SessionApproval } from "#src/session-approval";
5
7
  import { deriveApprovalPattern } from "#src/session-rules";
6
8
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
7
9
  import type { GateResult } from "./descriptor";
8
- import { formatExternalDirectoryAskPrompt } from "./external-directory-messages";
9
10
  import { resolveExternalDirectoryPolicy } from "./external-directory-policy";
10
11
  import { accessFactsFromPath } from "./helpers";
11
12
  import type { ToolCallContext } from "./types";
@@ -69,13 +70,6 @@ export function describeExternalDirectoryGate(
69
70
 
70
71
  // ── Build descriptor for permission check ───────────────────────────────
71
72
  const resolvedAlias = accessPath.resolvedAlias();
72
- const extDirMessage = formatExternalDirectoryAskPrompt(
73
- tcc.toolName,
74
- externalDirectoryPath,
75
- resolvedAlias,
76
- tcc.cwd,
77
- tcc.agentName ?? undefined,
78
- );
79
73
 
80
74
  // The runner consumes this preCheck and skips its own resolve.
81
75
  const preCheck = resolveExternalDirectoryPolicy(
@@ -85,6 +79,16 @@ export function describeExternalDirectoryGate(
85
79
  );
86
80
  const pattern = deriveApprovalPattern(accessPath.value());
87
81
 
82
+ const payload = buildExternalDirectoryAskPayload({
83
+ toolName: tcc.toolName,
84
+ pathValue: externalDirectoryPath,
85
+ resolvedPath: resolvedAlias,
86
+ cwd: tcc.cwd,
87
+ agentName: tcc.agentName,
88
+ matchedPattern: preCheck.matchedPattern,
89
+ });
90
+ const extDirMessage = renderLegacyMessage(payload);
91
+
88
92
  return {
89
93
  surface: "external_directory",
90
94
  input: {},
@@ -102,6 +106,7 @@ export function describeExternalDirectoryGate(
102
106
  source: "tool_call",
103
107
  agentName: tcc.agentName,
104
108
  message: extDirMessage,
109
+ payload,
105
110
  toolCallId: tcc.toolCallId,
106
111
  toolName: tcc.toolName,
107
112
  path: externalDirectoryPath,
@@ -1,6 +1,8 @@
1
1
  import { getToolInputPath } from "#src/access-intent/tool-input-path";
2
2
  import type { PathNormalizer } from "#src/path-normalizer";
3
3
  import type { ScopedPermissionResolver } from "#src/permission-resolver";
4
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
5
+ import { buildPathAskPayload } from "#src/presentation/path-ask-payload";
4
6
  import { SessionApproval } from "#src/session-approval";
5
7
  import { deriveApprovalPattern } from "#src/session-rules";
6
8
  import type { ToolAccessExtractorLookup } from "#src/tool-access-extractor-registry";
@@ -47,6 +49,13 @@ export function describePathGate(
47
49
  // the policy values a later call produces.
48
50
  const pattern = deriveApprovalPattern(accessPath.value());
49
51
 
52
+ const payload = buildPathAskPayload({
53
+ toolName: tcc.toolName,
54
+ pathValue: filePath,
55
+ agentName: tcc.agentName,
56
+ matchedPattern: check.matchedPattern,
57
+ });
58
+
50
59
  const descriptor: GateDescriptor = {
51
60
  surface: "path",
52
61
  input: { path: filePath },
@@ -60,11 +69,8 @@ export function describePathGate(
60
69
  promptDetails: {
61
70
  source: "tool_call",
62
71
  agentName: tcc.agentName,
63
- message: formatPathAskPrompt(
64
- tcc.toolName,
65
- filePath,
66
- tcc.agentName ?? undefined,
67
- ),
72
+ message: renderLegacyMessage(payload),
73
+ payload,
68
74
  toolCallId: tcc.toolCallId,
69
75
  toolName: tcc.toolName,
70
76
  path: filePath,
@@ -86,12 +92,3 @@ export function describePathGate(
86
92
 
87
93
  return descriptor;
88
94
  }
89
-
90
- export function formatPathAskPrompt(
91
- toolName: string,
92
- pathValue: string,
93
- agentName?: string,
94
- ): string {
95
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
96
- return `${subject} requested tool '${toolName}' for path '${pathValue}'. Allow this path access?`;
97
- }
@@ -1,4 +1,5 @@
1
- import { formatSkillAskPrompt } from "#src/permission-prompts";
1
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
2
+ import { buildSkillAskPayload } from "#src/presentation/skill-ask-payload";
2
3
  import type { PermissionCheckResult } from "#src/types";
3
4
  import type { GateDescriptor } from "./descriptor";
4
5
  import { accessFactsFromValue } from "./helpers";
@@ -15,7 +16,8 @@ export function describeSkillInputGate(
15
16
  agentName: string | null,
16
17
  preCheck: PermissionCheckResult,
17
18
  ): GateDescriptor {
18
- const message = formatSkillAskPrompt(skillName, agentName ?? undefined);
19
+ const payload = buildSkillAskPayload(skillName, agentName);
20
+ const message = renderLegacyMessage(payload);
19
21
  return {
20
22
  surface: "skill",
21
23
  input: { name: skillName },
@@ -29,6 +31,7 @@ export function describeSkillInputGate(
29
31
  source: "skill_input",
30
32
  agentName,
31
33
  message,
34
+ payload,
32
35
  skillName,
33
36
  accessIntent: accessFactsFromValue("skill", skillName),
34
37
  },
@@ -1,5 +1,6 @@
1
1
  import type { PathNormalizer } from "#src/path-normalizer";
2
- import { formatSkillPathAskPrompt } from "#src/permission-prompts";
2
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
3
+ import { buildSkillPathAskPayload } from "#src/presentation/skill-ask-payload";
3
4
  import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
4
5
  import { findSkillPathMatch } from "#src/skill-prompt-sanitizer";
5
6
  import { toRecord } from "#src/value-guards";
@@ -42,11 +43,8 @@ export function describeSkillReadGate(
42
43
  return null;
43
44
  }
44
45
 
45
- const skillReadMessage = formatSkillPathAskPrompt(
46
- matchedSkill,
47
- path,
48
- tcc.agentName ?? undefined,
49
- );
46
+ const payload = buildSkillPathAskPayload(matchedSkill, path, tcc.agentName);
47
+ const skillReadMessage = renderLegacyMessage(payload);
50
48
 
51
49
  return {
52
50
  surface: "skill",
@@ -61,6 +59,7 @@ export function describeSkillReadGate(
61
59
  source: "skill_read",
62
60
  agentName: tcc.agentName,
63
61
  message: skillReadMessage,
62
+ payload,
64
63
  toolCallId: tcc.toolCallId,
65
64
  toolName: tcc.toolName,
66
65
  skillName: matchedSkill.name,
@@ -6,7 +6,8 @@ import {
6
6
  type ShellInvocation,
7
7
  } from "#src/access-intent/tool-kind";
8
8
  import { suggestSessionPattern } from "#src/pattern-suggest";
9
- import { formatAskPrompt } from "#src/permission-prompts";
9
+ import { renderLegacyMessage } from "#src/presentation/legacy-message";
10
+ import { buildToolAskPayload } from "#src/presentation/tool-ask-payload";
10
11
  import { SessionApproval } from "#src/session-approval";
11
12
  import type { ToolPreviewFormatter } from "#src/tool-preview-formatter";
12
13
  import type { PermissionCheckResult } from "#src/types";
@@ -72,12 +73,15 @@ export function describeToolGate(
72
73
  deriveSuggestionValue(gateSurface, check, accessPath),
73
74
  );
74
75
 
75
- const askMessage = formatAskPrompt(
76
+ const payload = buildToolAskPayload({
76
77
  check,
77
- tcc.agentName ?? undefined,
78
- tcc.input,
78
+ agentName: tcc.agentName,
79
+ surface: gateSurface,
80
+ invokedToolName: tcc.toolName,
81
+ input: tcc.input,
79
82
  formatter,
80
- );
83
+ });
84
+ const askMessage = renderLegacyMessage(payload);
81
85
 
82
86
  const decisionValue = deriveDecisionValue(
83
87
  gateSurface,
@@ -108,6 +112,7 @@ export function describeToolGate(
108
112
  source: "tool_call",
109
113
  agentName: tcc.agentName,
110
114
  message: askMessage,
115
+ payload,
111
116
  toolCallId: tcc.toolCallId,
112
117
  toolName: tcc.toolName,
113
118
  sessionLabel: suggestion.label,
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ import { PermissionManager } from "./permission-manager";
38
38
  import { PermissionResolver } from "./permission-resolver";
39
39
  import { PermissionSession } from "./permission-session";
40
40
  import { LocalPermissionsService } from "./permissions-service";
41
+ import { resolveRenderBudget } from "./presentation/dialog-renderer";
41
42
  import { PermissionServiceLifecycle } from "./service-lifecycle";
42
43
  import { PermissionSessionLogger } from "./session-logger";
43
44
  import { SessionRules } from "./session-rules";
@@ -115,6 +116,7 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
115
116
  events: pi.events,
116
117
  getPromptPreferences: () => ({
117
118
  doublePressToConfirm: configStore.current().doublePressToConfirm,
119
+ budget: resolveRenderBudget(configStore.current()),
118
120
  }),
119
121
  requestPermissionDecision,
120
122
  forwardingDir: paths.forwardingDir,
@@ -1,13 +1,8 @@
1
- import { classifyToolKind, isMcpCheck } from "./access-intent/tool-kind";
2
- import { matchQualifier } from "./denial-messages";
3
- import type { SkillPromptEntry } from "./skill-prompt-sanitizer";
4
- import type { ToolPreviewFormatter } from "./tool-preview-formatter";
5
- import type { PermissionCheckResult } from "./types";
6
- import { getNonEmptyString, toRecord } from "./value-guards";
1
+ import { classifyToolKind } from "./access-intent/tool-kind";
7
2
 
8
- // NOTE: formatDenyReason, formatUserDeniedReason, and
9
- // formatPermissionHardStopHint have been moved to denial-messages.ts.
10
- // This module retains only pre-check messages and user-facing ask prompts.
3
+ // NOTE: the ask prompts are now payload builders under src/presentation/;
4
+ // denial text lives in denial-messages.ts. This module retains only the
5
+ // pre-check reasons, which are agent-facing rather than user-facing.
11
6
 
12
7
  export function formatMissingToolNameReason(): string {
13
8
  return "Tool call was blocked because no tool name was provided. Use a registered tool name from pi.getAllTools().";
@@ -29,66 +24,3 @@ export function formatUnknownToolReason(
29
24
 
30
25
  return `Tool '${toolName}' is not registered in this runtime and was blocked before permission checks.${mcpHint} Registered tools: ${availableList}.`;
31
26
  }
32
-
33
- export function formatAskPrompt(
34
- result: PermissionCheckResult,
35
- agentName?: string,
36
- input?: unknown,
37
- formatter?: ToolPreviewFormatter,
38
- ): string {
39
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
40
-
41
- if (classifyToolKind(result.toolName) === "bash") {
42
- const subCommand = result.command ?? "";
43
- const qualifier = matchQualifier(
44
- result.matchedPattern,
45
- result.commandContext,
46
- );
47
- const qualifierInfo = qualifier ? ` ${qualifier}` : "";
48
- const fullCommand = getNonEmptyString(toRecord(input).command);
49
- const fullCommandInfo =
50
- fullCommand && fullCommand !== subCommand
51
- ? ` (full command: '${fullCommand}')`
52
- : "";
53
- return `${subject} requested bash command '${subCommand}'${qualifierInfo}${fullCommandInfo}. Allow this command?`;
54
- }
55
-
56
- if (isMcpCheck(result) && result.target) {
57
- const patternInfo = result.matchedPattern
58
- ? ` (matched '${result.matchedPattern}')`
59
- : "";
60
- const mcpPreview = formatter
61
- ? formatter.formatToolInputForPrompt("mcp", input)
62
- : "";
63
- const previewSuffix = mcpPreview ? ` ${mcpPreview}` : "";
64
- return `${subject} requested MCP target '${result.target}'${patternInfo}${previewSuffix}. Allow this call?`;
65
- }
66
-
67
- const patternInfo = result.matchedPattern
68
- ? ` (matched '${result.matchedPattern}')`
69
- : "";
70
- const inputPreview = formatter
71
- ? formatter.formatToolInputForPrompt(result.toolName, input)
72
- : "";
73
- const inputSuffix = inputPreview ? ` ${inputPreview}` : "";
74
- return `${subject} requested tool '${result.toolName}'${patternInfo}${inputSuffix}. Allow this call?`;
75
- }
76
-
77
- export function formatSkillAskPrompt(
78
- skillName: string,
79
- agentName?: string,
80
- ): string {
81
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
82
- return `${subject} requested skill '${skillName}'. Allow loading this skill?`;
83
- }
84
-
85
- export function formatSkillPathAskPrompt(
86
- skill: SkillPromptEntry,
87
- readPath: string,
88
- agentName?: string,
89
- ): string {
90
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
91
- return `${subject} requested access to skill '${skill.name}' via '${readPath}'. Allow this read?`;
92
- }
93
-
94
- // formatSkillPathDenyReason has been moved to denial-messages.ts.