@gotgenes/pi-permission-system 18.1.1 → 18.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.
Files changed (133) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/docs/configuration.md +11 -0
  3. package/package.json +1 -2
  4. package/src/access-intent/access-path.ts +29 -4
  5. package/src/access-intent/bash/bash-path-resolver.ts +39 -18
  6. package/src/access-intent/bash/msys-bash-tokens.ts +64 -0
  7. package/src/extension-config.ts +7 -0
  8. package/src/forwarded-permissions/permission-forwarder.ts +6 -3
  9. package/src/handlers/gates/helpers.ts +1 -1
  10. package/src/handlers/gates/runner.ts +22 -0
  11. package/src/index.ts +10 -6
  12. package/src/path-normalizer.ts +105 -2
  13. package/src/permission-manager.ts +21 -1
  14. package/src/permission-prompter.ts +10 -16
  15. package/src/prompting-gateway.ts +13 -17
  16. package/src/rule.ts +21 -1
  17. package/src/status.ts +1 -1
  18. package/src/yolo-mode.ts +0 -30
  19. package/test/access-intent/access-path.test.ts +0 -277
  20. package/test/access-intent/bash/node-text.test.ts +0 -148
  21. package/test/access-intent/bash/parser.test.ts +0 -19
  22. package/test/access-intent/bash/program.test.ts +0 -673
  23. package/test/access-intent/bash/token-classification.test.ts +0 -363
  24. package/test/access-intent/bash/token-collection.test.ts +0 -300
  25. package/test/active-agent.test.ts +0 -155
  26. package/test/async-cache.test.ts +0 -48
  27. package/test/bash-arity.test.ts +0 -144
  28. package/test/bash-external-directory.test.ts +0 -1022
  29. package/test/builtin-tool-input-formatters.test.ts +0 -109
  30. package/test/canonicalize-path.test.ts +0 -119
  31. package/test/composition-root.test.ts +0 -698
  32. package/test/config-loader.test.ts +0 -740
  33. package/test/config-modal.test.ts +0 -320
  34. package/test/config-paths.test.ts +0 -83
  35. package/test/config-pipeline.test.ts +0 -90
  36. package/test/config-reporter.test.ts +0 -147
  37. package/test/config-store.test.ts +0 -466
  38. package/test/decision-audit.test.ts +0 -72
  39. package/test/decision-reporter.test.ts +0 -112
  40. package/test/denial-messages.test.ts +0 -714
  41. package/test/detect-permissive-bash-fallback.test.ts +0 -56
  42. package/test/expand-home.test.ts +0 -93
  43. package/test/extension-config.test.ts +0 -129
  44. package/test/extension-paths.test.ts +0 -108
  45. package/test/forwarded-permissions/io.test.ts +0 -251
  46. package/test/forwarding-manager.test.ts +0 -200
  47. package/test/handlers/before-agent-start.test.ts +0 -314
  48. package/test/handlers/external-directory-integration.test.ts +0 -515
  49. package/test/handlers/external-directory-session-dedup.test.ts +0 -175
  50. package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -167
  51. package/test/handlers/gates/bash-command-metamorphic.test.ts +0 -88
  52. package/test/handlers/gates/bash-command.test.ts +0 -257
  53. package/test/handlers/gates/bash-external-directory.test.ts +0 -268
  54. package/test/handlers/gates/bash-path.test.ts +0 -346
  55. package/test/handlers/gates/candidate-check.test.ts +0 -52
  56. package/test/handlers/gates/external-directory-messages.test.ts +0 -85
  57. package/test/handlers/gates/external-directory-policy.test.ts +0 -134
  58. package/test/handlers/gates/external-directory.test.ts +0 -267
  59. package/test/handlers/gates/helpers.test.ts +0 -165
  60. package/test/handlers/gates/path.test.ts +0 -369
  61. package/test/handlers/gates/runner.test.ts +0 -408
  62. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +0 -176
  63. package/test/handlers/gates/skill-input.test.ts +0 -128
  64. package/test/handlers/gates/skill-read.test.ts +0 -161
  65. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +0 -356
  66. package/test/handlers/gates/tool.test.ts +0 -244
  67. package/test/handlers/input-events.test.ts +0 -168
  68. package/test/handlers/input.test.ts +0 -199
  69. package/test/handlers/lifecycle.test.ts +0 -221
  70. package/test/handlers/tool-call-boundary.test.ts +0 -145
  71. package/test/handlers/tool-call-events.test.ts +0 -277
  72. package/test/handlers/tool-call.test.ts +0 -395
  73. package/test/handlers/validate-requested-tool.test.ts +0 -92
  74. package/test/helpers/external-directory-fixtures.ts +0 -269
  75. package/test/helpers/gate-fixtures.ts +0 -316
  76. package/test/helpers/handler-fixtures.ts +0 -335
  77. package/test/helpers/make-fake-pi.ts +0 -100
  78. package/test/helpers/manager-harness.ts +0 -112
  79. package/test/helpers/session-fixtures.ts +0 -199
  80. package/test/input-normalizer.test.ts +0 -325
  81. package/test/logging.test.ts +0 -51
  82. package/test/mcp-targets.test.ts +0 -233
  83. package/test/node-modules-discovery.test.ts +0 -97
  84. package/test/normalize.test.ts +0 -247
  85. package/test/path-containment.test.ts +0 -161
  86. package/test/path-normalization.test.ts +0 -233
  87. package/test/path-normalizer.test.ts +0 -196
  88. package/test/path-surfaces.test.ts +0 -55
  89. package/test/pattern-suggest.test.ts +0 -248
  90. package/test/permission-dialog.test.ts +0 -205
  91. package/test/permission-event-rpc.test.ts +0 -560
  92. package/test/permission-events.test.ts +0 -400
  93. package/test/permission-forwarder.test.ts +0 -370
  94. package/test/permission-forwarding.test.ts +0 -315
  95. package/test/permission-gate.test.ts +0 -269
  96. package/test/permission-manager-unified.test.ts +0 -3714
  97. package/test/permission-merge.test.ts +0 -61
  98. package/test/permission-prompter.test.ts +0 -518
  99. package/test/permission-prompts.test.ts +0 -363
  100. package/test/permission-resolver.test.ts +0 -277
  101. package/test/permission-session.test.ts +0 -404
  102. package/test/permission-ui-prompt.test.ts +0 -146
  103. package/test/permissions-service.test.ts +0 -192
  104. package/test/pi-infrastructure-read.test.ts +0 -432
  105. package/test/policy-loader.test.ts +0 -561
  106. package/test/prompting-gateway.test.ts +0 -231
  107. package/test/rule.test.ts +0 -650
  108. package/test/safe-system-paths.test.ts +0 -46
  109. package/test/scope-merge.test.ts +0 -116
  110. package/test/service-lifecycle.test.ts +0 -163
  111. package/test/service.test.ts +0 -261
  112. package/test/session-approval.test.ts +0 -75
  113. package/test/session-logger.test.ts +0 -200
  114. package/test/session-rules.test.ts +0 -321
  115. package/test/session-start.test.ts +0 -112
  116. package/test/skill-prompt-sanitizer.test.ts +0 -418
  117. package/test/status.test.ts +0 -10
  118. package/test/subagent-context.test.ts +0 -372
  119. package/test/subagent-lifecycle-events.test.ts +0 -132
  120. package/test/subagent-registry.test.ts +0 -145
  121. package/test/synthesize.test.ts +0 -302
  122. package/test/system-prompt-sanitizer.test.ts +0 -382
  123. package/test/tool-access-extractor-registry.test.ts +0 -77
  124. package/test/tool-input-formatter-registry.test.ts +0 -75
  125. package/test/tool-input-path.test.ts +0 -84
  126. package/test/tool-input-preview.test.ts +0 -129
  127. package/test/tool-input-prompt-formatters.test.ts +0 -115
  128. package/test/tool-preview-formatter.test.ts +0 -458
  129. package/test/tool-registry.test.ts +0 -197
  130. package/test/value-guards.test.ts +0 -193
  131. package/test/wildcard-matcher.test.ts +0 -424
  132. package/test/yaml-frontmatter.test.ts +0 -91
  133. package/test/yolo-mode.test.ts +0 -188
