@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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type RequestedToolValidation,
|
|
5
|
+
validateRequestedTool,
|
|
6
|
+
} from "#src/handlers/permission-gate-handler";
|
|
7
|
+
|
|
8
|
+
// ── helpers ────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
function makeTools(names: string[]): { name: string }[] {
|
|
11
|
+
return names.map((name) => ({ name }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TOOLS = makeTools(["read", "bash", "edit"]);
|
|
15
|
+
|
|
16
|
+
// ── validateRequestedTool ──────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
describe("validateRequestedTool", () => {
|
|
19
|
+
describe("missing / unresolvable tool name", () => {
|
|
20
|
+
it("blocks when event has no name field", () => {
|
|
21
|
+
const result = validateRequestedTool({ type: "tool_call" }, TOOLS);
|
|
22
|
+
expect(result.status).toBe("block");
|
|
23
|
+
expect(
|
|
24
|
+
(result as Extract<RequestedToolValidation, { status: "block" }>)
|
|
25
|
+
.reason,
|
|
26
|
+
).toBeTruthy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("blocks when name field is an empty string", () => {
|
|
30
|
+
const result = validateRequestedTool({ name: "" }, TOOLS);
|
|
31
|
+
expect(result.status).toBe("block");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("blocks when name field is null", () => {
|
|
35
|
+
const result = validateRequestedTool({ name: null }, TOOLS);
|
|
36
|
+
expect(result.status).toBe("block");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("blocks when event is a primitive", () => {
|
|
40
|
+
const result = validateRequestedTool("not-an-object", TOOLS);
|
|
41
|
+
expect(result.status).toBe("block");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("unregistered tool", () => {
|
|
46
|
+
it("blocks when the tool name is not in the registered list", () => {
|
|
47
|
+
const result = validateRequestedTool({ name: "unknown-tool" }, TOOLS);
|
|
48
|
+
expect(result.status).toBe("block");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("includes available tool names in the block reason", () => {
|
|
52
|
+
const result = validateRequestedTool({ name: "unknown-tool" }, TOOLS);
|
|
53
|
+
expect(result.status).toBe("block");
|
|
54
|
+
const { reason } = result as Extract<
|
|
55
|
+
RequestedToolValidation,
|
|
56
|
+
{ status: "block" }
|
|
57
|
+
>;
|
|
58
|
+
expect(reason).toContain("read");
|
|
59
|
+
expect(reason).toContain("bash");
|
|
60
|
+
expect(reason).toContain("edit");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("blocks with empty available list when no tools are registered", () => {
|
|
64
|
+
const result = validateRequestedTool({ name: "anything" }, []);
|
|
65
|
+
expect(result.status).toBe("block");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("registered tool (ok path)", () => {
|
|
70
|
+
it("returns ok with the raw tool name for a known tool", () => {
|
|
71
|
+
const result = validateRequestedTool({ name: "read" }, TOOLS);
|
|
72
|
+
expect(result).toEqual({ status: "ok", toolName: "read" });
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("returns the raw name as it appeared in the event (not normalised)", () => {
|
|
76
|
+
// If an alias mechanism were to normalise "Read" → "read",
|
|
77
|
+
// validateRequestedTool still returns the raw value from the event.
|
|
78
|
+
// Without aliases the raw name and registered name are the same; this
|
|
79
|
+
// asserts the contract that toolName comes from the event, not from the
|
|
80
|
+
// registration lookup's normalizedToolName field.
|
|
81
|
+
const result = validateRequestedTool({ name: "bash" }, TOOLS);
|
|
82
|
+
expect(result).toEqual({ status: "ok", toolName: "bash" });
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("resolves tool name via the `arguments` field naming convention", () => {
|
|
86
|
+
// getToolNameFromValue reads `.name` then falls back to other fields;
|
|
87
|
+
// a plain `{ name: "edit" }` event is sufficient here.
|
|
88
|
+
const result = validateRequestedTool({ name: "edit" }, TOOLS);
|
|
89
|
+
expect(result).toEqual({ status: "ok", toolName: "edit" });
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// Mock tool-input-preview collaborator before importing the module under test.
|
|
4
|
-
vi.mock("../src/tool-input-preview.js", () => ({
|
|
5
|
-
formatToolInputForPrompt: vi.fn(() => "mocked preview"),
|
|
6
|
-
}));
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
7
2
|
|
|
8
3
|
import {
|
|
9
4
|
formatAskPrompt,
|
|
@@ -13,18 +8,21 @@ import {
|
|
|
13
8
|
formatUnknownToolReason,
|
|
14
9
|
} from "#src/permission-prompts";
|
|
15
10
|
import type { SkillPromptEntry } from "#src/skill-prompt-sanitizer";
|
|
16
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
13
|
+
TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
14
|
+
TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
15
|
+
} from "#src/tool-input-preview";
|
|
16
|
+
import { ToolPreviewFormatter } from "#src/tool-preview-formatter";
|
|
17
17
|
import type { PermissionCheckResult } from "#src/types";
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
vi.restoreAllMocks();
|
|
27
|
-
});
|
|
19
|
+
function makeFormatter(): ToolPreviewFormatter {
|
|
20
|
+
return new ToolPreviewFormatter({
|
|
21
|
+
toolInputPreviewMaxLength: TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
22
|
+
toolTextSummaryMaxLength: TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
23
|
+
toolInputLogPreviewMaxLength: TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
28
26
|
|
|
29
27
|
function toolResult(
|
|
30
28
|
toolName: string,
|
|
@@ -104,66 +102,96 @@ describe("formatUnknownToolReason", () => {
|
|
|
104
102
|
|
|
105
103
|
describe("formatAskPrompt", () => {
|
|
106
104
|
test("uses 'Current agent' when no agent name given", () => {
|
|
107
|
-
const result = formatAskPrompt(
|
|
108
|
-
|
|
109
|
-
|
|
105
|
+
const result = formatAskPrompt(
|
|
106
|
+
toolResult("read"),
|
|
107
|
+
undefined,
|
|
108
|
+
{ path: "/src" },
|
|
109
|
+
makeFormatter(),
|
|
110
|
+
);
|
|
110
111
|
expect(result).toContain("Current agent");
|
|
111
112
|
});
|
|
112
113
|
|
|
113
114
|
test("uses agent name when provided", () => {
|
|
114
|
-
const result = formatAskPrompt(
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
const result = formatAskPrompt(
|
|
116
|
+
toolResult("read"),
|
|
117
|
+
"my-agent",
|
|
118
|
+
{ path: "/src" },
|
|
119
|
+
makeFormatter(),
|
|
120
|
+
);
|
|
117
121
|
expect(result).toContain("Agent 'my-agent'");
|
|
118
122
|
});
|
|
119
123
|
|
|
120
|
-
test("formats bash prompt with command and
|
|
124
|
+
test("formats bash prompt with command and does not use formatter", () => {
|
|
121
125
|
const result = formatAskPrompt(
|
|
122
126
|
toolResult("bash", { command: "git status" }),
|
|
127
|
+
undefined,
|
|
128
|
+
undefined,
|
|
129
|
+
makeFormatter(),
|
|
123
130
|
);
|
|
124
131
|
expect(result).toContain("git status");
|
|
125
132
|
expect(result).toContain("Allow this command?");
|
|
126
|
-
expect(mockedFormatToolInput).not.toHaveBeenCalled();
|
|
127
133
|
});
|
|
128
134
|
|
|
129
135
|
test("formats bash prompt with matched pattern", () => {
|
|
130
136
|
const result = formatAskPrompt(
|
|
131
137
|
toolResult("bash", { command: "git push", matchedPattern: "git *" }),
|
|
138
|
+
undefined,
|
|
139
|
+
undefined,
|
|
140
|
+
makeFormatter(),
|
|
132
141
|
);
|
|
133
142
|
expect(result).toContain("matched 'git *'");
|
|
134
143
|
});
|
|
135
144
|
|
|
136
145
|
test("formats MCP prompt with target", () => {
|
|
137
|
-
const result = formatAskPrompt(
|
|
146
|
+
const result = formatAskPrompt(
|
|
147
|
+
mcpResult("server:query"),
|
|
148
|
+
undefined,
|
|
149
|
+
undefined,
|
|
150
|
+
makeFormatter(),
|
|
151
|
+
);
|
|
138
152
|
expect(result).toContain("server:query");
|
|
139
153
|
expect(result).toContain("Allow this call?");
|
|
140
|
-
expect(mockedFormatToolInput).not.toHaveBeenCalled();
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
test("formats MCP prompt with matched pattern", () => {
|
|
144
157
|
const result = formatAskPrompt(
|
|
145
158
|
mcpResult("server:query", { matchedPattern: "server:*" }),
|
|
159
|
+
undefined,
|
|
160
|
+
undefined,
|
|
161
|
+
makeFormatter(),
|
|
146
162
|
);
|
|
147
163
|
expect(result).toContain("matched 'server:*'");
|
|
148
164
|
});
|
|
149
165
|
|
|
150
|
-
test("
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
expect(result).toContain("for '/src/foo.ts'");
|
|
166
|
+
test("includes real input preview for non-bash non-mcp tools", () => {
|
|
167
|
+
const result = formatAskPrompt(
|
|
168
|
+
toolResult("read"),
|
|
169
|
+
undefined,
|
|
170
|
+
{ path: "/src/foo.ts" },
|
|
171
|
+
makeFormatter(),
|
|
172
|
+
);
|
|
173
|
+
expect(result).toContain("path '/src/foo.ts'");
|
|
159
174
|
expect(result).toContain("Allow this call?");
|
|
160
175
|
});
|
|
161
176
|
|
|
162
|
-
test("omits input suffix when
|
|
163
|
-
|
|
164
|
-
|
|
177
|
+
test("omits input suffix when formatter returns empty string for input", () => {
|
|
178
|
+
const result = formatAskPrompt(
|
|
179
|
+
toolResult("task"),
|
|
180
|
+
undefined,
|
|
181
|
+
{},
|
|
182
|
+
makeFormatter(),
|
|
183
|
+
);
|
|
184
|
+
expect(result).toContain("task");
|
|
185
|
+
expect(result).not.toContain("undefined");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("omits input suffix when no formatter provided", () => {
|
|
189
|
+
const result = formatAskPrompt(toolResult("task"), undefined, {
|
|
190
|
+
path: "/src",
|
|
191
|
+
});
|
|
165
192
|
expect(result).toContain("task");
|
|
166
193
|
expect(result).not.toContain("undefined");
|
|
194
|
+
expect(result).toContain("Allow this call?");
|
|
167
195
|
});
|
|
168
196
|
});
|
|
169
197
|
|
package/test/service.test.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
publishPermissionsService,
|
|
7
7
|
unpublishPermissionsService,
|
|
8
8
|
} from "#src/service";
|
|
9
|
-
import { SubagentSessionRegistry } from "#src/subagent-registry";
|
|
10
9
|
import type { PermissionCheckResult } from "#src/types";
|
|
11
10
|
|
|
12
11
|
// ── helpers ────────────────────────────────────────────────────────────────
|
|
@@ -16,8 +15,6 @@ function makeService(
|
|
|
16
15
|
): PermissionsService {
|
|
17
16
|
return {
|
|
18
17
|
checkPermission: vi.fn(),
|
|
19
|
-
registerSubagentSession: vi.fn(),
|
|
20
|
-
unregisterSubagentSession: vi.fn(),
|
|
21
18
|
getToolPermission: vi.fn(),
|
|
22
19
|
...overrides,
|
|
23
20
|
};
|
|
@@ -130,61 +127,12 @@ describe("service adapter delegation", () => {
|
|
|
130
127
|
);
|
|
131
128
|
});
|
|
132
129
|
|
|
133
|
-
it("registerSubagentSession delegates to the registry", () => {
|
|
134
|
-
const registry = new SubagentSessionRegistry();
|
|
135
|
-
const service: PermissionsService = {
|
|
136
|
-
checkPermission: vi.fn(),
|
|
137
|
-
registerSubagentSession(key, info) {
|
|
138
|
-
registry.register(key, info);
|
|
139
|
-
},
|
|
140
|
-
unregisterSubagentSession(key) {
|
|
141
|
-
registry.unregister(key);
|
|
142
|
-
},
|
|
143
|
-
getToolPermission: vi.fn((): "allow" => "allow"),
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
publishPermissionsService(service);
|
|
147
|
-
getPermissionsService()!.registerSubagentSession("/sessions/task-1", {
|
|
148
|
-
agentName: "Explore",
|
|
149
|
-
parentSessionId: "parent-abc",
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
expect(registry.has("/sessions/task-1")).toBe(true);
|
|
153
|
-
expect(registry.get("/sessions/task-1")).toEqual({
|
|
154
|
-
agentName: "Explore",
|
|
155
|
-
parentSessionId: "parent-abc",
|
|
156
|
-
});
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("unregisterSubagentSession delegates to the registry", () => {
|
|
160
|
-
const registry = new SubagentSessionRegistry();
|
|
161
|
-
const service: PermissionsService = {
|
|
162
|
-
checkPermission: vi.fn(),
|
|
163
|
-
registerSubagentSession(key, info) {
|
|
164
|
-
registry.register(key, info);
|
|
165
|
-
},
|
|
166
|
-
unregisterSubagentSession(key) {
|
|
167
|
-
registry.unregister(key);
|
|
168
|
-
},
|
|
169
|
-
getToolPermission: vi.fn((): "allow" => "allow"),
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
publishPermissionsService(service);
|
|
173
|
-
const svc = getPermissionsService()!;
|
|
174
|
-
svc.registerSubagentSession("/sessions/task-1", { agentName: "Explore" });
|
|
175
|
-
svc.unregisterSubagentSession("/sessions/task-1");
|
|
176
|
-
|
|
177
|
-
expect(registry.has("/sessions/task-1")).toBe(false);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
130
|
it("getToolPermission delegates to the permission manager", () => {
|
|
181
131
|
const getToolPermissionFn = vi.fn(
|
|
182
132
|
(_t: string, _a?: string): "deny" => "deny",
|
|
183
133
|
);
|
|
184
134
|
const service: PermissionsService = {
|
|
185
135
|
checkPermission: vi.fn(),
|
|
186
|
-
registerSubagentSession: vi.fn(),
|
|
187
|
-
unregisterSubagentSession: vi.fn(),
|
|
188
136
|
getToolPermission(toolName, agentName) {
|
|
189
137
|
return getToolPermissionFn(toolName, agentName);
|
|
190
138
|
},
|
|
@@ -206,8 +154,6 @@ describe("service adapter delegation", () => {
|
|
|
206
154
|
);
|
|
207
155
|
const service: PermissionsService = {
|
|
208
156
|
checkPermission: vi.fn(),
|
|
209
|
-
registerSubagentSession: vi.fn(),
|
|
210
|
-
unregisterSubagentSession: vi.fn(),
|
|
211
157
|
getToolPermission(toolName, agentName) {
|
|
212
158
|
return getToolPermissionFn(toolName, agentName);
|
|
213
159
|
},
|
|
@@ -10,22 +10,15 @@ import {
|
|
|
10
10
|
countTextLines,
|
|
11
11
|
formatCount,
|
|
12
12
|
formatEditInputForPrompt,
|
|
13
|
-
formatGenericToolInputForLog,
|
|
14
13
|
formatReadInputForPrompt,
|
|
15
|
-
formatSearchInputForPrompt,
|
|
16
|
-
formatToolInputForPrompt,
|
|
17
14
|
formatWriteInputForPrompt,
|
|
18
|
-
getPermissionLogContext,
|
|
19
15
|
getPromptPath,
|
|
20
|
-
getToolInputPreviewForLog,
|
|
21
|
-
sanitizeInlineText,
|
|
22
16
|
serializeToolInputPreview,
|
|
23
17
|
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH,
|
|
24
18
|
TOOL_INPUT_PREVIEW_MAX_LENGTH,
|
|
25
19
|
TOOL_TEXT_SUMMARY_MAX_LENGTH,
|
|
26
20
|
truncateInlineText,
|
|
27
21
|
} from "#src/tool-input-preview";
|
|
28
|
-
import type { PermissionCheckResult } from "#src/types";
|
|
29
22
|
|
|
30
23
|
const mockedStringify = vi.mocked(safeJsonStringify);
|
|
31
24
|
|
|
@@ -73,29 +66,6 @@ describe("truncateInlineText", () => {
|
|
|
73
66
|
});
|
|
74
67
|
});
|
|
75
68
|
|
|
76
|
-
describe("sanitizeInlineText", () => {
|
|
77
|
-
test("collapses whitespace and trims", () => {
|
|
78
|
-
expect(sanitizeInlineText(" hello world ")).toBe("hello world");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("returns 'empty text' for blank string", () => {
|
|
82
|
-
expect(sanitizeInlineText("")).toBe("empty text");
|
|
83
|
-
expect(sanitizeInlineText(" ")).toBe("empty text");
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("truncates to default TOOL_TEXT_SUMMARY_MAX_LENGTH", () => {
|
|
87
|
-
const long = "x".repeat(100);
|
|
88
|
-
const result = sanitizeInlineText(long);
|
|
89
|
-
expect(result.length).toBeLessThanOrEqual(TOOL_TEXT_SUMMARY_MAX_LENGTH + 1); // +1 for ellipsis char
|
|
90
|
-
expect(result).toContain("…");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("respects custom maxLength", () => {
|
|
94
|
-
const result = sanitizeInlineText("hello world", 5);
|
|
95
|
-
expect(result).toBe("hello…");
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
69
|
describe("countTextLines", () => {
|
|
100
70
|
test("returns 0 for empty string", () => {
|
|
101
71
|
expect(countTextLines("")).toBe(0);
|
|
@@ -239,33 +209,6 @@ describe("formatReadInputForPrompt", () => {
|
|
|
239
209
|
});
|
|
240
210
|
});
|
|
241
211
|
|
|
242
|
-
describe("formatSearchInputForPrompt", () => {
|
|
243
|
-
test("includes pattern and path", () => {
|
|
244
|
-
const result = formatSearchInputForPrompt("grep", {
|
|
245
|
-
pattern: "TODO",
|
|
246
|
-
path: "/src",
|
|
247
|
-
});
|
|
248
|
-
expect(result).toContain("pattern 'TODO'");
|
|
249
|
-
expect(result).toContain("path '/src'");
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test("includes glob when present", () => {
|
|
253
|
-
const result = formatSearchInputForPrompt("find", { glob: "*.ts" });
|
|
254
|
-
expect(result).toContain("glob '*.ts'");
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test("uses 'current working directory' for find/grep/ls without path", () => {
|
|
258
|
-
for (const toolName of ["find", "grep", "ls"]) {
|
|
259
|
-
const result = formatSearchInputForPrompt(toolName, {});
|
|
260
|
-
expect(result).toContain("current working directory");
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
test("returns empty string for other tools with no input", () => {
|
|
265
|
-
expect(formatSearchInputForPrompt("other", {})).toBe("");
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
212
|
describe("serializeToolInputPreview", () => {
|
|
270
213
|
test("delegates serialization to safeJsonStringify", () => {
|
|
271
214
|
mockedStringify.mockReturnValue('{"key":"value"}');
|
|
@@ -295,190 +238,3 @@ describe("serializeToolInputPreview", () => {
|
|
|
295
238
|
expect(result).toBe('{ "key": "val" }');
|
|
296
239
|
});
|
|
297
240
|
});
|
|
298
|
-
|
|
299
|
-
describe("formatToolInputForPrompt", () => {
|
|
300
|
-
test("dispatches 'edit' to formatEditInputForPrompt", () => {
|
|
301
|
-
mockedStringify.mockReturnValue(undefined);
|
|
302
|
-
const result = formatToolInputForPrompt("edit", {
|
|
303
|
-
path: "/foo.ts",
|
|
304
|
-
edits: [],
|
|
305
|
-
});
|
|
306
|
-
expect(result).toContain("for '/foo.ts'");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("dispatches 'write' to formatWriteInputForPrompt", () => {
|
|
310
|
-
const result = formatToolInputForPrompt("write", {
|
|
311
|
-
path: "/out.ts",
|
|
312
|
-
content: "hi",
|
|
313
|
-
});
|
|
314
|
-
expect(result).toContain("for '/out.ts'");
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
test("dispatches 'read' to formatReadInputForPrompt", () => {
|
|
318
|
-
const result = formatToolInputForPrompt("read", { path: "/src/x.ts" });
|
|
319
|
-
expect(result).toContain("path '/src/x.ts'");
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test("dispatches 'find'/'grep'/'ls' to formatSearchInputForPrompt", () => {
|
|
323
|
-
for (const tool of ["find", "grep", "ls"]) {
|
|
324
|
-
const result = formatToolInputForPrompt(tool, {});
|
|
325
|
-
expect(result).toContain("current working directory");
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test("falls back to JSON preview for unknown tools", () => {
|
|
330
|
-
mockedStringify.mockReturnValue('{"x":1}');
|
|
331
|
-
const result = formatToolInputForPrompt("unknown", { x: 1 });
|
|
332
|
-
expect(result).toContain('{"x":1}');
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
describe("formatGenericToolInputForLog", () => {
|
|
337
|
-
test("returns undefined when serialization yields empty string", () => {
|
|
338
|
-
mockedStringify.mockReturnValue(undefined);
|
|
339
|
-
expect(formatGenericToolInputForLog({})).toBeUndefined();
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
test("returns prefixed input preview", () => {
|
|
343
|
-
mockedStringify.mockReturnValue('{"k":"v"}');
|
|
344
|
-
expect(formatGenericToolInputForLog({ k: "v" })).toBe('input {"k":"v"}');
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test("truncates to TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH", () => {
|
|
348
|
-
const longJson = `{"k":"${"x".repeat(2000)}"}`;
|
|
349
|
-
mockedStringify.mockReturnValue(longJson);
|
|
350
|
-
const result = formatGenericToolInputForLog({});
|
|
351
|
-
expect(result).toBeDefined();
|
|
352
|
-
// result is "input " + truncated, so total > TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH by "input ".length
|
|
353
|
-
const preview = result!.slice("input ".length);
|
|
354
|
-
expect(preview.length).toBeLessThanOrEqual(
|
|
355
|
-
TOOL_INPUT_LOG_PREVIEW_MAX_LENGTH + 1,
|
|
356
|
-
);
|
|
357
|
-
});
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
describe("getToolInputPreviewForLog", () => {
|
|
361
|
-
const pathBearingTools = new Set(["read", "write", "edit"]);
|
|
362
|
-
|
|
363
|
-
test("returns undefined for bash tool", () => {
|
|
364
|
-
const result: PermissionCheckResult = {
|
|
365
|
-
toolName: "bash",
|
|
366
|
-
state: "allow",
|
|
367
|
-
source: "tool",
|
|
368
|
-
origin: "builtin",
|
|
369
|
-
};
|
|
370
|
-
expect(
|
|
371
|
-
getToolInputPreviewForLog(result, { command: "ls" }, pathBearingTools),
|
|
372
|
-
).toBeUndefined();
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
test("returns undefined for mcp tool", () => {
|
|
376
|
-
const result: PermissionCheckResult = {
|
|
377
|
-
toolName: "mcp",
|
|
378
|
-
state: "allow",
|
|
379
|
-
source: "tool",
|
|
380
|
-
origin: "builtin",
|
|
381
|
-
};
|
|
382
|
-
expect(
|
|
383
|
-
getToolInputPreviewForLog(result, {}, pathBearingTools),
|
|
384
|
-
).toBeUndefined();
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
test("returns undefined for mcp source", () => {
|
|
388
|
-
const result: PermissionCheckResult = {
|
|
389
|
-
toolName: "some-server:some-tool",
|
|
390
|
-
state: "allow",
|
|
391
|
-
source: "mcp",
|
|
392
|
-
origin: "builtin",
|
|
393
|
-
};
|
|
394
|
-
expect(
|
|
395
|
-
getToolInputPreviewForLog(result, {}, pathBearingTools),
|
|
396
|
-
).toBeUndefined();
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("returns path-based preview for path-bearing tools", () => {
|
|
400
|
-
const result: PermissionCheckResult = {
|
|
401
|
-
toolName: "read",
|
|
402
|
-
state: "allow",
|
|
403
|
-
source: "tool",
|
|
404
|
-
origin: "builtin",
|
|
405
|
-
};
|
|
406
|
-
const preview = getToolInputPreviewForLog(
|
|
407
|
-
result,
|
|
408
|
-
{ path: "/src/foo.ts" },
|
|
409
|
-
pathBearingTools,
|
|
410
|
-
);
|
|
411
|
-
expect(preview).toContain("/src/foo.ts");
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
test("returns generic JSON preview for non-path-bearing tools", () => {
|
|
415
|
-
mockedStringify.mockReturnValue('{"n":1}');
|
|
416
|
-
const result: PermissionCheckResult = {
|
|
417
|
-
toolName: "task",
|
|
418
|
-
state: "allow",
|
|
419
|
-
source: "tool",
|
|
420
|
-
origin: "builtin",
|
|
421
|
-
};
|
|
422
|
-
const preview = getToolInputPreviewForLog(
|
|
423
|
-
result,
|
|
424
|
-
{ n: 1 },
|
|
425
|
-
pathBearingTools,
|
|
426
|
-
);
|
|
427
|
-
expect(preview).toContain('{"n":1}');
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
describe("getPermissionLogContext", () => {
|
|
432
|
-
const pathBearingTools = new Set(["read", "write", "edit"]);
|
|
433
|
-
|
|
434
|
-
test("returns command, target, and toolInputPreview", () => {
|
|
435
|
-
const result: PermissionCheckResult = {
|
|
436
|
-
toolName: "bash",
|
|
437
|
-
state: "allow",
|
|
438
|
-
source: "tool",
|
|
439
|
-
origin: "builtin",
|
|
440
|
-
command: "ls -la",
|
|
441
|
-
};
|
|
442
|
-
const ctx = getPermissionLogContext(result, {}, pathBearingTools);
|
|
443
|
-
expect(ctx.command).toBe("ls -la");
|
|
444
|
-
expect(ctx.target).toBeUndefined();
|
|
445
|
-
expect(ctx.toolInputPreview).toBeUndefined();
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test("includes toolInputPreview for non-bash path-bearing tools", () => {
|
|
449
|
-
const result: PermissionCheckResult = {
|
|
450
|
-
toolName: "read",
|
|
451
|
-
state: "allow",
|
|
452
|
-
source: "tool",
|
|
453
|
-
origin: "builtin",
|
|
454
|
-
};
|
|
455
|
-
const ctx = getPermissionLogContext(
|
|
456
|
-
result,
|
|
457
|
-
{ path: "/foo.ts" },
|
|
458
|
-
pathBearingTools,
|
|
459
|
-
);
|
|
460
|
-
expect(ctx.toolInputPreview).toContain("/foo.ts");
|
|
461
|
-
});
|
|
462
|
-
|
|
463
|
-
test("includes origin from check result when present", () => {
|
|
464
|
-
const result: PermissionCheckResult = {
|
|
465
|
-
toolName: "read",
|
|
466
|
-
state: "allow",
|
|
467
|
-
source: "tool",
|
|
468
|
-
origin: "project",
|
|
469
|
-
};
|
|
470
|
-
const ctx = getPermissionLogContext(result, {}, pathBearingTools);
|
|
471
|
-
expect(ctx.origin).toBe("project");
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
test("origin is 'builtin' when check result has builtin origin", () => {
|
|
475
|
-
const result: PermissionCheckResult = {
|
|
476
|
-
toolName: "read",
|
|
477
|
-
state: "allow",
|
|
478
|
-
source: "tool",
|
|
479
|
-
origin: "builtin",
|
|
480
|
-
};
|
|
481
|
-
const ctx = getPermissionLogContext(result, {}, pathBearingTools);
|
|
482
|
-
expect(ctx.origin).toBe("builtin");
|
|
483
|
-
});
|
|
484
|
-
});
|