@gotgenes/pi-permission-system 7.4.1 → 8.1.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.
@@ -1,6 +1,5 @@
1
1
  import { getNonEmptyString, toRecord } from "./common";
2
2
  import { safeJsonStringify } from "./logging";
3
- import type { PermissionCheckResult } from "./types";
4
3
 
5
4
  export const TOOL_INPUT_PREVIEW_MAX_LENGTH = 200;
6
5
  export const TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH = 1000;
@@ -10,14 +9,6 @@ export function truncateInlineText(value: string, maxLength: number): string {
10
9
  return value.length > maxLength ? `${value.slice(0, maxLength)}…` : value;
11
10
  }
12
11
 
13
- export function sanitizeInlineText(
14
- value: string,
15
- maxLength = TOOL_TEXT_SUMMARY_MAX_LENGTH,
16
- ): string {
17
- const normalized = value.replace(/\s+/g, " ").trim();
18
- return normalized ? truncateInlineText(normalized, maxLength) : "empty text";
19
- }
20
-
21
12
  export function countTextLines(value: string): number {
22
13
  if (!value) {
23
14
  return 0;
@@ -95,30 +86,6 @@ export function formatReadInputForPrompt(
95
86
  return parts.length > 0 ? `for ${parts.join(", ")}` : "";
96
87
  }
97
88
 
98
- export function formatSearchInputForPrompt(
99
- toolName: string,
100
- input: Record<string, unknown>,
101
- ): string {
102
- const parts: string[] = [];
103
- const path = getPromptPath(input);
104
- const pattern = getNonEmptyString(input.pattern);
105
- const glob = getNonEmptyString(input.glob);
106
-
107
- if (pattern) {
108
- parts.push(`pattern '${sanitizeInlineText(pattern)}'`);
109
- }
110
- if (glob) {
111
- parts.push(`glob '${sanitizeInlineText(glob)}'`);
112
- }
113
- if (path) {
114
- parts.push(`path '${path}'`);
115
- } else if (toolName === "find" || toolName === "grep" || toolName === "ls") {
116
- parts.push("current working directory");
117
- }
118
-
119
- return parts.length > 0 ? `for ${parts.join(", ")}` : "";
120
- }
121
-
122
89
  export function serializeToolInputPreview(input: unknown): string {
123
90
  const serialized = safeJsonStringify(input);
124
91
  if (!serialized || serialized === "{}" || serialized === "null") {
@@ -127,86 +94,3 @@ export function serializeToolInputPreview(input: unknown): string {
127
94
 
128
95
  return serialized.replace(/\s+/g, " ").trim();
129
96
  }
130
-
131
- export function formatJsonInputForPrompt(input: unknown): string {
132
- const inline = serializeToolInputPreview(input);
133
- return inline
134
- ? `with input ${truncateInlineText(inline, TOOL_INPUT_PREVIEW_MAX_LENGTH)}`
135
- : "";
136
- }
137
-
138
- export function formatToolInputForPrompt(
139
- toolName: string,
140
- input: unknown,
141
- ): string {
142
- const inputRecord = toRecord(input);
143
-
144
- switch (toolName) {
145
- case "edit":
146
- return formatEditInputForPrompt(inputRecord);
147
- case "write":
148
- return formatWriteInputForPrompt(inputRecord);
149
- case "read":
150
- return formatReadInputForPrompt(inputRecord);
151
- case "find":
152
- case "grep":
153
- case "ls":
154
- return formatSearchInputForPrompt(toolName, inputRecord);
155
- default:
156
- return formatJsonInputForPrompt(input);
157
- }
158
- }
159
-
160
- export function formatGenericToolInputForLog(
161
- input: unknown,
162
- ): string | undefined {
163
- const inline = serializeToolInputPreview(input);
164
- return inline
165
- ? `input ${truncateInlineText(inline, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)}`
166
- : undefined;
167
- }
168
-
169
- export function getToolInputPreviewForLog(
170
- result: PermissionCheckResult,
171
- input: unknown,
172
- pathBearingTools: ReadonlySet<string>,
173
- ): string | undefined {
174
- if (
175
- result.toolName === "bash" ||
176
- result.toolName === "mcp" ||
177
- result.source === "mcp"
178
- ) {
179
- return undefined;
180
- }
181
-
182
- if (pathBearingTools.has(result.toolName)) {
183
- const inputPreview = formatToolInputForPrompt(result.toolName, input);
184
- return inputPreview
185
- ? truncateInlineText(inputPreview, TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH)
186
- : undefined;
187
- }
188
-
189
- return formatGenericToolInputForLog(input);
190
- }
191
-
192
- export function getPermissionLogContext(
193
- result: PermissionCheckResult,
194
- input: unknown,
195
- pathBearingTools: ReadonlySet<string>,
196
- ): {
197
- command?: string;
198
- target?: string;
199
- toolInputPreview?: string;
200
- origin?: string;
201
- } {
202
- return {
203
- command: result.command,
204
- target: result.target,
205
- toolInputPreview: getToolInputPreviewForLog(
206
- result,
207
- input,
208
- pathBearingTools,
209
- ),
210
- origin: result.origin,
211
- };
212
- }
@@ -0,0 +1,188 @@
1
+ import { getNonEmptyString, toRecord } from "./common";
2
+ import type { PermissionSystemExtensionConfig } from "./extension-config";
3
+ import {
4
+ formatEditInputForPrompt,
5
+ formatReadInputForPrompt,
6
+ formatWriteInputForPrompt,
7
+ getPromptPath,
8
+ serializeToolInputPreview,
9
+ TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
10
+ TOOL_INPUT_PREVIEW_MAX_LENGTH,
11
+ TOOL_TEXT_SUMMARY_MAX_LENGTH,
12
+ truncateInlineText,
13
+ } from "./tool-input-preview";
14
+ import type { PermissionCheckResult } from "./types";
15
+
16
+ export interface ToolPreviewFormatterOptions {
17
+ toolInputPreviewMaxLength: number;
18
+ toolTextSummaryMaxLength: number;
19
+ toolInputLogPreviewMaxLength: number;
20
+ }
21
+
22
+ type ConfigurablePreviewLimits = Pick<
23
+ PermissionSystemExtensionConfig,
24
+ "toolInputPreviewMaxLength" | "toolTextSummaryMaxLength"
25
+ >;
26
+
27
+ /**
28
+ * Resolve `ToolPreviewFormatterOptions` from a config object, falling back to
29
+ * the built-in defaults for any field that is absent.
30
+ */
31
+ export function resolveToolPreviewLimits(
32
+ config: ConfigurablePreviewLimits,
33
+ ): ToolPreviewFormatterOptions {
34
+ return {
35
+ toolInputPreviewMaxLength:
36
+ config.toolInputPreviewMaxLength ?? TOOL_INPUT_PREVIEW_MAX_LENGTH,
37
+ toolTextSummaryMaxLength:
38
+ config.toolTextSummaryMaxLength ?? TOOL_TEXT_SUMMARY_MAX_LENGTH,
39
+ toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Formats tool inputs for permission prompts and review logs.
45
+ *
46
+ * Accepts configurable limits in its constructor — the single injection
47
+ * point for preview-length configuration (#266).
48
+ */
49
+ export class ToolPreviewFormatter {
50
+ constructor(private readonly options: ToolPreviewFormatterOptions) {}
51
+
52
+ // ── Prompt formatting ───────────────────────────────────────────────────
53
+
54
+ /**
55
+ * Collapse whitespace, trim, and truncate a string to fit inline.
56
+ * An explicit `maxLength` overrides the constructor default.
57
+ */
58
+ sanitizeInlineText(value: string, maxLength?: number): string {
59
+ const limit = maxLength ?? this.options.toolTextSummaryMaxLength;
60
+ const normalized = value.replace(/\s+/g, " ").trim();
61
+ return normalized ? truncateInlineText(normalized, limit) : "empty text";
62
+ }
63
+
64
+ /** Serialize `input` to inline JSON and truncate at `toolInputPreviewMaxLength`. */
65
+ formatJsonInputForPrompt(input: unknown): string {
66
+ const inline = serializeToolInputPreview(input);
67
+ return inline
68
+ ? `with input ${truncateInlineText(inline, this.options.toolInputPreviewMaxLength)}`
69
+ : "";
70
+ }
71
+
72
+ /** Format search-tool (grep/find/ls) input for a permission prompt. */
73
+ formatSearchInputForPrompt(
74
+ toolName: string,
75
+ input: Record<string, unknown>,
76
+ ): string {
77
+ const parts: string[] = [];
78
+ const path = getPromptPath(input);
79
+ const pattern = getNonEmptyString(input.pattern);
80
+ const glob = getNonEmptyString(input.glob);
81
+
82
+ if (pattern) {
83
+ parts.push(`pattern '${this.sanitizeInlineText(pattern)}'`);
84
+ }
85
+ if (glob) {
86
+ parts.push(`glob '${this.sanitizeInlineText(glob)}'`);
87
+ }
88
+ if (path) {
89
+ parts.push(`path '${path}'`);
90
+ } else if (
91
+ toolName === "find" ||
92
+ toolName === "grep" ||
93
+ toolName === "ls"
94
+ ) {
95
+ parts.push("current working directory");
96
+ }
97
+
98
+ return parts.length > 0 ? `for ${parts.join(", ")}` : "";
99
+ }
100
+
101
+ /**
102
+ * Format any tool input for display in a permission ask-prompt.
103
+ *
104
+ * Dispatches to the appropriate pure formatter for known tools
105
+ * and falls back to inline JSON for everything else.
106
+ */
107
+ formatToolInputForPrompt(toolName: string, input: unknown): string {
108
+ const inputRecord = toRecord(input);
109
+
110
+ switch (toolName) {
111
+ case "edit":
112
+ return formatEditInputForPrompt(inputRecord);
113
+ case "write":
114
+ return formatWriteInputForPrompt(inputRecord);
115
+ case "read":
116
+ return formatReadInputForPrompt(inputRecord);
117
+ case "find":
118
+ case "grep":
119
+ case "ls":
120
+ return this.formatSearchInputForPrompt(toolName, inputRecord);
121
+ default:
122
+ return this.formatJsonInputForPrompt(input);
123
+ }
124
+ }
125
+
126
+ // ── Log formatting ──────────────────────────────────────────────────────
127
+
128
+ /** Serialize `input` to inline JSON and truncate at `toolInputLogPreviewMaxLength`. */
129
+ formatGenericToolInputForLog(input: unknown): string | undefined {
130
+ const inline = serializeToolInputPreview(input);
131
+ return inline
132
+ ? `input ${truncateInlineText(inline, this.options.toolInputLogPreviewMaxLength)}`
133
+ : undefined;
134
+ }
135
+
136
+ /** Derive a loggable input preview string for the review log. */
137
+ getToolInputPreviewForLog(
138
+ result: PermissionCheckResult,
139
+ input: unknown,
140
+ pathBearingTools: ReadonlySet<string>,
141
+ ): string | undefined {
142
+ if (
143
+ result.toolName === "bash" ||
144
+ result.toolName === "mcp" ||
145
+ result.source === "mcp"
146
+ ) {
147
+ return undefined;
148
+ }
149
+
150
+ if (pathBearingTools.has(result.toolName)) {
151
+ const inputPreview = this.formatToolInputForPrompt(
152
+ result.toolName,
153
+ input,
154
+ );
155
+ return inputPreview
156
+ ? truncateInlineText(
157
+ inputPreview,
158
+ this.options.toolInputLogPreviewMaxLength,
159
+ )
160
+ : undefined;
161
+ }
162
+
163
+ return this.formatGenericToolInputForLog(input);
164
+ }
165
+
166
+ /** Build the structured log context object for a permission review log entry. */
167
+ getPermissionLogContext(
168
+ result: PermissionCheckResult,
169
+ input: unknown,
170
+ pathBearingTools: ReadonlySet<string>,
171
+ ): {
172
+ command?: string;
173
+ target?: string;
174
+ toolInputPreview?: string;
175
+ origin?: string;
176
+ } {
177
+ return {
178
+ command: result.command,
179
+ target: result.target,
180
+ toolInputPreview: this.getToolInputPreviewForLog(
181
+ result,
182
+ input,
183
+ pathBearingTools,
184
+ ),
185
+ origin: result.origin,
186
+ };
187
+ }
188
+ }
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import {
4
4
  detectMisplacedPermissionKeys,
5
+ normalizeOptionalPositiveInt,
5
6
  normalizePermissionSystemConfig,
6
7
  } from "#src/extension-config";
7
8
 
@@ -74,6 +75,36 @@ describe("detectMisplacedPermissionKeys", () => {
74
75
  });
75
76
  });
76
77
 
78
+ describe("normalizeOptionalPositiveInt", () => {
79
+ it("returns the value for a valid positive integer", () => {
80
+ expect(normalizeOptionalPositiveInt(1)).toBe(1);
81
+ expect(normalizeOptionalPositiveInt(200)).toBe(200);
82
+ expect(normalizeOptionalPositiveInt(9999)).toBe(9999);
83
+ });
84
+
85
+ it("returns undefined for zero", () => {
86
+ expect(normalizeOptionalPositiveInt(0)).toBeUndefined();
87
+ });
88
+
89
+ it("returns undefined for negative integers", () => {
90
+ expect(normalizeOptionalPositiveInt(-1)).toBeUndefined();
91
+ expect(normalizeOptionalPositiveInt(-100)).toBeUndefined();
92
+ });
93
+
94
+ it("returns undefined for non-integer numbers (floats)", () => {
95
+ expect(normalizeOptionalPositiveInt(400.5)).toBeUndefined();
96
+ expect(normalizeOptionalPositiveInt(1.1)).toBeUndefined();
97
+ });
98
+
99
+ it("returns undefined for non-number types", () => {
100
+ expect(normalizeOptionalPositiveInt("200")).toBeUndefined();
101
+ expect(normalizeOptionalPositiveInt(true)).toBeUndefined();
102
+ expect(normalizeOptionalPositiveInt(null)).toBeUndefined();
103
+ expect(normalizeOptionalPositiveInt(undefined)).toBeUndefined();
104
+ expect(normalizeOptionalPositiveInt({})).toBeUndefined();
105
+ });
106
+ });
107
+
77
108
  describe("normalizePermissionSystemConfig", () => {
78
109
  it("normalizes a valid config object", () => {
79
110
  const result = normalizePermissionSystemConfig({
@@ -122,4 +153,66 @@ describe("normalizePermissionSystemConfig", () => {
122
153
  yoloMode: false,
123
154
  });
124
155
  });
156
+
157
+ it("includes toolInputPreviewMaxLength when a valid positive integer is provided", () => {
158
+ const result = normalizePermissionSystemConfig({
159
+ toolInputPreviewMaxLength: 400,
160
+ });
161
+ expect(result.toolInputPreviewMaxLength).toBe(400);
162
+ });
163
+
164
+ it("omits toolInputPreviewMaxLength when absent", () => {
165
+ const result = normalizePermissionSystemConfig({});
166
+ expect("toolInputPreviewMaxLength" in result).toBe(false);
167
+ });
168
+
169
+ it("omits toolInputPreviewMaxLength for invalid values", () => {
170
+ expect(
171
+ normalizePermissionSystemConfig({ toolInputPreviewMaxLength: 0 })
172
+ .toolInputPreviewMaxLength,
173
+ ).toBeUndefined();
174
+ expect(
175
+ normalizePermissionSystemConfig({ toolInputPreviewMaxLength: -1 })
176
+ .toolInputPreviewMaxLength,
177
+ ).toBeUndefined();
178
+ expect(
179
+ normalizePermissionSystemConfig({ toolInputPreviewMaxLength: 200.5 })
180
+ .toolInputPreviewMaxLength,
181
+ ).toBeUndefined();
182
+ expect(
183
+ normalizePermissionSystemConfig({ toolInputPreviewMaxLength: "200" })
184
+ .toolInputPreviewMaxLength,
185
+ ).toBeUndefined();
186
+ });
187
+
188
+ it("includes toolTextSummaryMaxLength when a valid positive integer is provided", () => {
189
+ const result = normalizePermissionSystemConfig({
190
+ toolTextSummaryMaxLength: 120,
191
+ });
192
+ expect(result.toolTextSummaryMaxLength).toBe(120);
193
+ });
194
+
195
+ it("omits toolTextSummaryMaxLength when absent", () => {
196
+ const result = normalizePermissionSystemConfig({});
197
+ expect("toolTextSummaryMaxLength" in result).toBe(false);
198
+ });
199
+
200
+ it("omits toolTextSummaryMaxLength for invalid values", () => {
201
+ expect(
202
+ normalizePermissionSystemConfig({ toolTextSummaryMaxLength: 0 })
203
+ .toolTextSummaryMaxLength,
204
+ ).toBeUndefined();
205
+ expect(
206
+ normalizePermissionSystemConfig({ toolTextSummaryMaxLength: -1 })
207
+ .toolTextSummaryMaxLength,
208
+ ).toBeUndefined();
209
+ expect(
210
+ normalizePermissionSystemConfig({ toolTextSummaryMaxLength: 80.1 })
211
+ .toolTextSummaryMaxLength,
212
+ ).toBeUndefined();
213
+ expect(
214
+ normalizePermissionSystemConfig({ toolTextSummaryMaxLength: true })
215
+ .toolTextSummaryMaxLength,
216
+ ).toBeUndefined();
217
+ });
125
218
  });
@@ -12,6 +12,7 @@ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { describe, expect, it, vi } from "vitest";
13
13
 
14
14
  import { EXTENSION_TAG } from "#src/denial-messages";
15
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
15
16
  import { formatExternalDirectoryAskPrompt } from "#src/handlers/gates/external-directory-messages";
16
17
  import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
17
18
  import {
@@ -116,6 +117,7 @@ function makeSession(
116
117
  getActiveSkillEntries: vi.fn().mockReturnValue([]),
117
118
  getInfrastructureDirs: vi.fn().mockReturnValue([]),
118
119
  getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
120
+ config: DEFAULT_EXTENSION_CONFIG,
119
121
  canPrompt: vi.fn().mockReturnValue(true),
120
122
  prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
121
123
  ...overrides,
@@ -11,6 +11,7 @@
11
11
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
12
12
  import { describe, expect, it, vi } from "vitest";
13
13
 
14
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
14
15
  import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
15
16
  import type { PermissionSession } from "#src/permission-session";
16
17
  import type { Rule } from "#src/rule";
@@ -139,6 +140,7 @@ function makeStatefulSession(
139
140
  getActiveSkillEntries: vi.fn().mockReturnValue([]),
140
141
  getInfrastructureDirs: vi.fn().mockReturnValue([]),
141
142
  getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
143
+ config: DEFAULT_EXTENSION_CONFIG,
142
144
  canPrompt: vi.fn().mockReturnValue(true),
143
145
  prompt: vi
144
146
  .fn()
@@ -2,10 +2,24 @@ import { describe, expect, it } from "vitest";
2
2
 
3
3
  import { describeToolGate } from "#src/handlers/gates/tool";
4
4
  import type { ToolCallContext } from "#src/handlers/gates/types";
5
+ import {
6
+ TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
7
+ TOOL_INPUT_PREVIEW_MAX_LENGTH,
8
+ TOOL_TEXT_SUMMARY_MAX_LENGTH,
9
+ } from "#src/tool-input-preview";
10
+ import { ToolPreviewFormatter } from "#src/tool-preview-formatter";
5
11
  import type { PermissionCheckResult } from "#src/types";
6
12
 
7
13
  // ── helpers ────────────────────────────────────────────────────────────────
8
14
 
15
+ function makeFormatter(): ToolPreviewFormatter {
16
+ return new ToolPreviewFormatter({
17
+ toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
18
+ toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
19
+ toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
20
+ });
21
+ }
22
+
9
23
  function makeTcc(overrides: Partial<ToolCallContext> = {}): ToolCallContext {
10
24
  return {
11
25
  toolName: "read",
@@ -38,6 +52,7 @@ describe("describeToolGate", () => {
38
52
  const desc = describeToolGate(
39
53
  makeTcc({ toolName: "read" }),
40
54
  makeCheckResult("ask"),
55
+ makeFormatter(),
41
56
  );
42
57
  expect(desc.surface).toBe("read");
43
58
  expect(desc.decision.surface).toBe("read");
@@ -47,6 +62,7 @@ describe("describeToolGate", () => {
47
62
  const desc = describeToolGate(
48
63
  makeTcc({ toolName: "write" }),
49
64
  makeCheckResult("ask"),
65
+ makeFormatter(),
50
66
  );
51
67
  expect(desc.decision.value).toBe("write");
52
68
  });
@@ -59,6 +75,7 @@ describe("describeToolGate", () => {
59
75
  const desc = describeToolGate(
60
76
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
61
77
  check,
78
+ makeFormatter(),
62
79
  );
63
80
  expect(desc.surface).toBe("bash");
64
81
  expect(desc.decision.surface).toBe("bash");
@@ -73,6 +90,7 @@ describe("describeToolGate", () => {
73
90
  const desc = describeToolGate(
74
91
  makeTcc({ toolName: "mcp", input: { tool: "server:tool" } }),
75
92
  check,
93
+ makeFormatter(),
76
94
  );
77
95
  expect(desc.surface).toBe("mcp");
78
96
  expect(desc.decision.surface).toBe("mcp");
@@ -81,7 +99,7 @@ describe("describeToolGate", () => {
81
99
 
82
100
  it("populates denialContext with kind 'tool' and check result", () => {
83
101
  const check = makeCheckResult("deny", { toolName: "read" });
84
- const desc = describeToolGate(makeTcc(), check);
102
+ const desc = describeToolGate(makeTcc(), check, makeFormatter());
85
103
  expect(desc.denialContext).toEqual({
86
104
  kind: "tool",
87
105
  check,
@@ -92,7 +110,11 @@ describe("describeToolGate", () => {
92
110
 
93
111
  it("populates denialContext with agent name when provided", () => {
94
112
  const check = makeCheckResult("ask", { toolName: "read" });
95
- const desc = describeToolGate(makeTcc({ agentName: "my-agent" }), check);
113
+ const desc = describeToolGate(
114
+ makeTcc({ agentName: "my-agent" }),
115
+ check,
116
+ makeFormatter(),
117
+ );
96
118
  expect(desc.denialContext.agentName).toBe("my-agent");
97
119
  });
98
120
 
@@ -101,6 +123,7 @@ describe("describeToolGate", () => {
101
123
  const desc = describeToolGate(
102
124
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
103
125
  check,
126
+ makeFormatter(),
104
127
  );
105
128
  expect(desc.denialContext).toMatchObject({
106
129
  kind: "tool",
@@ -116,6 +139,7 @@ describe("describeToolGate", () => {
116
139
  const desc = describeToolGate(
117
140
  makeTcc({ toolName: "bash", input: { command: "git status" } }),
118
141
  check,
142
+ makeFormatter(),
119
143
  );
120
144
  expect(desc.sessionApproval).toBeDefined();
121
145
  expect(desc.sessionApproval!).toHaveProperty("surface", "bash");
@@ -127,6 +151,7 @@ describe("describeToolGate", () => {
127
151
  const desc = describeToolGate(
128
152
  makeTcc({ toolName: "read", agentName: "my-agent", toolCallId: "tc-42" }),
129
153
  check,
154
+ makeFormatter(),
130
155
  );
131
156
  expect(desc.promptDetails).toMatchObject({
132
157
  source: "tool_call",
@@ -143,6 +168,7 @@ describe("describeToolGate", () => {
143
168
  const desc = describeToolGate(
144
169
  makeTcc({ toolName: "bash", input: { command: "ls" } }),
145
170
  check,
171
+ makeFormatter(),
146
172
  );
147
173
  expect(desc.logContext).toMatchObject({
148
174
  source: "tool_call",
@@ -155,6 +181,7 @@ describe("describeToolGate", () => {
155
181
  const desc = describeToolGate(
156
182
  makeTcc({ toolName: "edit", input: { path: "/a.ts" } }),
157
183
  makeCheckResult("ask", { toolName: "edit" }),
184
+ makeFormatter(),
158
185
  );
159
186
  expect(desc.surface).toBe("edit");
160
187
  expect(desc.input).toEqual({ path: "/a.ts" });
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
6
  import { describe, expect, it, vi } from "vitest";
7
-
7
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
8
8
  import { PermissionGateHandler } from "#src/handlers/permission-gate-handler";
9
9
  import type { PermissionDecisionEvent } from "#src/permission-events";
10
10
  import { PERMISSIONS_DECISION_CHANNEL } from "#src/permission-events";
@@ -83,6 +83,7 @@ function makeSession(
83
83
  .fn()
84
84
  .mockReturnValue(["/test/agent", "/test/agent/git"]),
85
85
  getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
86
+ config: DEFAULT_EXTENSION_CONFIG,
86
87
  canPrompt: vi.fn().mockReturnValue(true),
87
88
  prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
88
89
  ...overrides,
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import { describe, expect, it, vi } from "vitest";
3
-
3
+ import { DEFAULT_EXTENSION_CONFIG } from "#src/extension-config";
4
4
  import {
5
5
  getEventInput,
6
6
  PermissionGateHandler,
@@ -74,6 +74,7 @@ function makeSession(
74
74
  .fn()
75
75
  .mockReturnValue(["/test/agent", "/test/agent/git"]),
76
76
  getInfrastructureReadPaths: vi.fn().mockReturnValue([]),
77
+ config: DEFAULT_EXTENSION_CONFIG,
77
78
  canPrompt: vi.fn().mockReturnValue(true),
78
79
  prompt: vi.fn().mockResolvedValue({ approved: true, state: "approved" }),
79
80
  ...overrides,