@gotgenes/pi-permission-system 6.0.2 → 7.0.1

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 CHANGED
@@ -5,6 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.0.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v7.0.0...pi-permission-system-v7.0.1) (2026-05-21)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * **retro:** add retro notes for issue [#78](https://github.com/gotgenes/pi-packages/issues/78) ([594d3e1](https://github.com/gotgenes/pi-packages/commit/594d3e13512facbf4b6241b9a394aca8e59c0707))
14
+ * **retro:** add retro notes for issue [#78](https://github.com/gotgenes/pi-packages/issues/78) ([5f93117](https://github.com/gotgenes/pi-packages/commit/5f93117969524af86b28db979649032e860fc72e))
15
+
16
+ ## [7.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v6.0.2...pi-permission-system-v7.0.0) (2026-05-21)
17
+
18
+
19
+ ### ⚠ BREAKING CHANGES
20
+
21
+ * GateDescriptor.messages has been replaced by GateDescriptor.denialContext. Any code constructing a GateDescriptor must provide a DenialContext instead of pre-formatted message strings.
22
+
23
+ ### Features
24
+
25
+ * add centralized denial message formatter ([#78](https://github.com/gotgenes/pi-packages/issues/78)) ([99f2d36](https://github.com/gotgenes/pi-packages/commit/99f2d362d669dd4d6a6a18365fecd42dd0d77eaa))
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * remove broken relative links in archived plan 0042 ([07bcca4](https://github.com/gotgenes/pi-packages/commit/07bcca49d506f5726ca96a4b9128b77635e84b7e))
31
+
32
+
33
+ ### Documentation
34
+
35
+ * add README to archived plans directory ([c3fcb9f](https://github.com/gotgenes/pi-packages/commit/c3fcb9f7f669c1d25207225e807970eea1c9adc8))
36
+ * archive pre-monorepo plans, plan soften denial messages ([#78](https://github.com/gotgenes/pi-packages/issues/78)) ([7709f8f](https://github.com/gotgenes/pi-packages/commit/7709f8f85a0b7c002a943a222f6005d6069245c1))
37
+
38
+
39
+ ### Code Refactoring
40
+
41
+ * remove messages from GateDescriptor ([#78](https://github.com/gotgenes/pi-packages/issues/78)) ([d3cae38](https://github.com/gotgenes/pi-packages/commit/d3cae387ea57c23ede6df27156d1a026aa6b58f2))
42
+
8
43
  ## [6.0.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v6.0.1...pi-permission-system-v6.0.2) (2026-05-20)
9
44
 
10
45
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-permission-system",
3
- "version": "6.0.2",
3
+ "version": "7.0.1",
4
4
  "description": "Permission enforcement extension for the Pi coding agent.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,12 +57,12 @@
57
57
  },
58
58
  "devDependencies": {
59
59
  "@biomejs/biome": "^2.4.14",
60
- "@earendil-works/pi-coding-agent": ">=0.75.0",
61
- "@earendil-works/pi-tui": ">=0.75.0",
60
+ "@earendil-works/pi-coding-agent": "0.75.4",
61
+ "@earendil-works/pi-tui": "0.75.4",
62
62
  "@types/node": "^22.15.3",
63
+ "rumdl": "^0.1.93",
63
64
  "typescript": "^6.0.3",
64
- "vitest": "^4.1.5",
65
- "rumdl": "^0.1.93"
65
+ "vitest": "^4.1.5"
66
66
  },
67
67
  "dependencies": {
68
68
  "tree-sitter-bash": "^0.25.1",
@@ -0,0 +1,179 @@
1
+ import { EXTENSION_ID } from "./extension-config";
2
+ import type { PermissionCheckResult } from "./types";
3
+
4
+ // ── Extension attribution tag ──────────────────────────────────────────────
5
+
6
+ export const EXTENSION_TAG = `[${EXTENSION_ID}]`;
7
+
8
+ // ── Denial context discriminated union ─────────────────────────────────────
9
+
10
+ export type DenialContext =
11
+ | {
12
+ kind: "tool";
13
+ check: PermissionCheckResult;
14
+ agentName?: string;
15
+ input?: unknown;
16
+ }
17
+ | {
18
+ kind: "path";
19
+ toolName: string;
20
+ pathValue: string;
21
+ agentName?: string;
22
+ }
23
+ | {
24
+ kind: "external_directory";
25
+ toolName: string;
26
+ pathValue: string;
27
+ cwd: string;
28
+ agentName?: string;
29
+ }
30
+ | {
31
+ kind: "bash_external_directory";
32
+ command: string;
33
+ externalPaths: string[];
34
+ cwd: string;
35
+ agentName?: string;
36
+ }
37
+ | {
38
+ kind: "bash_path";
39
+ command: string;
40
+ pathValue: string;
41
+ agentName?: string;
42
+ }
43
+ | {
44
+ kind: "skill_read";
45
+ skillName: string;
46
+ readPath: string;
47
+ agentName?: string;
48
+ };
49
+
50
+ // ── Public formatter API ───────────────────────────────────────────────────
51
+
52
+ /** Format the block reason when permission policy denies an operation. */
53
+ export function formatDenyReason(ctx: DenialContext): string {
54
+ return `${EXTENSION_TAG} ${buildDenyBody(ctx)}`;
55
+ }
56
+
57
+ /** Format the block reason when no interactive UI is available to prompt. */
58
+ export function formatUnavailableReason(ctx: DenialContext): string {
59
+ return `${EXTENSION_TAG} ${buildUnavailableBody(ctx)}`;
60
+ }
61
+
62
+ /** Format the block reason when the user denies at an interactive prompt. */
63
+ export function formatUserDeniedReason(
64
+ ctx: DenialContext,
65
+ denialReason?: string,
66
+ ): string {
67
+ return `${EXTENSION_TAG} ${buildUserDeniedBody(ctx, denialReason)}`;
68
+ }
69
+
70
+ // ── Private body builders ──────────────────────────────────────────────────
71
+
72
+ function subject(agentName?: string): string {
73
+ return agentName ? `Agent '${agentName}'` : "Current agent";
74
+ }
75
+
76
+ function reasonSuffix(denialReason?: string): string {
77
+ return denialReason ? ` Reason: ${denialReason}.` : "";
78
+ }
79
+
80
+ function buildDenyBody(ctx: DenialContext): string {
81
+ switch (ctx.kind) {
82
+ case "tool":
83
+ return buildToolDenyBody(ctx);
84
+ case "path":
85
+ return `${subject(ctx.agentName)} is not permitted to access path '${ctx.pathValue}' via tool '${ctx.toolName}'.`;
86
+ case "external_directory":
87
+ return `${subject(ctx.agentName)} is not permitted to run tool '${ctx.toolName}' for path '${ctx.pathValue}' outside working directory '${ctx.cwd}'.`;
88
+ case "bash_external_directory":
89
+ return `${subject(ctx.agentName)} is not permitted to run bash command '${ctx.command}' which references path(s) outside working directory '${ctx.cwd}': ${ctx.externalPaths.join(", ")}.`;
90
+ case "bash_path":
91
+ return `${subject(ctx.agentName)} is not permitted to access path '${ctx.pathValue}' via tool 'bash'.`;
92
+ case "skill_read":
93
+ return `${subject(ctx.agentName)} is not permitted to access skill '${ctx.skillName}' via '${ctx.readPath}'.`;
94
+ }
95
+ }
96
+
97
+ function buildToolDenyBody(
98
+ ctx: Extract<DenialContext, { kind: "tool" }>,
99
+ ): string {
100
+ const parts: string[] = [];
101
+ const { check, agentName } = ctx;
102
+
103
+ if (agentName) {
104
+ parts.push(`Agent '${agentName}'`);
105
+ }
106
+
107
+ if (isMcpCheck(check)) {
108
+ parts.push(`is not permitted to run MCP target '${check.target}'`);
109
+ } else {
110
+ parts.push(`is not permitted to run '${check.toolName}'`);
111
+ }
112
+
113
+ if (check.command) {
114
+ parts.push(`command '${check.command}'`);
115
+ }
116
+
117
+ if (check.matchedPattern) {
118
+ parts.push(`(matched '${check.matchedPattern}')`);
119
+ }
120
+
121
+ return `${parts.join(" ")}.`;
122
+ }
123
+
124
+ function buildUnavailableBody(ctx: DenialContext): string {
125
+ switch (ctx.kind) {
126
+ case "tool": {
127
+ const { check } = ctx;
128
+ if (check.toolName === "bash" && check.command) {
129
+ return `Running bash command '${check.command}' requires approval, but no interactive UI is available.`;
130
+ }
131
+ if (isMcpCheck(check)) {
132
+ return "Using tool 'mcp' requires approval, but no interactive UI is available.";
133
+ }
134
+ return `Using tool '${check.toolName}' requires approval, but no interactive UI is available.`;
135
+ }
136
+ case "path":
137
+ return `Accessing '${ctx.pathValue}' requires approval, but no interactive UI is available.`;
138
+ case "external_directory":
139
+ return `Accessing '${ctx.pathValue}' outside the working directory requires approval, but no interactive UI is available.`;
140
+ case "bash_external_directory":
141
+ return `Bash command '${ctx.command}' references path(s) outside the working directory and requires approval, but no interactive UI is available.`;
142
+ case "bash_path":
143
+ return `Bash command '${ctx.command}' accesses path '${ctx.pathValue}' which requires approval, but no interactive UI is available.`;
144
+ case "skill_read":
145
+ return `Accessing skill '${ctx.skillName}' requires approval, but no interactive UI is available.`;
146
+ }
147
+ }
148
+
149
+ function buildUserDeniedBody(
150
+ ctx: DenialContext,
151
+ denialReason?: string,
152
+ ): string {
153
+ switch (ctx.kind) {
154
+ case "tool": {
155
+ const { check } = ctx;
156
+ if (isMcpCheck(check)) {
157
+ return `User denied MCP target '${check.target}'.${reasonSuffix(denialReason)}`;
158
+ }
159
+ if (check.toolName === "bash" && check.command) {
160
+ return `User denied bash command '${check.command}'.${reasonSuffix(denialReason)}`;
161
+ }
162
+ return `User denied tool '${check.toolName}'.${reasonSuffix(denialReason)}`;
163
+ }
164
+ case "path":
165
+ return `User denied access to path '${ctx.pathValue}'.${reasonSuffix(denialReason)}`;
166
+ case "external_directory":
167
+ return `User denied external directory access for tool '${ctx.toolName}' path '${ctx.pathValue}'.${reasonSuffix(denialReason)}`;
168
+ case "bash_external_directory":
169
+ return `User denied external directory access for bash command '${ctx.command}'.${reasonSuffix(denialReason)}`;
170
+ case "bash_path":
171
+ return `User denied path access for bash command '${ctx.command}' (path '${ctx.pathValue}').${reasonSuffix(denialReason)}`;
172
+ case "skill_read":
173
+ return `User denied access to skill '${ctx.skillName}'.${reasonSuffix(denialReason)}`;
174
+ }
175
+ }
176
+
177
+ function isMcpCheck(check: PermissionCheckResult): boolean {
178
+ return (check.source === "mcp" || check.toolName === "mcp") && !!check.target;
179
+ }
@@ -4,11 +4,7 @@ import { deriveApprovalPattern } from "../../session-rules";
4
4
  import type { PermissionCheckResult } from "../../types";
5
5
  import { extractExternalPathsFromBashCommand } from "./bash-path-extractor";
6
6
  import type { GateResult } from "./descriptor";
7
- import {
8
- formatBashExternalDirectoryAskPrompt,
9
- formatBashExternalDirectoryDenyReason,
10
- formatExternalDirectoryHardStopHint,
11
- } from "./external-directory-messages";
7
+ import { formatBashExternalDirectoryAskPrompt } from "./external-directory-messages";
12
8
  import type { ToolCallContext } from "./types";
13
9
 
14
10
  /** Function type for checkPermission used by the descriptor factory. */
@@ -92,20 +88,12 @@ export async function describeBashExternalDirectoryGate(
92
88
  return {
93
89
  surface: "external_directory",
94
90
  input: {},
95
- messages: {
96
- denyReason: formatBashExternalDirectoryDenyReason(
97
- command,
98
- uncoveredPaths,
99
- tcc.cwd,
100
- tcc.agentName ?? undefined,
101
- ),
102
- unavailableReason: `Bash command '${command}' references path(s) outside the working directory and requires approval, but no interactive UI is available.`,
103
- userDeniedReason: (decision) => {
104
- const reasonSuffix = decision.denialReason
105
- ? ` Reason: ${decision.denialReason}.`
106
- : "";
107
- return `User denied external directory access for bash command '${command}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
108
- },
91
+ denialContext: {
92
+ kind: "bash_external_directory",
93
+ command,
94
+ externalPaths: uncoveredPaths,
95
+ cwd: tcc.cwd,
96
+ agentName: tcc.agentName ?? undefined,
109
97
  },
110
98
  sessionApproval: {
111
99
  surface: "external_directory",
@@ -4,7 +4,7 @@ import { deriveApprovalPattern } from "../../session-rules";
4
4
  import type { PermissionCheckResult } from "../../types";
5
5
  import { extractTokensForPathRules } from "./bash-path-extractor";
6
6
  import type { GateResult } from "./descriptor";
7
- import { formatPathAskPrompt, formatPathDenyReason } from "./path";
7
+ import { formatPathAskPrompt } from "./path";
8
8
  import type { ToolCallContext } from "./types";
9
9
 
10
10
  /** Function type for checkPermission used by the descriptor factory. */
@@ -111,19 +111,11 @@ export async function describeBashPathGate(
111
111
  return {
112
112
  surface: "path",
113
113
  input: { path: worstToken },
114
- messages: {
115
- denyReason: formatPathDenyReason(
116
- tcc.toolName,
117
- worstToken,
118
- tcc.agentName ?? undefined,
119
- ),
120
- unavailableReason: `Bash command '${command}' accesses path '${worstToken}' which requires approval, but no interactive UI is available.`,
121
- userDeniedReason: (decision) => {
122
- const reasonSuffix = decision.denialReason
123
- ? ` Reason: ${decision.denialReason}.`
124
- : "";
125
- return `User denied path access for bash command '${command}' (path '${worstToken}').${reasonSuffix} Hard stop: this path permission denial is policy-enforced. Do not retry this path, do not attempt a filesystem bypass, and report the block to the user.`;
126
- },
114
+ denialContext: {
115
+ kind: "bash_path",
116
+ command,
117
+ pathValue: worstToken,
118
+ agentName: tcc.agentName ?? undefined,
127
119
  },
128
120
  sessionApproval: {
129
121
  surface: "path",
@@ -1,8 +1,6 @@
1
+ import type { DenialContext } from "../../denial-messages";
1
2
  import type { PermissionPromptDecision } from "../../permission-dialog";
2
- import type {
3
- PermissionDecisionEvent,
4
- PermissionDecisionResolution,
5
- } from "../../permission-events";
3
+ import type { PermissionDecisionEvent } from "../../permission-events";
6
4
  import type { PromptPermissionDetails } from "../../permission-prompter";
7
5
  import type { Rule } from "../../rule";
8
6
  import type { PermissionCheckResult, PermissionState } from "../../types";
@@ -21,12 +19,8 @@ export interface GateDescriptor {
21
19
  surface: string;
22
20
  /** Input passed to checkPermission. */
23
21
  input: unknown;
24
- /** Message strings/factories for each outcome. */
25
- messages: {
26
- denyReason: string;
27
- unavailableReason: string;
28
- userDeniedReason: (decision: PermissionPromptDecision) => string;
29
- };
22
+ /** Structured denial context the runner formats messages from this. */
23
+ denialContext: DenialContext;
30
24
  /**
31
25
  * Session-approval suggestion for "for this session" option.
32
26
  * Single pattern or multiple patterns (bash external-directory gate).
@@ -1,7 +1,3 @@
1
- export function formatExternalDirectoryHardStopHint(): string {
2
- return "Hard stop: this external directory permission denial is policy-enforced. Do not retry this path, do not attempt a filesystem bypass, and report the block to the user.";
3
- }
4
-
5
1
  export function formatExternalDirectoryAskPrompt(
6
2
  toolName: string,
7
3
  pathValue: string,
@@ -12,25 +8,6 @@ export function formatExternalDirectoryAskPrompt(
12
8
  return `${subject} requested tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. Allow this external directory access?`;
13
9
  }
14
10
 
15
- export function formatExternalDirectoryDenyReason(
16
- toolName: string,
17
- pathValue: string,
18
- cwd: string,
19
- agentName?: string,
20
- ): string {
21
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
22
- return `${subject} is not permitted to run tool '${toolName}' for path '${pathValue}' outside working directory '${cwd}'. ${formatExternalDirectoryHardStopHint()}`;
23
- }
24
-
25
- export function formatExternalDirectoryUserDeniedReason(
26
- toolName: string,
27
- pathValue: string,
28
- denialReason?: string,
29
- ): string {
30
- const reasonSuffix = denialReason ? ` Reason: ${denialReason}.` : "";
31
- return `User denied external directory access for tool '${toolName}' path '${pathValue}'.${reasonSuffix} ${formatExternalDirectoryHardStopHint()}`;
32
- }
33
-
34
11
  export function formatBashExternalDirectoryAskPrompt(
35
12
  command: string,
36
13
  externalPaths: string[],
@@ -41,14 +18,3 @@ export function formatBashExternalDirectoryAskPrompt(
41
18
  const pathList = externalPaths.join(", ");
42
19
  return `${subject} requested bash command '${command}' which references path(s) outside working directory '${cwd}': ${pathList}. Allow this external directory access?`;
43
20
  }
44
-
45
- export function formatBashExternalDirectoryDenyReason(
46
- command: string,
47
- externalPaths: string[],
48
- cwd: string,
49
- agentName?: string,
50
- ): string {
51
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
52
- const pathList = externalPaths.join(", ");
53
- return `${subject} is not permitted to run bash command '${command}' which references path(s) outside working directory '${cwd}': ${pathList}. ${formatExternalDirectoryHardStopHint()}`;
54
- }
@@ -6,11 +6,7 @@ import {
6
6
  } from "../../path-utils";
7
7
  import { deriveApprovalPattern } from "../../session-rules";
8
8
  import type { GateResult } from "./descriptor";
9
- import {
10
- formatExternalDirectoryAskPrompt,
11
- formatExternalDirectoryDenyReason,
12
- formatExternalDirectoryUserDeniedReason,
13
- } from "./external-directory-messages";
9
+ import { formatExternalDirectoryAskPrompt } from "./external-directory-messages";
14
10
  import type { ToolCallContext } from "./types";
15
11
 
16
12
  /**
@@ -80,20 +76,12 @@ export function describeExternalDirectoryGate(
80
76
  return {
81
77
  surface: "external_directory",
82
78
  input: { path: normalizedExtPath },
83
- messages: {
84
- denyReason: formatExternalDirectoryDenyReason(
85
- tcc.toolName,
86
- externalDirectoryPath,
87
- tcc.cwd,
88
- tcc.agentName ?? undefined,
89
- ),
90
- unavailableReason: `Accessing '${externalDirectoryPath}' outside the working directory requires approval, but no interactive UI is available.`,
91
- userDeniedReason: (decision) =>
92
- formatExternalDirectoryUserDeniedReason(
93
- tcc.toolName,
94
- externalDirectoryPath,
95
- decision.denialReason,
96
- ),
79
+ denialContext: {
80
+ kind: "external_directory",
81
+ toolName: tcc.toolName,
82
+ pathValue: externalDirectoryPath,
83
+ cwd: tcc.cwd,
84
+ agentName: tcc.agentName ?? undefined,
97
85
  },
98
86
  sessionApproval: {
99
87
  surface: "external_directory",
@@ -49,19 +49,11 @@ export function describePathGate(
49
49
  const descriptor: GateDescriptor = {
50
50
  surface: "path",
51
51
  input: { path: filePath },
52
- messages: {
53
- denyReason: formatPathDenyReason(
54
- tcc.toolName,
55
- filePath,
56
- tcc.agentName ?? undefined,
57
- ),
58
- unavailableReason: `Accessing '${filePath}' requires approval, but no interactive UI is available.`,
59
- userDeniedReason: (decision) => {
60
- const reasonSuffix = decision.denialReason
61
- ? ` Reason: ${decision.denialReason}.`
62
- : "";
63
- return `User denied access to path '${filePath}'.${reasonSuffix} Hard stop: this path permission denial is policy-enforced. Do not retry this path, do not attempt a filesystem bypass, and report the block to the user.`;
64
- },
52
+ denialContext: {
53
+ kind: "path",
54
+ toolName: tcc.toolName,
55
+ pathValue: filePath,
56
+ agentName: tcc.agentName ?? undefined,
65
57
  },
66
58
  sessionApproval: {
67
59
  surface: "path",
@@ -96,15 +88,6 @@ export function describePathGate(
96
88
  return descriptor;
97
89
  }
98
90
 
99
- export function formatPathDenyReason(
100
- toolName: string,
101
- pathValue: string,
102
- agentName?: string,
103
- ): string {
104
- const subject = agentName ? `Agent '${agentName}'` : "Current agent";
105
- return `${subject} is not permitted to access path '${pathValue}' via tool '${toolName}'. Hard stop: this path permission denial is policy-enforced. Do not retry this path, do not attempt a filesystem bypass, and report the block to the user.`;
106
- }
107
-
108
91
  export function formatPathAskPrompt(
109
92
  toolName: string,
110
93
  pathValue: string,
@@ -1,3 +1,8 @@
1
+ import {
2
+ formatDenyReason,
3
+ formatUnavailableReason,
4
+ formatUserDeniedReason,
5
+ } from "../../denial-messages";
1
6
  import type { PermissionPromptDecision } from "../../permission-dialog";
2
7
  import { applyPermissionGate } from "../../permission-gate";
3
8
  import type { PermissionCheckResult } from "../../types";
@@ -81,6 +86,14 @@ export async function runGateCheck(
81
86
  : undefined
82
87
  : undefined;
83
88
 
89
+ // Construct messages from the centralized formatter.
90
+ const messages = {
91
+ denyReason: formatDenyReason(descriptor.denialContext),
92
+ unavailableReason: formatUnavailableReason(descriptor.denialContext),
93
+ userDeniedReason: (decision: PermissionPromptDecision) =>
94
+ formatUserDeniedReason(descriptor.denialContext, decision.denialReason),
95
+ };
96
+
84
97
  let autoApproved = false;
85
98
  const gateResult = await applyPermissionGate({
86
99
  state: check.state,
@@ -96,7 +109,7 @@ export async function runGateCheck(
96
109
  },
97
110
  writeLog: deps.writeReviewLog,
98
111
  logContext: { ...descriptor.logContext, agentName },
99
- messages: descriptor.messages,
112
+ messages,
100
113
  });
101
114
 
102
115
  // 4. Determine whether session approval was granted
@@ -1,9 +1,6 @@
1
1
  import { toRecord } from "../../common";
2
2
  import { normalizePathForComparison } from "../../path-utils";
3
- import {
4
- formatSkillPathAskPrompt,
5
- formatSkillPathDenyReason,
6
- } from "../../permission-prompts";
3
+ import { formatSkillPathAskPrompt } from "../../permission-prompts";
7
4
  import type { SkillPromptEntry } from "../../skill-prompt-sanitizer";
8
5
  import { findSkillPathMatch } from "../../skill-prompt-sanitizer";
9
6
  import type { GateDescriptor } from "./descriptor";
@@ -55,19 +52,11 @@ export function describeSkillReadGate(
55
52
  return {
56
53
  surface: "skill",
57
54
  input: { name: matchedSkill.name },
58
- messages: {
59
- denyReason: formatSkillPathDenyReason(
60
- matchedSkill,
61
- path,
62
- tcc.agentName ?? undefined,
63
- ),
64
- unavailableReason: `Accessing skill '${matchedSkill.name}' requires approval, but no interactive UI is available.`,
65
- userDeniedReason: (decision) => {
66
- const denialReason = decision.denialReason
67
- ? ` Reason: ${decision.denialReason}.`
68
- : "";
69
- return `User denied access to skill '${matchedSkill.name}'.${denialReason}`;
70
- },
55
+ denialContext: {
56
+ kind: "skill_read",
57
+ skillName: matchedSkill.name,
58
+ readPath: path,
59
+ agentName: tcc.agentName ?? undefined,
71
60
  },
72
61
  promptDetails: {
73
62
  source: "skill_read",
@@ -1,10 +1,6 @@
1
1
  import { getPathBearingToolPath, PATH_BEARING_TOOLS } from "../../path-utils";
2
2
  import { suggestSessionPattern } from "../../pattern-suggest";
3
- import {
4
- formatAskPrompt,
5
- formatDenyReason,
6
- formatUserDeniedReason,
7
- } from "../../permission-prompts";
3
+ import { formatAskPrompt } from "../../permission-prompts";
8
4
  import { getPermissionLogContext } from "../../tool-input-preview";
9
5
  import type { PermissionCheckResult } from "../../types";
10
6
  import type { GateDescriptor } from "./descriptor";
@@ -48,18 +44,6 @@ export function describeToolGate(
48
44
  deriveSuggestionValue(tcc, check),
49
45
  );
50
46
 
51
- // Build the unavailable-reason message. Bash gets the command embedded.
52
- const inputCommand =
53
- tcc.toolName === "bash" &&
54
- typeof (tcc.input as Record<string, unknown>)?.command === "string"
55
- ? ((tcc.input as Record<string, unknown>).command as string)
56
- : null;
57
- const unavailableReason = inputCommand
58
- ? `Running bash command '${inputCommand}' requires approval, but no interactive UI is available.`
59
- : tcc.toolName === "mcp"
60
- ? "Using tool 'mcp' requires approval, but no interactive UI is available."
61
- : `Using tool '${tcc.toolName}' requires approval, but no interactive UI is available.`;
62
-
63
47
  const askMessage = formatAskPrompt(
64
48
  check,
65
49
  tcc.agentName ?? undefined,
@@ -69,11 +53,11 @@ export function describeToolGate(
69
53
  return {
70
54
  surface: tcc.toolName,
71
55
  input: tcc.input,
72
- messages: {
73
- denyReason: formatDenyReason(check, tcc.agentName ?? undefined),
74
- unavailableReason,
75
- userDeniedReason: (decision) =>
76
- formatUserDeniedReason(check, decision.denialReason),
56
+ denialContext: {
57
+ kind: "tool",
58
+ check,
59
+ agentName: tcc.agentName ?? undefined,
60
+ input: tcc.input,
77
61
  },
78
62
  sessionApproval: {
79
63
  surface: suggestion.surface,