@gotgenes/pi-permission-system 6.0.1 → 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 +34 -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
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { describe, expect, it, vi } from "vitest";
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
formatExternalDirectoryDenyReason,
|
|
17
|
-
formatExternalDirectoryHardStopHint,
|
|
18
|
-
formatExternalDirectoryUserDeniedReason,
|
|
19
|
-
} from "../../src/handlers/gates/external-directory-messages";
|
|
14
|
+
import { EXTENSION_TAG } from "../../src/denial-messages";
|
|
15
|
+
import { formatExternalDirectoryAskPrompt } from "../../src/handlers/gates/external-directory-messages";
|
|
20
16
|
import { PermissionGateHandler } from "../../src/handlers/permission-gate-handler";
|
|
21
17
|
import {
|
|
22
18
|
PERMISSIONS_DECISION_CHANNEL,
|
|
@@ -177,11 +173,6 @@ function getDecisionEvents(
|
|
|
177
173
|
// ── Regression guard: helper presence ──────────────────────────────────────
|
|
178
174
|
|
|
179
175
|
describe("external_directory helper regression guard", () => {
|
|
180
|
-
it("formatExternalDirectoryHardStopHint is a callable function", () => {
|
|
181
|
-
expect(typeof formatExternalDirectoryHardStopHint).toBe("function");
|
|
182
|
-
expect(formatExternalDirectoryHardStopHint()).toContain("Hard stop");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
176
|
it("formatExternalDirectoryAskPrompt is a callable function", () => {
|
|
186
177
|
expect(typeof formatExternalDirectoryAskPrompt).toBe("function");
|
|
187
178
|
expect(
|
|
@@ -189,19 +180,13 @@ describe("external_directory helper regression guard", () => {
|
|
|
189
180
|
).toContain("/outside/file");
|
|
190
181
|
});
|
|
191
182
|
|
|
192
|
-
it("
|
|
193
|
-
expect(
|
|
194
|
-
expect(
|
|
195
|
-
formatExternalDirectoryDenyReason("read", "/outside/file", "/project"),
|
|
196
|
-
).toContain("Hard stop");
|
|
183
|
+
it("EXTENSION_TAG is the expected value", () => {
|
|
184
|
+
expect(EXTENSION_TAG).toBe("[pi-permission-system]");
|
|
197
185
|
});
|
|
198
186
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
formatExternalDirectoryUserDeniedReason("read", "/outside/file"),
|
|
203
|
-
).toContain("User denied");
|
|
204
|
-
});
|
|
187
|
+
// formatExternalDirectoryDenyReason, formatExternalDirectoryUserDeniedReason,
|
|
188
|
+
// and formatExternalDirectoryHardStopHint have moved to denial-messages.ts.
|
|
189
|
+
// Their behavior is tested in denial-messages.test.ts.
|
|
205
190
|
});
|
|
206
191
|
|
|
207
192
|
// ── Path scope: gate applicability ────────────────────────────────────────
|
|
@@ -386,13 +371,14 @@ describe("external_directory policy state — deny", () => {
|
|
|
386
371
|
expect(result.reason).toContain(EXTERNAL_PATH);
|
|
387
372
|
});
|
|
388
373
|
|
|
389
|
-
it("block reason contains
|
|
374
|
+
it("block reason contains extension attribution", async () => {
|
|
390
375
|
const { handler } = makeHandler({
|
|
391
376
|
session: { checkPermission: makeCheckPermission("deny") },
|
|
392
377
|
});
|
|
393
378
|
const event = makeToolCallEvent("read", { path: EXTERNAL_PATH });
|
|
394
379
|
const result = await handler.handleToolCall(event, makeCtx());
|
|
395
|
-
expect(result.reason).toContain("
|
|
380
|
+
expect(result.reason).toContain("[pi-permission-system]");
|
|
381
|
+
expect(result.reason).not.toContain("Hard stop");
|
|
396
382
|
});
|
|
397
383
|
|
|
398
384
|
it("writes review-log entry with resolution policy_denied", async () => {
|
|
@@ -142,15 +142,18 @@ describe("describeBashExternalDirectoryGate", () => {
|
|
|
142
142
|
expect(desc.decision.surface).toBe("external_directory");
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
-
it("
|
|
145
|
+
it("denialContext contains the command and external paths", async () => {
|
|
146
146
|
const result = await describeBashExternalDirectoryGate(
|
|
147
147
|
makeTcc({ input: { command: "cat /outside/file.ts" } }),
|
|
148
148
|
vi.fn().mockReturnValue(makeCheckResult("ask")),
|
|
149
149
|
vi.fn().mockReturnValue([]),
|
|
150
150
|
);
|
|
151
151
|
const desc = result as GateDescriptor;
|
|
152
|
-
expect(desc.
|
|
153
|
-
|
|
152
|
+
expect(desc.denialContext).toMatchObject({
|
|
153
|
+
kind: "bash_external_directory",
|
|
154
|
+
command: "cat /outside/file.ts",
|
|
155
|
+
cwd: "/test/project",
|
|
156
|
+
});
|
|
154
157
|
});
|
|
155
158
|
|
|
156
159
|
it("promptDetails includes command and tool_call source", async () => {
|
|
@@ -144,7 +144,11 @@ describe("describeBashPathGate", () => {
|
|
|
144
144
|
checkPermission,
|
|
145
145
|
getSessionRuleset,
|
|
146
146
|
)) as GateDescriptor;
|
|
147
|
-
expect(result.
|
|
147
|
+
expect(result.denialContext).toMatchObject({
|
|
148
|
+
kind: "bash_path",
|
|
149
|
+
command: "cat .env",
|
|
150
|
+
pathValue: ".env",
|
|
151
|
+
});
|
|
148
152
|
expect(result.promptDetails.message).toContain(".env");
|
|
149
153
|
});
|
|
150
154
|
|
|
@@ -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
|
}
|