@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.
- package/CHANGELOG.md +29 -0
- package/config/config.example.json +3 -0
- package/package.json +1 -1
- package/schemas/permissions.schema.json +12 -0
- package/src/extension-config.ts +23 -0
- package/src/handlers/gates/tool.ts +4 -2
- package/src/handlers/permission-gate-handler.ts +106 -138
- package/src/index.ts +0 -6
- package/src/permission-prompts.ts +5 -2
- package/src/service.ts +1 -23
- package/src/subagent-registry.ts +3 -3
- package/src/tool-input-preview.ts +0 -116
- package/src/tool-preview-formatter.ts +188 -0
- package/test/extension-config.test.ts +93 -0
- package/test/handlers/external-directory-integration.test.ts +2 -0
- package/test/handlers/external-directory-session-dedup.test.ts +2 -0
- package/test/handlers/gates/tool.test.ts +29 -2
- package/test/handlers/tool-call-events.test.ts +2 -1
- package/test/handlers/tool-call.test.ts +2 -1
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/permission-prompts.test.ts +66 -38
- package/test/service.test.ts +0 -54
- package/test/tool-input-preview.test.ts +0 -244
- package/test/tool-preview-formatter.test.ts +385 -0
|
@@ -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(
|
|
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,
|