@@ -1,302 +0,0 @@
1
- import { describe, expect, test } from "vitest";
2
- import type { RuleOrigin } from "#src/rule";
3
- import { evaluate } from "#src/rule";
4
- import {
5
- composeRuleset,
6
- synthesizeBaseline,
7
- synthesizeDefaults,
8
- } from "#src/synthesize";
9
-
10
- // ── synthesizeDefaults ─────────────────────────────────────────────────────
11
-
12
- describe("synthesizeDefaults", () => {
13
- test("emits a single universal catch-all rule with layer 'default' and origin 'builtin'", () => {
14
- const rules = synthesizeDefaults("ask");
15
- expect(rules).toHaveLength(1);
16
- expect(rules[0]).toEqual({
17
- surface: "*",
18
- pattern: "*",
19
- action: "ask",
20
- layer: "default",
21
- origin: "builtin",
22
- });
23
- });
24
-
25
- test("reflects the supplied PermissionState as the action", () => {
26
- expect(synthesizeDefaults("allow")[0].action).toBe("allow");
27
- expect(synthesizeDefaults("deny")[0].action).toBe("deny");
28
- expect(synthesizeDefaults("ask")[0].action).toBe("ask");
29
- });
30
-
31
- test("universal rule catches any surface via wildcardMatch", () => {
32
- const rules = synthesizeDefaults("ask");
33
- expect(evaluate("read", "*", rules, "linux").action).toBe("ask");
34
- expect(evaluate("bash", "git status", rules, "linux").action).toBe("ask");
35
- expect(evaluate("external_directory", "*", rules, "linux").action).toBe(
36
- "ask",
37
- );
38
- expect(evaluate("future_surface", "*", rules, "linux").action).toBe("ask");
39
- });
40
-
41
- test("universal rule has layer 'default'", () => {
42
- const rules = synthesizeDefaults("allow");
43
- expect(evaluate("read", "*", rules, "linux").layer).toBe("default");
44
- });
45
-
46
- test("defaults to origin 'builtin' when no origin supplied", () => {
47
- const rules = synthesizeDefaults("ask");
48
- expect(rules[0].origin).toBe("builtin");
49
- });
50
-
51
- test("universal rule carries config scope origin when supplied", () => {
52
- const origin: RuleOrigin = "global";
53
- const rules = synthesizeDefaults("ask", origin);
54
- expect(rules[0].origin).toBe("global");
55
- });
56
-
57
- test("origin is preserved through evaluate()", () => {
58
- const rules = synthesizeDefaults("allow", "project");
59
- const result = evaluate("read", "*", rules, "linux");
60
- expect(result.origin).toBe("project");
61
- });
62
-
63
- test("all RuleOrigin values are accepted", () => {
64
- const origins: RuleOrigin[] = [
65
- "global",
66
- "project",
67
- "agent",
68
- "project-agent",
69
- "builtin",
70
- "baseline",
71
- "session",
72
- ];
73
- for (const origin of origins) {
74
- const rules = synthesizeDefaults("ask", origin);
75
- expect(rules[0].origin).toBe(origin);
76
- }
77
- });
78
- });
79
-
80
- // ── synthesizeBaseline ─────────────────────────────────────────────────────
81
-
82
- describe("synthesizeBaseline", () => {
83
- test("returns empty ruleset when config has no mcp allow rules", () => {
84
- const configRules = [
85
- {
86
- surface: "mcp",
87
- pattern: "*",
88
- action: "deny" as const,
89
- layer: "config" as const,
90
- origin: "global" as const,
91
- },
92
- ];
93
- expect(synthesizeBaseline(configRules)).toEqual([]);
94
- });
95
-
96
- test("returns empty ruleset for empty config rules", () => {
97
- expect(synthesizeBaseline([])).toEqual([]);
98
- });
99
-
100
- test("synthesizes 5 baseline rules when at least one mcp allow config rule exists", () => {
101
- const configRules = [
102
- {
103
- surface: "mcp",
104
- pattern: "exa:*",
105
- action: "allow" as const,
106
- layer: "config" as const,
107
- origin: "global" as const,
108
- },
109
- ];
110
- const rules = synthesizeBaseline(configRules);
111
- expect(rules).toHaveLength(5);
112
- });
113
-
114
- test("baseline rules all have layer 'baseline', action 'allow', and origin 'baseline'", () => {
115
- const configRules = [
116
- {
117
- surface: "mcp",
118
- pattern: "exa:*",
119
- action: "allow" as const,
120
- layer: "config" as const,
121
- origin: "global" as const,
122
- },
123
- ];
124
- const rules = synthesizeBaseline(configRules);
125
- for (const rule of rules) {
126
- expect(rule.layer).toBe("baseline");
127
- expect(rule.action).toBe("allow");
128
- expect(rule.surface).toBe("mcp");
129
- expect(rule.origin).toBe("baseline");
130
- }
131
- });
132
-
133
- test("baseline rules cover the 5 MCP metadata targets", () => {
134
- const configRules = [
135
- {
136
- surface: "mcp",
137
- pattern: "exa:*",
138
- action: "allow" as const,
139
- layer: "config" as const,
140
- origin: "global" as const,
141
- },
142
- ];
143
- const rules = synthesizeBaseline(configRules);
144
- const patterns = rules.map((r) => r.pattern);
145
- expect(patterns).toContain("mcp_status");
146
- expect(patterns).toContain("mcp_list");
147
- expect(patterns).toContain("mcp_search");
148
- expect(patterns).toContain("mcp_describe");
149
- expect(patterns).toContain("mcp_connect");
150
- });
151
-
152
- test("baseline is NOT synthesized when allow rule is on a non-mcp surface", () => {
153
- const configRules = [
154
- {
155
- surface: "bash",
156
- pattern: "git *",
157
- action: "allow" as const,
158
- layer: "config" as const,
159
- origin: "global" as const,
160
- },
161
- ];
162
- expect(synthesizeBaseline(configRules)).toEqual([]);
163
- });
164
-
165
- test("baseline auto-allows mcp_status when an mcp allow rule exists", () => {
166
- const configRules = [
167
- {
168
- surface: "mcp",
169
- pattern: "exa:*",
170
- action: "allow" as const,
171
- layer: "config" as const,
172
- origin: "global" as const,
173
- },
174
- ];
175
- const rules = synthesizeBaseline(configRules);
176
- const result = evaluate("mcp", "mcp_status", rules, "linux");
177
- expect(result.action).toBe("allow");
178
- expect(result.layer).toBe("baseline");
179
- expect(result.origin).toBe("baseline");
180
- });
181
- });
182
-
183
- // ── composeRuleset ─────────────────────────────────────────────────────────
184
-
185
- describe("composeRuleset", () => {
186
- test("returns concatenation of all layers in order", () => {
187
- const defaults = synthesizeDefaults("ask");
188
- const baseline = synthesizeBaseline([
189
- {
190
- surface: "mcp",
191
- pattern: "exa:*",
192
- action: "allow",
193
- layer: "config",
194
- origin: "global" as const,
195
- },
196
- ]);
197
- const config = [
198
- {
199
- surface: "bash",
200
- pattern: "rm -rf *",
201
- action: "deny" as const,
202
- origin: "global" as const,
203
- },
204
- ];
205
- const composed = composeRuleset(defaults, baseline, config);
206
- expect(composed.length).toBe(
207
- defaults.length + baseline.length + config.length,
208
- );
209
- });
210
-
211
- test("defaults come first (lowest priority), config comes last (highest priority)", () => {
212
- const defaults = synthesizeDefaults("ask");
213
- const config = [
214
- {
215
- surface: "bash",
216
- pattern: "*",
217
- action: "deny" as const,
218
- layer: "config" as const,
219
- origin: "global" as const,
220
- },
221
- ];
222
- const composed = composeRuleset(defaults, [], config);
223
- const result = evaluate("bash", "echo hello", composed, "linux");
224
- expect(result.action).toBe("deny");
225
- expect(result.layer).toBe("config");
226
- });
227
-
228
- test("config beats default for matching patterns", () => {
229
- const defaults = synthesizeDefaults("ask");
230
- const config = [
231
- {
232
- surface: "read",
233
- pattern: "*",
234
- action: "allow" as const,
235
- layer: "config" as const,
236
- origin: "global" as const,
237
- },
238
- ];
239
- const composed = composeRuleset(defaults, [], config);
240
- const result = evaluate("read", "*", composed, "linux");
241
- expect(result.action).toBe("allow");
242
- expect(result.layer).toBe("config");
243
- });
244
-
245
- test("baseline beats default but config beats baseline", () => {
246
- const defaults = synthesizeDefaults("ask");
247
- const baseline = [
248
- {
249
- surface: "mcp",
250
- pattern: "mcp_status",
251
- action: "allow" as const,
252
- layer: "baseline" as const,
253
- origin: "baseline" as const,
254
- },
255
- ];
256
- const config = [
257
- {
258
- surface: "mcp",
259
- pattern: "mcp_status",
260
- action: "deny" as const,
261
- layer: "config" as const,
262
- origin: "global" as const,
263
- },
264
- ];
265
- const composed = composeRuleset(defaults, baseline, config);
266
- const result = evaluate("mcp", "mcp_status", composed, "linux");
267
- expect(result.action).toBe("deny");
268
- expect(result.layer).toBe("config");
269
- });
270
-
271
- test("config beats baseline for specific patterns", () => {
272
- const defaults = synthesizeDefaults("ask");
273
- const baseline = [
274
- {
275
- surface: "mcp",
276
- pattern: "mcp_status",
277
- action: "allow" as const,
278
- layer: "baseline" as const,
279
- origin: "baseline" as const,
280
- },
281
- ];
282
- const config = [
283
- {
284
- surface: "mcp",
285
- pattern: "exa_web_search",
286
- action: "allow" as const,
287
- layer: "config" as const,
288
- origin: "global" as const,
289
- },
290
- ];
291
- const composed = composeRuleset(defaults, baseline, config);
292
- const result = evaluate("mcp", "exa_web_search", composed, "linux");
293
- expect(result.action).toBe("allow");
294
- expect(result.layer).toBe("config");
295
- });
296
-
297
- test("handles empty layers gracefully", () => {
298
- const defaults = synthesizeDefaults("ask");
299
- const composed = composeRuleset(defaults, [], []);
300
- expect(composed).toEqual(defaults);
301
- });
302
- });
@@ -1,382 +0,0 @@
1
- import { afterEach, describe, expect, test, vi } from "vitest";
2
-
3
- import { sanitizeAvailableToolsSection } from "#src/system-prompt-sanitizer";
4
-
5
- afterEach(() => {
6
- vi.restoreAllMocks();
7
- });
8
-
9
- // Helpers for building prompt sections.
10
- function availableToolsSection(tools: string[]): string {
11
- return ["Available tools:", ...tools.map((t) => `- ${t}`)].join("\n");
12
- }
13
-
14
- function guidelinesSection(guidelines: string[]): string {
15
- return ["Guidelines:", ...guidelines.map((g) => `- ${g}`)].join("\n");
16
- }
17
-
18
- function prompt(...sections: string[]): string {
19
- return sections.join("\n\n");
20
- }
21
-
22
- describe("sanitizeAvailableToolsSection — Available tools section", () => {
23
- test("keeps allowed tool lines and the header, drops denied ones", () => {
24
- const input = prompt(
25
- availableToolsSection(["bash", "read"]),
26
- "Other content",
27
- );
28
- const result = sanitizeAvailableToolsSection(input, ["read"]);
29
- expect(result.removed).toBe(true);
30
- expect(result.prompt).toContain("Available tools:");
31
- expect(result.prompt).toContain("- read");
32
- expect(result.prompt).not.toContain("- bash");
33
- });
34
-
35
- test("leaves the section untouched when every tool is allowed", () => {
36
- const input = prompt(
37
- availableToolsSection(["bash", "read"]),
38
- "Other content",
39
- );
40
- const result = sanitizeAvailableToolsSection(input, ["bash", "read"]);
41
- expect(result.removed).toBe(false);
42
- expect(result.prompt).toBe(input);
43
- });
44
-
45
- // Bug #33: findSection extends to lines.length when no subsequent recognised
46
- // header follows, so content after the last section is silently deleted.
47
- test("preserves content that follows the Available tools section (bug #33)", () => {
48
- const input = prompt(
49
- availableToolsSection(["bash", "read"]),
50
- "Other content",
51
- );
52
- const result = sanitizeAvailableToolsSection(input, ["read"]);
53
- expect(result.prompt).toContain("Other content");
54
- });
55
-
56
- test("removes the whole section when no tool is allowed", () => {
57
- const input = prompt(
58
- availableToolsSection(["bash", "read"]),
59
- "Other content",
60
- );
61
- const result = sanitizeAvailableToolsSection(input, []);
62
- expect(result.removed).toBe(true);
63
- expect(result.prompt).not.toContain("Available tools:");
64
- expect(result.prompt).toContain("Other content");
65
- });
66
-
67
- test("removed flag is false when no Available tools section is present", () => {
68
- const input = "Just some instructions.\n\nNo tools section.";
69
- const result = sanitizeAvailableToolsSection(input, ["bash"]);
70
- expect(result.removed).toBe(false);
71
- expect(result.prompt).toBe(input);
72
- });
73
-
74
- test("keeps non-tool boilerplate prose near the section", () => {
75
- const input = [
76
- "Available tools:",
77
- "- read: Read file contents",
78
- "- bash: Run shell commands",
79
- "",
80
- "In addition to the tools above, you may have access to other custom tools depending on the project.",
81
- ].join("\n");
82
- const result = sanitizeAvailableToolsSection(input, ["read"]);
83
- expect(result.prompt).toContain("- read: Read file contents");
84
- expect(result.prompt).not.toContain("- bash: Run shell commands");
85
- expect(result.prompt).toContain("In addition to the tools above");
86
- });
87
-
88
- test("returns original prompt reference unchanged when nothing is removed", () => {
89
- const input = "No tools section here.";
90
- const result = sanitizeAvailableToolsSection(input, []);
91
- expect(result.prompt).toBe(input);
92
- });
93
-
94
- test("narrowing the full listing yields the already-narrowed listing (cache byte-stability)", () => {
95
- const allowed = ["read", "edit", "write"];
96
- const fullProse = [
97
- "You are an assistant.",
98
- "",
99
- "Available tools:",
100
- "- bash: Run shell commands",
101
- "- read: Read file contents",
102
- "- edit: Edit a file",
103
- "- write: Write a file",
104
- "",
105
- "Guidelines:",
106
- "- use bash for file operations like ls, rg, find",
107
- "- use read to examine files instead of cat or sed.",
108
- "- Be concise in your responses",
109
- ].join("\n");
110
- const narrowedProse = [
111
- "You are an assistant.",
112
- "",
113
- "Available tools:",
114
- "- read: Read file contents",
115
- "- edit: Edit a file",
116
- "- write: Write a file",
117
- "",
118
- "Guidelines:",
119
- "- use read to examine files instead of cat or sed.",
120
- "- Be concise in your responses",
121
- ].join("\n");
122
-
123
- const fromFull = sanitizeAvailableToolsSection(fullProse, allowed).prompt;
124
- const fromNarrowed = sanitizeAvailableToolsSection(
125
- narrowedProse,
126
- allowed,
127
- ).prompt;
128
-
129
- // Idempotent on the already-narrowed input Pi feeds back on later turns.
130
- expect(fromNarrowed).toBe(narrowedProse);
131
- // Turn 1 (full) and turn 2+ (narrowed) produce identical wire bytes.
132
- expect(fromFull).toBe(fromNarrowed);
133
- });
134
- });
135
-
136
- describe("sanitizeAvailableToolsSection — Guidelines section", () => {
137
- test("removes bash guideline when bash is not in allowed tools", () => {
138
- const input = prompt(
139
- guidelinesSection(["use bash for file operations like ls, rg, find"]),
140
- );
141
- const result = sanitizeAvailableToolsSection(input, []);
142
- expect(result.removed).toBe(true);
143
- expect(result.prompt).not.toContain("use bash for file operations");
144
- });
145
-
146
- test("keeps bash guideline when bash is in allowed tools", () => {
147
- const input = prompt(
148
- guidelinesSection(["use bash for file operations like ls, rg, find"]),
149
- );
150
- const result = sanitizeAvailableToolsSection(input, ["bash"]);
151
- expect(result.removed).toBe(false);
152
- expect(result.prompt).toContain("use bash for file operations");
153
- });
154
-
155
- test("removes read guideline when read is not allowed", () => {
156
- const input = prompt(
157
- guidelinesSection(["use read to examine files instead of cat or sed."]),
158
- );
159
- const result = sanitizeAvailableToolsSection(input, []);
160
- expect(result.removed).toBe(true);
161
- expect(result.prompt).not.toContain("use read to examine files");
162
- });
163
-
164
- test("keeps read guideline when read is allowed", () => {
165
- const input = prompt(
166
- guidelinesSection(["use read to examine files instead of cat or sed."]),
167
- );
168
- const result = sanitizeAvailableToolsSection(input, ["read"]);
169
- expect(result.removed).toBe(false);
170
- expect(result.prompt).toContain("use read to examine files");
171
- });
172
-
173
- test("removes edit guideline when edit is not allowed", () => {
174
- const input = prompt(
175
- guidelinesSection([
176
- "use edit for precise changes (old text must match exactly)",
177
- ]),
178
- );
179
- const result = sanitizeAvailableToolsSection(input, []);
180
- expect(result.removed).toBe(true);
181
- expect(result.prompt).not.toContain("use edit for precise changes");
182
- });
183
-
184
- test("removes write guideline when write is not allowed", () => {
185
- const input = prompt(
186
- guidelinesSection(["use write only for new files or complete rewrites"]),
187
- );
188
- const result = sanitizeAvailableToolsSection(input, []);
189
- expect(result.removed).toBe(true);
190
- expect(result.prompt).not.toContain("use write only for new files");
191
- });
192
-
193
- test("removes entire Guidelines section when all bullets are filtered out", () => {
194
- const input = prompt(
195
- guidelinesSection([
196
- "use bash for file operations like ls, rg, find",
197
- "use write only for new files or complete rewrites",
198
- ]),
199
- );
200
- const result = sanitizeAvailableToolsSection(input, []);
201
- expect(result.removed).toBe(true);
202
- expect(result.prompt).not.toContain("Guidelines:");
203
- });
204
-
205
- test("preserves unrecognised guidelines regardless of allowed tools", () => {
206
- const input = prompt(
207
- guidelinesSection(["some custom guideline not in the rules"]),
208
- );
209
- const result = sanitizeAvailableToolsSection(input, []);
210
- expect(result.removed).toBe(false);
211
- expect(result.prompt).toContain("some custom guideline not in the rules");
212
- });
213
-
214
- test("handles both sections together: removes tools section and filters guidelines", () => {
215
- const input = prompt(
216
- availableToolsSection(["bash"]),
217
- guidelinesSection([
218
- "use bash for file operations like ls, rg, find",
219
- "use write only for new files or complete rewrites",
220
- "some custom guideline not in the rules",
221
- ]),
222
- );
223
- const result = sanitizeAvailableToolsSection(input, []);
224
- expect(result.removed).toBe(true);
225
- expect(result.prompt).not.toContain("Available tools:");
226
- expect(result.prompt).not.toContain("use bash for file operations");
227
- expect(result.prompt).not.toContain("use write only for new files");
228
- expect(result.prompt).toContain("some custom guideline not in the rules");
229
- });
230
-
231
- test("trims whitespace from allowed tool names", () => {
232
- const input = prompt(
233
- guidelinesSection(["use bash for file operations like ls, rg, find"]),
234
- );
235
- const result = sanitizeAvailableToolsSection(input, [" bash "]);
236
- expect(result.removed).toBe(false);
237
- expect(result.prompt).toContain("use bash for file operations");
238
- });
239
- });
240
-
241
- describe("sanitizeAvailableToolsSection — multi-section prompt", () => {
242
- test("collapses extra blank lines after removal", () => {
243
- const input = prompt(
244
- "Intro",
245
- availableToolsSection(["bash"]),
246
- guidelinesSection(["use bash for file operations like ls, rg, find"]),
247
- "Closing",
248
- );
249
- const result = sanitizeAvailableToolsSection(input, []);
250
- // No run of 3+ consecutive newlines
251
- expect(result.prompt).not.toMatch(/\n{3,}/);
252
- });
253
- });
254
-
255
- describe("sanitizeAvailableToolsSection — findSection boundary edge cases", () => {
256
- test("preserves content after Guidelines when Guidelines is the last recognised section", () => {
257
- const input = prompt(
258
- guidelinesSection(["use bash for file operations like ls, rg, find"]),
259
- "Trailing custom instructions",
260
- );
261
- const result = sanitizeAvailableToolsSection(input, []);
262
- expect(result.prompt).toContain("Trailing custom instructions");
263
- });
264
-
265
- test("preserves trailing prose when both sections are removed", () => {
266
- const input = prompt(
267
- availableToolsSection(["bash"]),
268
- guidelinesSection(["use bash for file operations like ls, rg, find"]),
269
- "Important user note",
270
- );
271
- const result = sanitizeAvailableToolsSection(input, []);
272
- expect(result.removed).toBe(true);
273
- expect(result.prompt).not.toContain("Available tools:");
274
- expect(result.prompt).not.toContain("Guidelines:");
275
- expect(result.prompt).toContain("Important user note");
276
- });
277
-
278
- test("section at EOF is removed entirely when no tool is allowed", () => {
279
- const input = availableToolsSection(["bash", "read"]);
280
- const result = sanitizeAvailableToolsSection(input, []);
281
- expect(result.removed).toBe(true);
282
- expect(result.prompt).toBe("");
283
- });
284
-
285
- test("section followed by blank lines then prose — prose survives removal", () => {
286
- const input = ["Available tools:", "- bash", "", "", "Custom note"].join(
287
- "\n",
288
- );
289
- const result = sanitizeAvailableToolsSection(input, []);
290
- expect(result.removed).toBe(true);
291
- expect(result.prompt).toContain("Custom note");
292
- expect(result.prompt).not.toContain("Available tools:");
293
- });
294
- });
295
-
296
- // ---------------------------------------------------------------------------
297
- // Moved from permission-system.test.ts catch-all (#342)
298
- // ---------------------------------------------------------------------------
299
-
300
- test("System prompt sanitizer keeps the active tools in the Available tools section", () => {
301
- const prompt = [
302
- "Available tools:",
303
- "- read: Read file contents",
304
- "- mcp: Discover, inspect, and call MCP tools across configured servers",
305
- "",
306
- "In addition to the tools above, you may have access to other custom tools depending on the project.",
307
- "",
308
- "Guidelines:",
309
- "- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
310
- "- Be concise in your responses",
311
- ].join("\n");
312
-
313
- const result = sanitizeAvailableToolsSection(prompt, ["read", "mcp"]);
314
-
315
- expect(result.removed).toBe(false);
316
- expect(result.prompt).toContain("Available tools:");
317
- expect(result.prompt).toContain("- read: Read file contents");
318
- expect(result.prompt).toContain("- mcp: Discover");
319
- expect(result.prompt).toContain("In addition to the tools above");
320
- expect(result.prompt).toMatch(/Guidelines:/);
321
- });
322
-
323
- test("System prompt sanitizer drops a denied tool's line but keeps the section", () => {
324
- const prompt = [
325
- "Available tools:",
326
- "- read: Read file contents",
327
- "- mcp: Discover, inspect, and call MCP tools across configured servers",
328
- "",
329
- "Guidelines:",
330
- "- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
331
- "- Be concise in your responses",
332
- ].join("\n");
333
-
334
- const result = sanitizeAvailableToolsSection(prompt, ["read"]);
335
-
336
- expect(result.removed).toBe(true);
337
- expect(result.prompt).toContain("Available tools:");
338
- expect(result.prompt).toContain("- read: Read file contents");
339
- expect(result.prompt).not.toContain("- mcp: Discover");
340
- });
341
-
342
- test("System prompt sanitizer removes denied tool guidelines while keeping global guidance", () => {
343
- const prompt = [
344
- "Guidelines:",
345
- "- Use task when work SHOULD be delegated to one or more specialized agents instead of handled entirely in the current session.",
346
- "- Use mcp for MCP discovery first: search by capability, describe one exact tool name, then call it.",
347
- "- Prefer grep/find/ls tools over bash for file exploration (faster, respects .gitignore)",
348
- "- Be concise in your responses",
349
- "- Show file paths clearly when working with files",
350
- ].join("\n");
351
-
352
- const result = sanitizeAvailableToolsSection(prompt, ["bash", "grep", "mcp"]);
353
-
354
- expect(result.removed).toBe(true);
355
- expect(result.prompt).not.toContain("Use task when work SHOULD");
356
- expect(result.prompt).toMatch(/Use mcp for MCP discovery first/i);
357
- expect(result.prompt).toMatch(/Prefer grep\/find\/ls tools over bash/i);
358
- expect(result.prompt).toMatch(/Be concise in your responses/);
359
- expect(result.prompt).toMatch(
360
- /Show file paths clearly when working with files/,
361
- );
362
- });
363
-
364
- test("System prompt sanitizer removes inactive built-in write guidance", () => {
365
- const prompt = [
366
- "Guidelines:",
367
- "- Use write only for new files or complete rewrites",
368
- "- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did",
369
- "- Be concise in your responses",
370
- ].join("\n");
371
-
372
- const result = sanitizeAvailableToolsSection(prompt, ["read"]);
373
-
374
- expect(result.removed).toBe(true);
375
- expect(result.prompt).not.toContain(
376
- "Use write only for new files or complete rewrites",
377
- );
378
- expect(result.prompt).not.toContain(
379
- "do NOT use cat or bash to display what you did",
380
- );
381
- expect(result.prompt).toMatch(/Be concise in your responses/);
382
- });