@gotgenes/pi-permission-system 8.0.0 → 8.2.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 +21 -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/bash-external-directory.ts +2 -4
- package/src/handlers/gates/bash-path.ts +2 -4
- package/src/handlers/gates/descriptor.ts +6 -6
- package/src/handlers/gates/external-directory.ts +2 -4
- package/src/handlers/gates/helpers.ts +30 -1
- package/src/handlers/gates/path.ts +2 -4
- package/src/handlers/gates/runner.ts +29 -56
- package/src/handlers/gates/tool.ts +9 -6
- package/src/handlers/permission-gate-handler.ts +110 -141
- package/src/permission-manager.ts +6 -49
- package/src/permission-prompts.ts +5 -2
- package/src/permission-session.ts +3 -2
- package/src/scope-merge.ts +72 -0
- package/src/session-approval.ts +43 -0
- package/src/session-rules.ts +13 -0
- 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 +3 -1
- package/test/handlers/external-directory-session-dedup.test.ts +17 -12
- package/test/handlers/gates/bash-external-directory.test.ts +11 -9
- package/test/handlers/gates/external-directory.test.ts +2 -5
- package/test/handlers/gates/helpers.test.ts +81 -0
- package/test/handlers/gates/path.test.ts +2 -2
- package/test/handlers/gates/runner.test.ts +18 -23
- package/test/handlers/gates/tool.test.ts +31 -4
- package/test/handlers/input-events.test.ts +1 -1
- package/test/handlers/input.test.ts +1 -1
- package/test/handlers/tool-call-events.test.ts +3 -2
- package/test/handlers/tool-call.test.ts +3 -2
- package/test/handlers/validate-requested-tool.test.ts +92 -0
- package/test/permission-prompts.test.ts +66 -38
- package/test/permission-session.test.ts +6 -3
- package/test/scope-merge.test.ts +116 -0
- package/test/session-approval.test.ts +75 -0
- package/test/session-rules.test.ts +49 -0
- package/test/tool-input-preview.test.ts +0 -244
- package/test/tool-preview-formatter.test.ts +385 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { MergedScopes } from "#src/scope-merge";
|
|
3
|
+
import { mergeScopesWithOrigins } from "#src/scope-merge";
|
|
4
|
+
|
|
5
|
+
describe("mergeScopesWithOrigins", () => {
|
|
6
|
+
it("returns empty result for empty scopes array", () => {
|
|
7
|
+
const result: MergedScopes = mergeScopesWithOrigins([]);
|
|
8
|
+
expect(result.mergedPermission).toEqual({});
|
|
9
|
+
expect(result.origins.size).toBe(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("attributes a string surface value to the contributing scope via the '*' pattern", () => {
|
|
13
|
+
const result = mergeScopesWithOrigins([
|
|
14
|
+
["global", { permission: { bash: "allow" } }],
|
|
15
|
+
]);
|
|
16
|
+
expect(result.mergedPermission).toEqual({ bash: "allow" });
|
|
17
|
+
expect(result.origins.get("bash")?.get("*")).toBe("global");
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("attributes each pattern of an object surface value to the contributing scope", () => {
|
|
21
|
+
const result = mergeScopesWithOrigins([
|
|
22
|
+
[
|
|
23
|
+
"project",
|
|
24
|
+
{ permission: { bash: { "git *": "allow", "npm *": "deny" } } },
|
|
25
|
+
],
|
|
26
|
+
]);
|
|
27
|
+
expect(result.mergedPermission).toEqual({
|
|
28
|
+
bash: { "git *": "allow", "npm *": "deny" },
|
|
29
|
+
});
|
|
30
|
+
expect(result.origins.get("bash")?.get("git *")).toBe("project");
|
|
31
|
+
expect(result.origins.get("bash")?.get("npm *")).toBe("project");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it(
|
|
35
|
+
"shallow-merge: patterns not redefined by the higher scope keep their lower-scope origin;" +
|
|
36
|
+
" patterns the higher scope defines switch to the higher scope",
|
|
37
|
+
() => {
|
|
38
|
+
const result = mergeScopesWithOrigins([
|
|
39
|
+
[
|
|
40
|
+
"global",
|
|
41
|
+
{ permission: { bash: { "ls *": "allow", "git *": "allow" } } },
|
|
42
|
+
],
|
|
43
|
+
["project", { permission: { bash: { "git *": "deny" } } }],
|
|
44
|
+
]);
|
|
45
|
+
expect(result.mergedPermission).toEqual({
|
|
46
|
+
bash: { "ls *": "allow", "git *": "deny" },
|
|
47
|
+
});
|
|
48
|
+
// "ls *" was not touched by project — retains global attribution
|
|
49
|
+
expect(result.origins.get("bash")?.get("ls *")).toBe("global");
|
|
50
|
+
// "git *" was overridden by project — switches to project attribution
|
|
51
|
+
expect(result.origins.get("bash")?.get("git *")).toBe("project");
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
it("full replacement (string over object): higher scope re-attributes the entire surface to its own origin", () => {
|
|
56
|
+
const result = mergeScopesWithOrigins([
|
|
57
|
+
["global", { permission: { bash: { "ls *": "allow" } } }],
|
|
58
|
+
["project", { permission: { bash: "deny" } }],
|
|
59
|
+
]);
|
|
60
|
+
expect(result.mergedPermission).toEqual({ bash: "deny" });
|
|
61
|
+
// The string value produces a single "*" pattern for the replacing scope
|
|
62
|
+
expect(result.origins.get("bash")?.get("*")).toBe("project");
|
|
63
|
+
// The former "ls *" pattern from global is gone — origins are replaced, not merged
|
|
64
|
+
expect(result.origins.get("bash")?.has("ls *")).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("full replacement (object over string): higher scope re-attributes the entire surface to its own origin", () => {
|
|
68
|
+
const result = mergeScopesWithOrigins([
|
|
69
|
+
["global", { permission: { bash: "ask" } }],
|
|
70
|
+
["project", { permission: { bash: { "git *": "deny" } } }],
|
|
71
|
+
]);
|
|
72
|
+
expect(result.mergedPermission).toEqual({ bash: { "git *": "deny" } });
|
|
73
|
+
// The object value attributes each pattern to the replacing scope
|
|
74
|
+
expect(result.origins.get("bash")?.get("git *")).toBe("project");
|
|
75
|
+
// The former "*" attribution from global is gone
|
|
76
|
+
expect(result.origins.get("bash")?.has("*")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("applies four-scope precedence in lowest→highest order (global → project → agent → project-agent)", () => {
|
|
80
|
+
const result = mergeScopesWithOrigins([
|
|
81
|
+
["global", { permission: { read: "ask" } }],
|
|
82
|
+
["project", { permission: { write: "deny" } }],
|
|
83
|
+
["agent", { permission: { bash: "deny" } }],
|
|
84
|
+
["project-agent", { permission: { mcp: "allow" } }],
|
|
85
|
+
]);
|
|
86
|
+
expect(result.mergedPermission).toEqual({
|
|
87
|
+
read: "ask",
|
|
88
|
+
write: "deny",
|
|
89
|
+
bash: "deny",
|
|
90
|
+
mcp: "allow",
|
|
91
|
+
});
|
|
92
|
+
expect(result.origins.get("read")?.get("*")).toBe("global");
|
|
93
|
+
expect(result.origins.get("write")?.get("*")).toBe("project");
|
|
94
|
+
expect(result.origins.get("bash")?.get("*")).toBe("agent");
|
|
95
|
+
expect(result.origins.get("mcp")?.get("*")).toBe("project-agent");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("skips scopes with no permission key, contributing nothing to either map", () => {
|
|
99
|
+
const result = mergeScopesWithOrigins([
|
|
100
|
+
["global", {}],
|
|
101
|
+
["project", { permission: { bash: "allow" } }],
|
|
102
|
+
]);
|
|
103
|
+
expect(result.mergedPermission).toEqual({ bash: "allow" });
|
|
104
|
+
expect(result.origins.get("bash")?.get("*")).toBe("project");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("attributes the universal '*' surface like any other (downstream reads origins.get('*')?.get('*') for universalFallbackOrigin)", () => {
|
|
108
|
+
const result = mergeScopesWithOrigins([
|
|
109
|
+
["global", { permission: { "*": "deny" } }],
|
|
110
|
+
["project", { permission: { "*": "allow" } }],
|
|
111
|
+
]);
|
|
112
|
+
expect(result.mergedPermission).toEqual({ "*": "allow" });
|
|
113
|
+
// Both scopes write a string — each is a full replacement; project wins last
|
|
114
|
+
expect(result.origins.get("*")?.get("*")).toBe("project");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { SessionApproval } from "#src/session-approval";
|
|
4
|
+
|
|
5
|
+
describe("SessionApproval", () => {
|
|
6
|
+
describe("single", () => {
|
|
7
|
+
it("stores surface and one pattern", () => {
|
|
8
|
+
const approval = SessionApproval.single("bash", "git *");
|
|
9
|
+
expect(approval.surface).toBe("bash");
|
|
10
|
+
expect(approval.patterns).toEqual(["git *"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("representativePattern returns the pattern", () => {
|
|
14
|
+
const approval = SessionApproval.single("bash", "git *");
|
|
15
|
+
expect(approval.representativePattern).toBe("git *");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("toGateApproval returns { surface, pattern }", () => {
|
|
19
|
+
const approval = SessionApproval.single("bash", "git *");
|
|
20
|
+
expect(approval.toGateApproval()).toEqual({
|
|
21
|
+
surface: "bash",
|
|
22
|
+
pattern: "git *",
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("multiple", () => {
|
|
28
|
+
it("stores surface and all patterns", () => {
|
|
29
|
+
const approval = SessionApproval.multiple("external_directory", [
|
|
30
|
+
"/outside/a/*",
|
|
31
|
+
"/outside/b/*",
|
|
32
|
+
]);
|
|
33
|
+
expect(approval.surface).toBe("external_directory");
|
|
34
|
+
expect(approval.patterns).toEqual(["/outside/a/*", "/outside/b/*"]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("representativePattern returns the first pattern", () => {
|
|
38
|
+
const approval = SessionApproval.multiple("external_directory", [
|
|
39
|
+
"/outside/a/*",
|
|
40
|
+
"/outside/b/*",
|
|
41
|
+
]);
|
|
42
|
+
expect(approval.representativePattern).toBe("/outside/a/*");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("toGateApproval returns { surface, pattern } using the first pattern", () => {
|
|
46
|
+
const approval = SessionApproval.multiple("external_directory", [
|
|
47
|
+
"/outside/a/*",
|
|
48
|
+
"/outside/b/*",
|
|
49
|
+
]);
|
|
50
|
+
expect(approval.toGateApproval()).toEqual({
|
|
51
|
+
surface: "external_directory",
|
|
52
|
+
pattern: "/outside/a/*",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("defensive copy — mutating the source array does not affect patterns", () => {
|
|
57
|
+
const source = ["/outside/a/*", "/outside/b/*"];
|
|
58
|
+
const approval = SessionApproval.multiple("external_directory", source);
|
|
59
|
+
source.push("/outside/c/*");
|
|
60
|
+
expect(approval.patterns).toEqual(["/outside/a/*", "/outside/b/*"]);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("empty patterns (degenerate case)", () => {
|
|
65
|
+
it("representativePattern returns undefined", () => {
|
|
66
|
+
const approval = SessionApproval.multiple("external_directory", []);
|
|
67
|
+
expect(approval.representativePattern).toBeUndefined();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("toGateApproval returns undefined", () => {
|
|
71
|
+
const approval = SessionApproval.multiple("external_directory", []);
|
|
72
|
+
expect(approval.toGateApproval()).toBeUndefined();
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import { evaluate } from "#src/rule";
|
|
4
|
+
import { SessionApproval } from "#src/session-approval";
|
|
4
5
|
import { deriveApprovalPattern, SessionRules } from "#src/session-rules";
|
|
5
6
|
|
|
6
7
|
// ── SessionRules ───────────────────────────────────────────────────────────
|
|
@@ -66,6 +67,54 @@ describe("SessionRules", () => {
|
|
|
66
67
|
});
|
|
67
68
|
});
|
|
68
69
|
|
|
70
|
+
describe("record", () => {
|
|
71
|
+
it("records a single-pattern approval as one rule", () => {
|
|
72
|
+
const rules = new SessionRules();
|
|
73
|
+
rules.record(SessionApproval.single("bash", "git *"));
|
|
74
|
+
expect(rules.getRuleset()).toEqual([
|
|
75
|
+
{
|
|
76
|
+
surface: "bash",
|
|
77
|
+
pattern: "git *",
|
|
78
|
+
action: "allow",
|
|
79
|
+
layer: "session",
|
|
80
|
+
origin: "session",
|
|
81
|
+
},
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("records a multi-pattern approval as one rule per pattern", () => {
|
|
86
|
+
const rules = new SessionRules();
|
|
87
|
+
rules.record(
|
|
88
|
+
SessionApproval.multiple("external_directory", [
|
|
89
|
+
"/outside/a/*",
|
|
90
|
+
"/outside/b/*",
|
|
91
|
+
]),
|
|
92
|
+
);
|
|
93
|
+
expect(rules.getRuleset()).toHaveLength(2);
|
|
94
|
+
expect(rules.getRuleset()[0].pattern).toBe("/outside/a/*");
|
|
95
|
+
expect(rules.getRuleset()[1].pattern).toBe("/outside/b/*");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("records each rule with the correct surface", () => {
|
|
99
|
+
const rules = new SessionRules();
|
|
100
|
+
rules.record(
|
|
101
|
+
SessionApproval.multiple("external_directory", [
|
|
102
|
+
"/outside/a/*",
|
|
103
|
+
"/outside/b/*",
|
|
104
|
+
]),
|
|
105
|
+
);
|
|
106
|
+
for (const rule of rules.getRuleset()) {
|
|
107
|
+
expect(rule.surface).toBe("external_directory");
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("records nothing for an empty patterns list", () => {
|
|
112
|
+
const rules = new SessionRules();
|
|
113
|
+
rules.record(SessionApproval.multiple("external_directory", []));
|
|
114
|
+
expect(rules.getRuleset()).toEqual([]);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
69
118
|
describe("evaluate() integration", () => {
|
|
70
119
|
it("returns allow for a path under an approved directory", () => {
|
|
71
120
|
const session = new SessionRules();
|
|
@@ -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
|
-
});
|