@gotgenes/pi-permission-system 18.1.1 → 18.1.2

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 (122) hide show
  1. package/CHANGELOG.md +16 -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/path-normalizer.ts +105 -2
  8. package/test/access-intent/access-path.test.ts +0 -277
  9. package/test/access-intent/bash/node-text.test.ts +0 -148
  10. package/test/access-intent/bash/parser.test.ts +0 -19
  11. package/test/access-intent/bash/program.test.ts +0 -673
  12. package/test/access-intent/bash/token-classification.test.ts +0 -363
  13. package/test/access-intent/bash/token-collection.test.ts +0 -300
  14. package/test/active-agent.test.ts +0 -155
  15. package/test/async-cache.test.ts +0 -48
  16. package/test/bash-arity.test.ts +0 -144
  17. package/test/bash-external-directory.test.ts +0 -1022
  18. package/test/builtin-tool-input-formatters.test.ts +0 -109
  19. package/test/canonicalize-path.test.ts +0 -119
  20. package/test/composition-root.test.ts +0 -698
  21. package/test/config-loader.test.ts +0 -740
  22. package/test/config-modal.test.ts +0 -320
  23. package/test/config-paths.test.ts +0 -83
  24. package/test/config-pipeline.test.ts +0 -90
  25. package/test/config-reporter.test.ts +0 -147
  26. package/test/config-store.test.ts +0 -466
  27. package/test/decision-audit.test.ts +0 -72
  28. package/test/decision-reporter.test.ts +0 -112
  29. package/test/denial-messages.test.ts +0 -714
  30. package/test/detect-permissive-bash-fallback.test.ts +0 -56
  31. package/test/expand-home.test.ts +0 -93
  32. package/test/extension-config.test.ts +0 -129
  33. package/test/extension-paths.test.ts +0 -108
  34. package/test/forwarded-permissions/io.test.ts +0 -251
  35. package/test/forwarding-manager.test.ts +0 -200
  36. package/test/handlers/before-agent-start.test.ts +0 -314
  37. package/test/handlers/external-directory-integration.test.ts +0 -515
  38. package/test/handlers/external-directory-session-dedup.test.ts +0 -175
  39. package/test/handlers/external-directory-symlink-acceptance.test.ts +0 -167
  40. package/test/handlers/gates/bash-command-metamorphic.test.ts +0 -88
  41. package/test/handlers/gates/bash-command.test.ts +0 -257
  42. package/test/handlers/gates/bash-external-directory.test.ts +0 -268
  43. package/test/handlers/gates/bash-path.test.ts +0 -346
  44. package/test/handlers/gates/candidate-check.test.ts +0 -52
  45. package/test/handlers/gates/external-directory-messages.test.ts +0 -85
  46. package/test/handlers/gates/external-directory-policy.test.ts +0 -134
  47. package/test/handlers/gates/external-directory.test.ts +0 -267
  48. package/test/handlers/gates/helpers.test.ts +0 -165
  49. package/test/handlers/gates/path.test.ts +0 -369
  50. package/test/handlers/gates/runner.test.ts +0 -408
  51. package/test/handlers/gates/skill-input-gate-pipeline.test.ts +0 -176
  52. package/test/handlers/gates/skill-input.test.ts +0 -128
  53. package/test/handlers/gates/skill-read.test.ts +0 -161
  54. package/test/handlers/gates/tool-call-gate-pipeline.test.ts +0 -356
  55. package/test/handlers/gates/tool.test.ts +0 -244
  56. package/test/handlers/input-events.test.ts +0 -168
  57. package/test/handlers/input.test.ts +0 -199
  58. package/test/handlers/lifecycle.test.ts +0 -221
  59. package/test/handlers/tool-call-boundary.test.ts +0 -145
  60. package/test/handlers/tool-call-events.test.ts +0 -277
  61. package/test/handlers/tool-call.test.ts +0 -395
  62. package/test/handlers/validate-requested-tool.test.ts +0 -92
  63. package/test/helpers/external-directory-fixtures.ts +0 -269
  64. package/test/helpers/gate-fixtures.ts +0 -316
  65. package/test/helpers/handler-fixtures.ts +0 -335
  66. package/test/helpers/make-fake-pi.ts +0 -100
  67. package/test/helpers/manager-harness.ts +0 -112
  68. package/test/helpers/session-fixtures.ts +0 -199
  69. package/test/input-normalizer.test.ts +0 -325
  70. package/test/logging.test.ts +0 -51
  71. package/test/mcp-targets.test.ts +0 -233
  72. package/test/node-modules-discovery.test.ts +0 -97
  73. package/test/normalize.test.ts +0 -247
  74. package/test/path-containment.test.ts +0 -161
  75. package/test/path-normalization.test.ts +0 -233
  76. package/test/path-normalizer.test.ts +0 -196
  77. package/test/path-surfaces.test.ts +0 -55
  78. package/test/pattern-suggest.test.ts +0 -248
  79. package/test/permission-dialog.test.ts +0 -205
  80. package/test/permission-event-rpc.test.ts +0 -560
  81. package/test/permission-events.test.ts +0 -400
  82. package/test/permission-forwarder.test.ts +0 -370
  83. package/test/permission-forwarding.test.ts +0 -315
  84. package/test/permission-gate.test.ts +0 -269
  85. package/test/permission-manager-unified.test.ts +0 -3714
  86. package/test/permission-merge.test.ts +0 -61
  87. package/test/permission-prompter.test.ts +0 -518
  88. package/test/permission-prompts.test.ts +0 -363
  89. package/test/permission-resolver.test.ts +0 -277
  90. package/test/permission-session.test.ts +0 -404
  91. package/test/permission-ui-prompt.test.ts +0 -146
  92. package/test/permissions-service.test.ts +0 -192
  93. package/test/pi-infrastructure-read.test.ts +0 -432
  94. package/test/policy-loader.test.ts +0 -561
  95. package/test/prompting-gateway.test.ts +0 -231
  96. package/test/rule.test.ts +0 -650
  97. package/test/safe-system-paths.test.ts +0 -46
  98. package/test/scope-merge.test.ts +0 -116
  99. package/test/service-lifecycle.test.ts +0 -163
  100. package/test/service.test.ts +0 -261
  101. package/test/session-approval.test.ts +0 -75
  102. package/test/session-logger.test.ts +0 -200
  103. package/test/session-rules.test.ts +0 -321
  104. package/test/session-start.test.ts +0 -112
  105. package/test/skill-prompt-sanitizer.test.ts +0 -418
  106. package/test/status.test.ts +0 -10
  107. package/test/subagent-context.test.ts +0 -372
  108. package/test/subagent-lifecycle-events.test.ts +0 -132
  109. package/test/subagent-registry.test.ts +0 -145
  110. package/test/synthesize.test.ts +0 -302
  111. package/test/system-prompt-sanitizer.test.ts +0 -382
  112. package/test/tool-access-extractor-registry.test.ts +0 -77
  113. package/test/tool-input-formatter-registry.test.ts +0 -75
  114. package/test/tool-input-path.test.ts +0 -84
  115. package/test/tool-input-preview.test.ts +0 -129
  116. package/test/tool-input-prompt-formatters.test.ts +0 -115
  117. package/test/tool-preview-formatter.test.ts +0 -458
  118. package/test/tool-registry.test.ts +0 -197
  119. package/test/value-guards.test.ts +0 -193
  120. package/test/wildcard-matcher.test.ts +0 -424
  121. package/test/yaml-frontmatter.test.ts +0 -91
  122. 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
- });