@gotgenes/pi-permission-system 6.0.2 → 7.0.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 +27 -0
- package/package.json +5 -5
- package/src/denial-messages.ts +179 -0
- package/src/handlers/gates/bash-external-directory.ts +7 -19
- package/src/handlers/gates/bash-path.ts +6 -14
- package/src/handlers/gates/descriptor.ts +4 -10
- package/src/handlers/gates/external-directory-messages.ts +0 -34
- package/src/handlers/gates/external-directory.ts +7 -19
- package/src/handlers/gates/path.ts +5 -22
- package/src/handlers/gates/runner.ts +14 -1
- package/src/handlers/gates/skill-read.ts +6 -17
- package/src/handlers/gates/tool.ts +6 -22
- package/src/permission-prompts.ts +5 -60
- package/tests/bash-external-directory.test.ts +12 -30
- package/tests/denial-messages.test.ts +517 -0
- package/tests/handlers/external-directory-integration.test.ts +10 -24
- package/tests/handlers/gates/bash-external-directory.test.ts +6 -3
- package/tests/handlers/gates/bash-path.test.ts +5 -1
- package/tests/handlers/gates/external-directory-messages.test.ts +4 -80
- package/tests/handlers/gates/external-directory.test.ts +8 -6
- package/tests/handlers/gates/path.test.ts +7 -3
- package/tests/handlers/gates/runner.test.ts +105 -4
- package/tests/handlers/gates/skill-read.test.ts +6 -7
- package/tests/handlers/gates/tool.test.ts +19 -37
- package/tests/permission-prompts.test.ts +2 -107
- package/tests/permission-system.test.ts +11 -7
|
@@ -2,20 +2,13 @@ import { describe, expect, test } from "vitest";
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
formatBashExternalDirectoryAskPrompt,
|
|
5
|
-
formatBashExternalDirectoryDenyReason,
|
|
6
5
|
formatExternalDirectoryAskPrompt,
|
|
7
|
-
formatExternalDirectoryDenyReason,
|
|
8
|
-
formatExternalDirectoryHardStopHint,
|
|
9
|
-
formatExternalDirectoryUserDeniedReason,
|
|
10
6
|
} from "../../../src/handlers/gates/external-directory-messages";
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
expect(hint).toContain("external directory");
|
|
17
|
-
});
|
|
18
|
-
});
|
|
8
|
+
// Denial message functions (formatExternalDirectoryDenyReason,
|
|
9
|
+
// formatExternalDirectoryUserDeniedReason, formatExternalDirectoryHardStopHint,
|
|
10
|
+
// formatBashExternalDirectoryDenyReason) have moved to denial-messages.ts.
|
|
11
|
+
// Their behavior is tested in denial-messages.test.ts.
|
|
19
12
|
|
|
20
13
|
describe("formatExternalDirectoryAskPrompt", () => {
|
|
21
14
|
test("uses 'Current agent' when no agent name provided", () => {
|
|
@@ -43,60 +36,6 @@ describe("formatExternalDirectoryAskPrompt", () => {
|
|
|
43
36
|
});
|
|
44
37
|
});
|
|
45
38
|
|
|
46
|
-
describe("formatExternalDirectoryDenyReason", () => {
|
|
47
|
-
test("includes tool name, path, cwd, agent name, and hard stop hint", () => {
|
|
48
|
-
const result = formatExternalDirectoryDenyReason(
|
|
49
|
-
"read",
|
|
50
|
-
"/etc/passwd",
|
|
51
|
-
"/projects/my-app",
|
|
52
|
-
"sec-agent",
|
|
53
|
-
);
|
|
54
|
-
expect(result).toContain("Agent 'sec-agent'");
|
|
55
|
-
expect(result).toContain("read");
|
|
56
|
-
expect(result).toContain("/etc/passwd");
|
|
57
|
-
expect(result).toContain("/projects/my-app");
|
|
58
|
-
expect(result).toContain("Hard stop");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("uses 'Current agent' without agent name", () => {
|
|
62
|
-
const result = formatExternalDirectoryDenyReason(
|
|
63
|
-
"read",
|
|
64
|
-
"/etc",
|
|
65
|
-
"/projects",
|
|
66
|
-
);
|
|
67
|
-
expect(result).toContain("Current agent");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
describe("formatExternalDirectoryUserDeniedReason", () => {
|
|
72
|
-
test("includes tool name and path", () => {
|
|
73
|
-
const result = formatExternalDirectoryUserDeniedReason(
|
|
74
|
-
"edit",
|
|
75
|
-
"/etc/hosts",
|
|
76
|
-
);
|
|
77
|
-
expect(result).toContain("edit");
|
|
78
|
-
expect(result).toContain("/etc/hosts");
|
|
79
|
-
expect(result).toContain("Hard stop");
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test("appends denial reason when provided", () => {
|
|
83
|
-
const result = formatExternalDirectoryUserDeniedReason(
|
|
84
|
-
"edit",
|
|
85
|
-
"/etc/hosts",
|
|
86
|
-
"too risky",
|
|
87
|
-
);
|
|
88
|
-
expect(result).toContain("Reason: too risky");
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test("omits reason suffix when not provided", () => {
|
|
92
|
-
const result = formatExternalDirectoryUserDeniedReason(
|
|
93
|
-
"edit",
|
|
94
|
-
"/etc/hosts",
|
|
95
|
-
);
|
|
96
|
-
expect(result).not.toContain("Reason:");
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
39
|
describe("formatBashExternalDirectoryAskPrompt", () => {
|
|
101
40
|
test("includes command, paths, cwd, and agent name", () => {
|
|
102
41
|
const result = formatBashExternalDirectoryAskPrompt(
|
|
@@ -120,18 +59,3 @@ describe("formatBashExternalDirectoryAskPrompt", () => {
|
|
|
120
59
|
expect(result).toContain("Current agent");
|
|
121
60
|
});
|
|
122
61
|
});
|
|
123
|
-
|
|
124
|
-
describe("formatBashExternalDirectoryDenyReason", () => {
|
|
125
|
-
test("includes command, paths, cwd, agent name, and hard stop hint", () => {
|
|
126
|
-
const result = formatBashExternalDirectoryDenyReason(
|
|
127
|
-
"rm /etc/hosts",
|
|
128
|
-
["/etc/hosts"],
|
|
129
|
-
"/projects/my-app",
|
|
130
|
-
"sec-agent",
|
|
131
|
-
);
|
|
132
|
-
expect(result).toContain("Agent 'sec-agent'");
|
|
133
|
-
expect(result).toContain("rm /etc/hosts");
|
|
134
|
-
expect(result).toContain("/etc/hosts");
|
|
135
|
-
expect(result).toContain("Hard stop");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
4
|
GateBypass,
|
|
@@ -136,15 +136,17 @@ describe("describeExternalDirectoryGate", () => {
|
|
|
136
136
|
expect(result.sessionApproval).toHaveProperty("pattern");
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
it("
|
|
139
|
+
it("denialContext contains the external path and cwd", () => {
|
|
140
140
|
const result = describeExternalDirectoryGate(
|
|
141
141
|
makeTcc({ input: { path: "/outside/project/file.ts" } }),
|
|
142
142
|
["/test/agent"],
|
|
143
143
|
) as GateDescriptor;
|
|
144
|
-
expect(result.
|
|
145
|
-
|
|
146
|
-
"
|
|
147
|
-
|
|
144
|
+
expect(result.denialContext).toMatchObject({
|
|
145
|
+
kind: "external_directory",
|
|
146
|
+
toolName: "read",
|
|
147
|
+
pathValue: "/outside/project/file.ts",
|
|
148
|
+
cwd: "/test/project",
|
|
149
|
+
});
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
it("promptDetails includes path and tool_call source", () => {
|
|
@@ -164,7 +164,7 @@ describe("describePathGate", () => {
|
|
|
164
164
|
expect(result.sessionApproval).toHaveProperty("pattern");
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
-
it("descriptor
|
|
167
|
+
it("descriptor denialContext references the file path and tool name", () => {
|
|
168
168
|
const checkPermission = vi
|
|
169
169
|
.fn<CheckPermissionFn>()
|
|
170
170
|
.mockReturnValue(
|
|
@@ -175,8 +175,12 @@ describe("describePathGate", () => {
|
|
|
175
175
|
checkPermission,
|
|
176
176
|
getSessionRuleset,
|
|
177
177
|
) as GateDescriptor;
|
|
178
|
-
expect(result.
|
|
179
|
-
|
|
178
|
+
expect(result.denialContext).toEqual({
|
|
179
|
+
kind: "path",
|
|
180
|
+
toolName: "read",
|
|
181
|
+
pathValue: ".env",
|
|
182
|
+
agentName: undefined,
|
|
183
|
+
});
|
|
180
184
|
});
|
|
181
185
|
|
|
182
186
|
it("descriptor decision uses surface 'path' and the file path as value", () => {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
+
import type { DenialContext } from "../../../src/denial-messages";
|
|
4
|
+
import { EXTENSION_TAG } from "../../../src/denial-messages";
|
|
3
5
|
import type {
|
|
4
6
|
GateDescriptor,
|
|
5
7
|
GateRunnerDeps,
|
|
@@ -15,10 +17,9 @@ function makeDescriptor(
|
|
|
15
17
|
return {
|
|
16
18
|
surface: "read",
|
|
17
19
|
input: {},
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
userDeniedReason: (d) => `User denied. ${d.denialReason ?? ""}`,
|
|
20
|
+
denialContext: {
|
|
21
|
+
kind: "tool",
|
|
22
|
+
check: makeCheckResult("deny"),
|
|
22
23
|
},
|
|
23
24
|
promptDetails: {
|
|
24
25
|
source: "tool_call",
|
|
@@ -358,4 +359,104 @@ describe("runGateCheck", () => {
|
|
|
358
359
|
await runGateCheck(makeDescriptor(), null, "tc-1", deps);
|
|
359
360
|
expect(deps.approveSessionRule).not.toHaveBeenCalled();
|
|
360
361
|
});
|
|
362
|
+
|
|
363
|
+
describe("denialContext formatting", () => {
|
|
364
|
+
function makeDenialContextDescriptor(
|
|
365
|
+
denialContext: DenialContext,
|
|
366
|
+
overrides: Partial<GateDescriptor> = {},
|
|
367
|
+
): GateDescriptor {
|
|
368
|
+
return {
|
|
369
|
+
surface: "write",
|
|
370
|
+
input: {},
|
|
371
|
+
denialContext,
|
|
372
|
+
promptDetails: {
|
|
373
|
+
source: "tool_call",
|
|
374
|
+
agentName: null,
|
|
375
|
+
message: "Allow tool 'write'?",
|
|
376
|
+
toolCallId: "tc-1",
|
|
377
|
+
toolName: "write",
|
|
378
|
+
},
|
|
379
|
+
logContext: {
|
|
380
|
+
source: "tool_call",
|
|
381
|
+
toolCallId: "tc-1",
|
|
382
|
+
toolName: "write",
|
|
383
|
+
},
|
|
384
|
+
decision: {
|
|
385
|
+
surface: "write",
|
|
386
|
+
value: "write",
|
|
387
|
+
},
|
|
388
|
+
...overrides,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
it("uses denialContext to format denyReason with extension tag", async () => {
|
|
393
|
+
const deps = makeRunnerDeps({
|
|
394
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("deny")),
|
|
395
|
+
});
|
|
396
|
+
const ctx: DenialContext = {
|
|
397
|
+
kind: "tool",
|
|
398
|
+
check: makeCheckResult("deny"),
|
|
399
|
+
agentName: "test-agent",
|
|
400
|
+
};
|
|
401
|
+
const result = await runGateCheck(
|
|
402
|
+
makeDenialContextDescriptor(ctx),
|
|
403
|
+
"test-agent",
|
|
404
|
+
"tc-1",
|
|
405
|
+
deps,
|
|
406
|
+
);
|
|
407
|
+
expect(result.action).toBe("block");
|
|
408
|
+
if (result.action === "block") {
|
|
409
|
+
expect(result.reason).toContain(EXTENSION_TAG);
|
|
410
|
+
expect(result.reason).not.toContain("Hard stop");
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("uses denialContext to format unavailableReason with extension tag", async () => {
|
|
415
|
+
const deps = makeRunnerDeps({
|
|
416
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
417
|
+
canConfirm: vi.fn().mockReturnValue(false),
|
|
418
|
+
});
|
|
419
|
+
const ctx: DenialContext = {
|
|
420
|
+
kind: "tool",
|
|
421
|
+
check: makeCheckResult("ask"),
|
|
422
|
+
};
|
|
423
|
+
const result = await runGateCheck(
|
|
424
|
+
makeDenialContextDescriptor(ctx),
|
|
425
|
+
null,
|
|
426
|
+
"tc-1",
|
|
427
|
+
deps,
|
|
428
|
+
);
|
|
429
|
+
expect(result.action).toBe("block");
|
|
430
|
+
if (result.action === "block") {
|
|
431
|
+
expect(result.reason).toContain(EXTENSION_TAG);
|
|
432
|
+
expect(result.reason).toContain("no interactive UI");
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("uses denialContext to format userDeniedReason with extension tag", async () => {
|
|
437
|
+
const deps = makeRunnerDeps({
|
|
438
|
+
checkPermission: vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
439
|
+
promptPermission: vi.fn().mockResolvedValue({
|
|
440
|
+
approved: false,
|
|
441
|
+
state: "denied",
|
|
442
|
+
denialReason: "too risky",
|
|
443
|
+
}),
|
|
444
|
+
});
|
|
445
|
+
const ctx: DenialContext = {
|
|
446
|
+
kind: "tool",
|
|
447
|
+
check: makeCheckResult("ask"),
|
|
448
|
+
};
|
|
449
|
+
const result = await runGateCheck(
|
|
450
|
+
makeDenialContextDescriptor(ctx),
|
|
451
|
+
null,
|
|
452
|
+
"tc-1",
|
|
453
|
+
deps,
|
|
454
|
+
);
|
|
455
|
+
expect(result.action).toBe("block");
|
|
456
|
+
if (result.action === "block") {
|
|
457
|
+
expect(result.reason).toContain(EXTENSION_TAG);
|
|
458
|
+
expect(result.reason).toContain("too risky");
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
});
|
|
361
462
|
});
|
|
@@ -104,17 +104,16 @@ describe("describeSkillReadGate", () => {
|
|
|
104
104
|
expect(result.decision.value).toBe("my-skill");
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
it("
|
|
107
|
+
it("denialContext contains the skill name and read path", () => {
|
|
108
108
|
const result = describeSkillReadGate(makeTcc(), () => [
|
|
109
109
|
makeSkillEntry({ name: "librarian" }),
|
|
110
110
|
]) as GateDescriptor;
|
|
111
|
-
expect(result.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
expect(result.denialContext).toEqual({
|
|
112
|
+
kind: "skill_read",
|
|
113
|
+
skillName: "librarian",
|
|
114
|
+
readPath: "/skills/librarian/SKILL.md",
|
|
115
|
+
agentName: undefined,
|
|
116
116
|
});
|
|
117
|
-
expect(deniedMsg).toContain("librarian");
|
|
118
117
|
});
|
|
119
118
|
|
|
120
119
|
it("promptDetails includes skill_read source and skillName", () => {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { describe, expect, it
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
2
|
|
|
3
|
-
import type { GateDescriptor } from "../../../src/handlers/gates/descriptor";
|
|
4
3
|
import { describeToolGate } from "../../../src/handlers/gates/tool";
|
|
5
4
|
import type { ToolCallContext } from "../../../src/handlers/gates/types";
|
|
6
5
|
import type { PermissionCheckResult } from "../../../src/types";
|
|
@@ -80,50 +79,33 @@ describe("describeToolGate", () => {
|
|
|
80
79
|
expect(desc.decision.value).toBe("server:tool");
|
|
81
80
|
});
|
|
82
81
|
|
|
83
|
-
it("populates
|
|
82
|
+
it("populates denialContext with kind 'tool' and check result", () => {
|
|
84
83
|
const check = makeCheckResult("deny", { toolName: "read" });
|
|
85
84
|
const desc = describeToolGate(makeTcc(), check);
|
|
86
|
-
expect(desc.
|
|
87
|
-
|
|
85
|
+
expect(desc.denialContext).toEqual({
|
|
86
|
+
kind: "tool",
|
|
87
|
+
check,
|
|
88
|
+
agentName: undefined,
|
|
89
|
+
input: {},
|
|
90
|
+
});
|
|
88
91
|
});
|
|
89
92
|
|
|
90
|
-
it("populates
|
|
91
|
-
const check = makeCheckResult("ask", {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
95
|
-
const desc = describeToolGate(
|
|
96
|
-
makeTcc({ toolName: "bash", input: { command: "rm -rf /" } }),
|
|
97
|
-
check,
|
|
98
|
-
);
|
|
99
|
-
expect(desc.messages.unavailableReason).toContain("rm -rf /");
|
|
100
|
-
expect(desc.messages.unavailableReason).toContain("no interactive UI");
|
|
93
|
+
it("populates denialContext with agent name when provided", () => {
|
|
94
|
+
const check = makeCheckResult("ask", { toolName: "read" });
|
|
95
|
+
const desc = describeToolGate(makeTcc({ agentName: "my-agent" }), check);
|
|
96
|
+
expect(desc.denialContext!.agentName).toBe("my-agent");
|
|
101
97
|
});
|
|
102
98
|
|
|
103
|
-
it("populates
|
|
99
|
+
it("populates denialContext with input for tool context", () => {
|
|
100
|
+
const check = makeCheckResult("ask", { toolName: "bash", command: "ls" });
|
|
104
101
|
const desc = describeToolGate(
|
|
105
|
-
makeTcc({ toolName: "
|
|
106
|
-
|
|
102
|
+
makeTcc({ toolName: "bash", input: { command: "ls" } }),
|
|
103
|
+
check,
|
|
107
104
|
);
|
|
108
|
-
expect(desc.
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
it("populates messages.unavailableReason with mcp for mcp tool", () => {
|
|
113
|
-
const check = makeCheckResult("ask", { toolName: "mcp", target: "s:t" });
|
|
114
|
-
const desc = describeToolGate(makeTcc({ toolName: "mcp" }), check);
|
|
115
|
-
expect(desc.messages.unavailableReason).toContain("mcp");
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("populates messages.userDeniedReason as a function", () => {
|
|
119
|
-
const check = makeCheckResult("ask", { toolName: "read" });
|
|
120
|
-
const desc = describeToolGate(makeTcc(), check);
|
|
121
|
-
const reason = desc.messages.userDeniedReason({
|
|
122
|
-
approved: false,
|
|
123
|
-
state: "denied",
|
|
124
|
-
denialReason: "too risky",
|
|
105
|
+
expect(desc.denialContext).toMatchObject({
|
|
106
|
+
kind: "tool",
|
|
107
|
+
input: { command: "ls" },
|
|
125
108
|
});
|
|
126
|
-
expect(reason).toContain("too risky");
|
|
127
109
|
});
|
|
128
110
|
|
|
129
111
|
it("populates sessionApproval via suggestSessionPattern", () => {
|
|
@@ -7,14 +7,10 @@ vi.mock("../src/tool-input-preview.js", () => ({
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
formatAskPrompt,
|
|
10
|
-
formatDenyReason,
|
|
11
10
|
formatMissingToolNameReason,
|
|
12
|
-
formatPermissionHardStopHint,
|
|
13
11
|
formatSkillAskPrompt,
|
|
14
12
|
formatSkillPathAskPrompt,
|
|
15
|
-
formatSkillPathDenyReason,
|
|
16
13
|
formatUnknownToolReason,
|
|
17
|
-
formatUserDeniedReason,
|
|
18
14
|
} from "../src/permission-prompts";
|
|
19
15
|
import type { SkillPromptEntry } from "../src/skill-prompt-sanitizer";
|
|
20
16
|
import { formatToolInputForPrompt } from "../src/tool-input-preview";
|
|
@@ -106,89 +102,6 @@ describe("formatUnknownToolReason", () => {
|
|
|
106
102
|
});
|
|
107
103
|
});
|
|
108
104
|
|
|
109
|
-
describe("formatPermissionHardStopHint", () => {
|
|
110
|
-
test("returns MCP-specific message for mcp tool with target", () => {
|
|
111
|
-
const result = formatPermissionHardStopHint(mcpResult("server:tool"));
|
|
112
|
-
expect(result).toContain("MCP permission denial");
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
test("returns MCP-specific message for mcp source with target", () => {
|
|
116
|
-
const result = formatPermissionHardStopHint(
|
|
117
|
-
toolResult("anything", { source: "mcp", target: "server:tool" }),
|
|
118
|
-
);
|
|
119
|
-
expect(result).toContain("MCP permission denial");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("returns generic message for non-MCP tools", () => {
|
|
123
|
-
const result = formatPermissionHardStopHint(toolResult("read"));
|
|
124
|
-
expect(result).toContain("Hard stop");
|
|
125
|
-
expect(result).not.toContain("MCP");
|
|
126
|
-
});
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
describe("formatDenyReason", () => {
|
|
130
|
-
test("includes tool name and hard stop hint", () => {
|
|
131
|
-
const result = formatDenyReason(toolResult("read"));
|
|
132
|
-
expect(result).toContain("read");
|
|
133
|
-
expect(result).toContain("Hard stop");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("includes agent name when provided", () => {
|
|
137
|
-
const result = formatDenyReason(toolResult("write"), "my-agent");
|
|
138
|
-
expect(result).toContain("Agent 'my-agent'");
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
test("includes MCP target for mcp results", () => {
|
|
142
|
-
const result = formatDenyReason(mcpResult("server:do-thing"));
|
|
143
|
-
expect(result).toContain("server:do-thing");
|
|
144
|
-
expect(result).toContain("MCP");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
test("includes bash command when present", () => {
|
|
148
|
-
const result = formatDenyReason(
|
|
149
|
-
toolResult("bash", { command: "rm -rf /" }),
|
|
150
|
-
);
|
|
151
|
-
expect(result).toContain("rm -rf /");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("includes matched pattern when present", () => {
|
|
155
|
-
const result = formatDenyReason(
|
|
156
|
-
toolResult("bash", { command: "rm -rf /", matchedPattern: "rm *" }),
|
|
157
|
-
);
|
|
158
|
-
expect(result).toContain("matched 'rm *'");
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
describe("formatUserDeniedReason", () => {
|
|
163
|
-
test("mentions tool name for generic tools", () => {
|
|
164
|
-
const result = formatUserDeniedReason(toolResult("read"));
|
|
165
|
-
expect(result).toContain("read");
|
|
166
|
-
expect(result).toContain("Hard stop");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test("mentions bash command for bash results", () => {
|
|
170
|
-
const result = formatUserDeniedReason(
|
|
171
|
-
toolResult("bash", { command: "ls -la" }),
|
|
172
|
-
);
|
|
173
|
-
expect(result).toContain("ls -la");
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("mentions MCP target for mcp results", () => {
|
|
177
|
-
const result = formatUserDeniedReason(mcpResult("server:query"));
|
|
178
|
-
expect(result).toContain("server:query");
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
test("appends denial reason when provided", () => {
|
|
182
|
-
const result = formatUserDeniedReason(toolResult("read"), "too sensitive");
|
|
183
|
-
expect(result).toContain("Reason: too sensitive");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test("omits reason suffix when not provided", () => {
|
|
187
|
-
const result = formatUserDeniedReason(toolResult("read"));
|
|
188
|
-
expect(result).not.toContain("Reason:");
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
105
|
describe("formatAskPrompt", () => {
|
|
193
106
|
test("uses 'Current agent' when no agent name given", () => {
|
|
194
107
|
const result = formatAskPrompt(toolResult("read"), undefined, {
|
|
@@ -289,23 +202,5 @@ describe("formatSkillPathAskPrompt", () => {
|
|
|
289
202
|
});
|
|
290
203
|
});
|
|
291
204
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const result = formatSkillPathDenyReason(
|
|
295
|
-
skillEntry("librarian"),
|
|
296
|
-
"/skills/librarian/SKILL.md",
|
|
297
|
-
"my-agent",
|
|
298
|
-
);
|
|
299
|
-
expect(result).toContain("librarian");
|
|
300
|
-
expect(result).toContain("/skills/librarian/SKILL.md");
|
|
301
|
-
expect(result).toContain("Agent 'my-agent'");
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
test("uses 'Current agent' without agent name", () => {
|
|
305
|
-
const result = formatSkillPathDenyReason(
|
|
306
|
-
skillEntry("librarian"),
|
|
307
|
-
"/skills/librarian/SKILL.md",
|
|
308
|
-
);
|
|
309
|
-
expect(result).toContain("Current agent");
|
|
310
|
-
});
|
|
311
|
-
});
|
|
205
|
+
// formatSkillPathDenyReason has moved to denial-messages.ts.
|
|
206
|
+
// Its behavior is tested in denial-messages.test.ts.
|
|
@@ -1874,10 +1874,11 @@ test("tool_call blocks path-bearing tools outside cwd when external_directory is
|
|
|
1874
1874
|
});
|
|
1875
1875
|
|
|
1876
1876
|
expect(result.block).toBe(true);
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
);
|
|
1880
|
-
expect(
|
|
1877
|
+
const reason = String(result.reason);
|
|
1878
|
+
expect(reason).toContain("is not permitted to run tool 'read'");
|
|
1879
|
+
expect(reason).toContain("repo-sibling");
|
|
1880
|
+
expect(reason).toContain("[pi-permission-system]");
|
|
1881
|
+
expect(reason).not.toContain("Hard stop");
|
|
1881
1882
|
} finally {
|
|
1882
1883
|
await harness.cleanup();
|
|
1883
1884
|
rmSync(rootDir, { recursive: true, force: true });
|
|
@@ -2003,10 +2004,13 @@ test("tool_call blocks bash command with external path when external_directory i
|
|
|
2003
2004
|
});
|
|
2004
2005
|
|
|
2005
2006
|
expect(result.block).toBe(true);
|
|
2006
|
-
|
|
2007
|
-
|
|
2007
|
+
const reason = String(result.reason);
|
|
2008
|
+
expect(reason).toContain(
|
|
2009
|
+
"is not permitted to run bash command 'cat /etc/hosts'",
|
|
2008
2010
|
);
|
|
2009
|
-
expect(
|
|
2011
|
+
expect(reason).toContain("/etc/hosts");
|
|
2012
|
+
expect(reason).toContain("[pi-permission-system]");
|
|
2013
|
+
expect(reason).not.toContain("Hard stop");
|
|
2010
2014
|
} finally {
|
|
2011
2015
|
await harness.cleanup();
|
|
2012
2016
|
}
|