@gotgenes/pi-permission-system 6.0.2 → 7.0.1

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.
@@ -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
- formatExternalDirectoryAskPrompt,
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("formatExternalDirectoryDenyReason is a callable function", () => {
193
- expect(typeof formatExternalDirectoryDenyReason).toBe("function");
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
- it("formatExternalDirectoryUserDeniedReason is a callable function", () => {
200
- expect(typeof formatExternalDirectoryUserDeniedReason).toBe("function");
201
- expect(
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 the hard-stop hint", async () => {
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("Hard stop");
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("messages contain the command", async () => {
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.messages.denyReason).toContain("cat /outside/file.ts");
153
- expect(desc.messages.unavailableReason).toContain("cat /outside/file.ts");
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.messages.denyReason).toContain(".env");
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
- describe("formatExternalDirectoryHardStopHint", () => {
13
- test("returns the hard stop instruction string", () => {
14
- const hint = formatExternalDirectoryHardStopHint();
15
- expect(hint).toContain("Hard stop");
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, vi } from "vitest";
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("messages contain the external path", () => {
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.messages.denyReason).toContain("/outside/project/file.ts");
145
- expect(result.messages.unavailableReason).toContain(
146
- "/outside/project/file.ts",
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 messages reference the file path", () => {
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.messages.denyReason).toContain(".env");
179
- expect(result.messages.unavailableReason).toContain(".env");
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
- messages: {
19
- denyReason: "Tool 'read' is denied.",
20
- unavailableReason: "No UI available.",
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("messages contain the skill name", () => {
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.messages.denyReason).toContain("librarian");
112
- expect(result.messages.unavailableReason).toContain("librarian");
113
- const deniedMsg = result.messages.userDeniedReason({
114
- approved: false,
115
- state: "denied",
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, vi } from "vitest";
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 messages.denyReason via formatDenyReason", () => {
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.messages.denyReason).toContain("read");
87
- expect(desc.messages.denyReason).toContain("not permitted");
85
+ expect(desc.denialContext).toEqual({
86
+ kind: "tool",
87
+ check,
88
+ agentName: undefined,
89
+ input: {},
90
+ });
88
91
  });
89
92
 
90
- it("populates messages.unavailableReason with bash command when tool is bash", () => {
91
- const check = makeCheckResult("ask", {
92
- toolName: "bash",
93
- command: "rm -rf /",
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 messages.unavailableReason with tool name for non-bash tools", () => {
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: "write" }),
106
- makeCheckResult("ask"),
102
+ makeTcc({ toolName: "bash", input: { command: "ls" } }),
103
+ check,
107
104
  );
108
- expect(desc.messages.unavailableReason).toContain("write");
109
- expect(desc.messages.unavailableReason).toContain("no interactive UI");
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
- describe("formatSkillPathDenyReason", () => {
293
- test("includes skill name, read path, and agent name", () => {
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
- expect(String(result.reason)).toMatch(
1878
- /external directory permission denial/i,
1879
- );
1880
- expect(String(result.reason)).toMatch(/repo-sibling/);
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
- expect(String(result.reason)).toMatch(
2007
- /external directory permission denial/i,
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(String(result.reason)).toMatch(/\/etc\/hosts/);
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
  }