@herbertgao/pi-extensions 2026.9.4 → 2026.9.6

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 (146) hide show
  1. package/README.md +3 -15
  2. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  3. package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
  4. package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
  5. package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
  6. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  7. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
  8. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
  9. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
  10. package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
  11. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
  12. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
  14. package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
  15. package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
  16. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
  17. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
  18. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
  19. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
  20. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +69 -14
  21. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  22. package/node_modules/@herbertgao/sol-pi/docs/compatibility.md +1 -1
  23. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  24. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/action-fusion/index.ts +0 -5
  25. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
  26. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
  27. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/state.ts +3 -1
  28. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
  29. package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
  30. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  31. package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
  32. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
  33. package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
  34. package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
  35. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
  36. package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
  37. package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
  38. package/node_modules/@tifan/pi-copy-response/README.md +2 -2
  39. package/node_modules/@tifan/pi-copy-response/package.json +3 -2
  40. package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
  41. package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
  42. package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
  43. package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
  44. package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
  45. package/node_modules/@tifan/pi-recap/package.json +3 -2
  46. package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
  47. package/node_modules/pi-mcp-adapter/README.md +125 -19
  48. package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
  49. package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
  50. package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
  51. package/node_modules/pi-mcp-adapter/commands.ts +13 -15
  52. package/node_modules/pi-mcp-adapter/config.ts +198 -30
  53. package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
  54. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
  55. package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
  56. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
  57. package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
  58. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
  59. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
  60. package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
  61. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
  62. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
  63. package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
  64. package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
  65. package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
  66. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  67. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
  68. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  69. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
  70. package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
  71. package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
  72. package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
  73. package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
  74. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
  75. package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
  76. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  77. package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
  78. package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
  79. package/node_modules/pi-mcp-adapter/index.ts +827 -164
  80. package/node_modules/pi-mcp-adapter/init.ts +48 -10
  81. package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
  82. package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
  83. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
  84. package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
  85. package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
  86. package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
  87. package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
  88. package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
  89. package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
  90. package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
  91. package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
  92. package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
  93. package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
  94. package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
  95. package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
  96. package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
  97. package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
  98. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
  99. package/node_modules/pi-mcp-adapter/package.json +13 -3
  100. package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
  101. package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
  102. package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
  103. package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
  104. package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
  105. package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
  106. package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
  107. package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
  108. package/node_modules/pi-mcp-adapter/state.ts +10 -1
  109. package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
  110. package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
  111. package/node_modules/pi-mcp-adapter/types.ts +57 -4
  112. package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
  113. package/node_modules/pi-mcp-adapter/utils.ts +64 -6
  114. package/package.json +13 -21
  115. package/examples/pi-footer.json +0 -275
  116. package/node_modules/@czottmann/pi-automode/CHANGELOG.md +0 -70
  117. package/node_modules/@czottmann/pi-automode/LICENSE.md +0 -21
  118. package/node_modules/@czottmann/pi-automode/README.md +0 -215
  119. package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +0 -39
  120. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-001-permission-precedence-and-trust-boundaries.md +0 -46
  121. package/node_modules/@czottmann/pi-automode/docs/adr/ADR-002-global-config-in-extension-data-directory.md +0 -60
  122. package/node_modules/@czottmann/pi-automode/docs/adr/INDEX.md +0 -6
  123. package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +0 -428
  124. package/node_modules/@czottmann/pi-automode/docs/configuration.md +0 -171
  125. package/node_modules/@czottmann/pi-automode/docs/defaults.md +0 -178
  126. package/node_modules/@czottmann/pi-automode/docs/diagnostics.md +0 -90
  127. package/node_modules/@czottmann/pi-automode/docs/observability-logging.md +0 -160
  128. package/node_modules/@czottmann/pi-automode/examples/automode.local.json +0 -44
  129. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/bash.ts +0 -692
  130. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +0 -852
  131. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +0 -904
  132. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +0 -228
  133. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +0 -954
  134. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +0 -429
  135. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/log.ts +0 -173
  136. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model-selector.ts +0 -113
  137. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/model.ts +0 -13
  138. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/paths.ts +0 -297
  139. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/permissions.ts +0 -623
  140. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/state.ts +0 -102
  141. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/transcript.ts +0 -236
  142. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/types.ts +0 -205
  143. package/node_modules/@czottmann/pi-automode/extensions/auto-mode/utils.ts +0 -54
  144. package/node_modules/@czottmann/pi-automode/extensions/auto-mode.ts +0 -26
  145. package/node_modules/@czottmann/pi-automode/package.json +0 -61
  146. package/node_modules/@czottmann/pi-automode/skills/automode-diagnostics/SKILL.md +0 -63
@@ -1,623 +0,0 @@
1
- import {
2
- analyzeBash,
3
- type BashAnalysis,
4
- type BashCommandAnalysis,
5
- type BashRedirectAnalysis,
6
- } from "./bash.ts";
7
- import type { ToolPattern } from "./types.ts";
8
- import {
9
- expandHomePattern,
10
- normalizePathForMatch,
11
- resolveInputPath,
12
- resolvePathForPolicy,
13
- resolveToolInputPath,
14
- } from "./paths.ts";
15
-
16
- export const MAX_WILDCARD_PATTERN_LENGTH = 4096;
17
- export const MAX_WILDCARD_INPUT_LENGTH = 1024 * 1024;
18
-
19
- const bashPatternAnalyses = new WeakMap<ToolPattern, BashAnalysis>();
20
-
21
- /** Preserve the previous non-Unicode RegExp `/i` case-equivalence rules. */
22
- function canonicalizeCase(value: string): string {
23
- let canonical = "";
24
- for (let index = 0; index < value.length; index += 1) {
25
- const character = value[index] ?? "";
26
- const uppercase = character.toUpperCase();
27
- if (
28
- uppercase.length !== 1 ||
29
- (character.charCodeAt(0) >= 128 && uppercase.charCodeAt(0) < 128)
30
- ) {
31
- canonical += character;
32
- } else {
33
- canonical += uppercase;
34
- }
35
- }
36
- return canonical;
37
- }
38
-
39
- function normalizeToolName(name: string): string {
40
- const lower = name.trim().replace(/^@/, "").toLowerCase();
41
- const aliases: Record<string, string> = {
42
- bash: "bash",
43
- read: "read",
44
- edit: "edit",
45
- write: "write",
46
- grep: "grep",
47
- find: "find",
48
- ls: "ls",
49
- };
50
- return aliases[lower] ?? lower;
51
- }
52
-
53
- /**
54
- * Parse Pi permission entries such as `bash(git push *)`.
55
- *
56
- * Capitalized names such as `Bash(...)` are accepted as a convenience, but Pi's
57
- * actual tool names are lowercase. Scoped entries stay scoped: we do not flatten
58
- * `bash(git status *)` into a blanket `bash` permission.
59
- */
60
- export function parseToolPattern(value: unknown): ToolPattern | undefined {
61
- if (typeof value !== "string") return undefined;
62
- const raw = value.trim();
63
- if (!raw) return undefined;
64
-
65
- const match = raw.match(/^@?([A-Za-z0-9_-]+)(?:\((.*)\))?$/s);
66
- if (!match) return { raw };
67
- const toolName = normalizeToolName(match[1] ?? "");
68
- const argumentPattern = match[2];
69
- const bashPatternAnalysis = toolName === "bash" && argumentPattern
70
- ? analyzeBash(argumentPattern)
71
- : undefined;
72
- const pattern: ToolPattern = { raw, toolName, argumentPattern };
73
- if (bashPatternAnalysis) bashPatternAnalyses.set(pattern, bashPatternAnalysis);
74
- return pattern;
75
- }
76
-
77
- function literalPrefixTable(value: string): number[] {
78
- const table = new Array<number>(value.length).fill(0);
79
- let prefixLength = 0;
80
- for (let index = 1; index < value.length; index += 1) {
81
- while (
82
- prefixLength > 0 && value[index] !== value[prefixLength]
83
- ) {
84
- prefixLength = table[prefixLength - 1] ?? 0;
85
- }
86
- if (value[index] === value[prefixLength]) prefixLength += 1;
87
- table[index] = prefixLength;
88
- }
89
- return table;
90
- }
91
-
92
- function findLiteral(
93
- value: string,
94
- literal: string,
95
- start: number,
96
- end: number,
97
- ): number {
98
- const prefixTable = literalPrefixTable(literal);
99
- let matched = 0;
100
- for (let index = start; index < end; index += 1) {
101
- while (matched > 0 && value[index] !== literal[matched]) {
102
- matched = prefixTable[matched - 1] ?? 0;
103
- }
104
- if (value[index] === literal[matched]) matched += 1;
105
- if (matched === literal.length) return index - literal.length + 1;
106
- }
107
- return -1;
108
- }
109
-
110
- export type WildcardOverflowPolicy = "match" | "no-match";
111
-
112
- /**
113
- * Match a case-insensitive `*` wildcard pattern in linear time.
114
- *
115
- * `*` matches zero or more characters, including newlines and path separators.
116
- * Denial callers use `match` for over-limit values so they fail closed. Allow
117
- * callers use `no-match` so an oversized input cannot broaden an allow rule.
118
- */
119
- export function matchesWildcardPattern(
120
- pattern: string,
121
- value: string,
122
- overflowPolicy: WildcardOverflowPolicy = "match",
123
- ): boolean {
124
- if (
125
- pattern.length > MAX_WILDCARD_PATTERN_LENGTH ||
126
- value.length > MAX_WILDCARD_INPUT_LENGTH
127
- ) {
128
- return overflowPolicy === "match";
129
- }
130
-
131
- const normalizedPattern = canonicalizeCase(pattern);
132
- const normalizedValue = canonicalizeCase(value);
133
- if (!normalizedPattern.includes("*")) {
134
- return normalizedPattern === normalizedValue;
135
- }
136
-
137
- const startsWithWildcard = normalizedPattern.startsWith("*");
138
- const endsWithWildcard = normalizedPattern.endsWith("*");
139
- const literals = normalizedPattern.split("*").filter(Boolean);
140
- if (literals.length === 0) return true;
141
-
142
- let literalIndex = 0;
143
- let valueIndex = 0;
144
- let lastLiteralIndex = literals.length;
145
-
146
- if (!startsWithWildcard) {
147
- const prefix = literals[0] ?? "";
148
- if (!normalizedValue.startsWith(prefix)) return false;
149
- valueIndex = prefix.length;
150
- literalIndex = 1;
151
- }
152
-
153
- let searchEnd = normalizedValue.length;
154
- if (!endsWithWildcard) {
155
- const suffix = literals[literals.length - 1] ?? "";
156
- searchEnd -= suffix.length;
157
- if (
158
- searchEnd < valueIndex ||
159
- !normalizedValue.endsWith(suffix)
160
- ) {
161
- return false;
162
- }
163
- lastLiteralIndex -= 1;
164
- }
165
-
166
- for (; literalIndex < lastLiteralIndex; literalIndex += 1) {
167
- const literal = literals[literalIndex] ?? "";
168
- const found = findLiteral(
169
- normalizedValue,
170
- literal,
171
- valueIndex,
172
- searchEnd,
173
- );
174
- if (found < 0) return false;
175
- valueIndex = found + literal.length;
176
- }
177
-
178
- return true;
179
- }
180
-
181
- export function normalizePermissionPathForMatch(
182
- path: string,
183
- platform: NodeJS.Platform = process.platform,
184
- ): string {
185
- return platform === "win32" ? path.replace(/\\/g, "/") : path;
186
- }
187
-
188
- function pathArgumentsForMatch(
189
- toolName: string,
190
- cwd: string,
191
- value: string,
192
- overflowPolicy: WildcardOverflowPolicy,
193
- ): string[] {
194
- const resolved = resolveToolInputPath(toolName, cwd, value) ?? value;
195
- const canonical = resolvePathForPolicy(resolved);
196
- const candidates = canonical
197
- ? [canonical, normalizePathForMatch(canonical, cwd)]
198
- : [];
199
- if (overflowPolicy === "match") {
200
- candidates.push(resolved, normalizePathForMatch(resolved, cwd));
201
- }
202
- return [...new Set(
203
- candidates.map((candidate) => normalizePermissionPathForMatch(candidate)),
204
- )];
205
- }
206
-
207
- function getPrimaryArguments(
208
- toolName: string,
209
- input: Record<string, unknown>,
210
- cwd: string,
211
- overflowPolicy: WildcardOverflowPolicy,
212
- ): string[] {
213
- if (toolName === "bash" && typeof input.command === "string") {
214
- return [input.command];
215
- }
216
- if (
217
- (toolName === "read" || toolName === "write" || toolName === "edit") &&
218
- typeof input.path === "string"
219
- ) {
220
- return pathArgumentsForMatch(toolName, cwd, input.path, overflowPolicy);
221
- }
222
- if (toolName === "grep" && typeof input.pattern === "string") {
223
- return [input.pattern];
224
- }
225
- if (
226
- (toolName === "find" || toolName === "ls") &&
227
- typeof input.path === "string"
228
- ) {
229
- return pathArgumentsForMatch(toolName, cwd, input.path, overflowPolicy);
230
- }
231
- return [JSON.stringify(input)];
232
- }
233
-
234
- function isPermissionPathTool(toolName: string): boolean {
235
- return toolName === "read" ||
236
- toolName === "write" ||
237
- toolName === "edit" ||
238
- toolName === "find" ||
239
- toolName === "ls";
240
- }
241
-
242
- export function appendPermissionPathPatternSuffix(
243
- scope: string,
244
- suffix: string,
245
- ): string {
246
- const normalizedScope = withoutTrailingSlash(
247
- normalizePermissionPathForMatch(scope),
248
- );
249
- return normalizedScope.endsWith("/")
250
- ? `${normalizedScope}${suffix}`
251
- : `${normalizedScope}/${suffix}`;
252
- }
253
-
254
- function permissionPathPatternVariants(
255
- pattern: string,
256
- cwd: string,
257
- ): string[] {
258
- const expanded = normalizePermissionPathForMatch(expandHomePattern(pattern));
259
- const wildcardIndex = expanded.indexOf("*");
260
- if (wildcardIndex === -1) {
261
- const resolved = resolveInputPath(cwd, expanded);
262
- const canonical = resolved ? resolvePathForPolicy(resolved) : undefined;
263
- return [...new Set(
264
- [expanded, resolved, canonical]
265
- .filter((value): value is string => !!value)
266
- .map((value) => normalizePermissionPathForMatch(value)),
267
- )];
268
- }
269
-
270
- const fixedPrefix = expanded.slice(0, wildcardIndex);
271
- const lastSlash = fixedPrefix.lastIndexOf("/");
272
- if (lastSlash < 0) return [expanded];
273
- const fixedScope = fixedPrefix.slice(0, lastSlash) || "/";
274
- const resolvedScope = resolveInputPath(cwd, fixedScope);
275
- if (!resolvedScope) return [expanded];
276
- const canonicalScope = resolvePathForPolicy(resolvedScope);
277
- const suffix = expanded.slice(lastSlash).replace(/^\/+/, "");
278
- return [...new Set([
279
- expanded,
280
- appendPermissionPathPatternSuffix(resolvedScope, suffix),
281
- ...(canonicalScope
282
- ? [appendPermissionPathPatternSuffix(canonicalScope, suffix)]
283
- : []),
284
- ])];
285
- }
286
-
287
- /**
288
- * Whether a resolved absolute path matches a configured path-denial pattern.
289
- * Patterns support `~`/`$HOME` expansion and `*` globs, where `*` matches any
290
- * characters, including `/`. Matching is case-insensitive and
291
- * conservative-safe: over-matching only blocks more.
292
- */
293
- export function matchesDeniedPath(
294
- resolvedPath: string,
295
- deniedPaths: string[],
296
- ): boolean {
297
- const normalized = resolvedPath.replace(/\\/g, "/").normalize("NFC");
298
- return deniedPaths.some((pattern) =>
299
- deniedPatternVariants(pattern).some((variant) =>
300
- matchesWildcardPattern(variant.normalize("NFC"), normalized)
301
- )
302
- );
303
- }
304
-
305
- function deniedPatternVariants(pattern: string): string[] {
306
- const expanded = expandHomePattern(pattern).replace(/\\/g, "/");
307
- const wildcardIndex = expanded.indexOf("*");
308
- if (wildcardIndex === -1) {
309
- const canonical = resolvePathForPolicy(expanded)?.replace(/\\/g, "/");
310
- return canonical && canonical !== expanded
311
- ? [expanded, canonical]
312
- : [expanded];
313
- }
314
-
315
- const fixedPrefix = expanded.slice(0, wildcardIndex);
316
- const lastSlash = fixedPrefix.lastIndexOf("/");
317
- if (lastSlash < 0) return [expanded];
318
- const fixedScope = fixedPrefix.slice(0, lastSlash) || "/";
319
- const canonicalScope = resolvePathForPolicy(fixedScope)?.replace(/\\/g, "/");
320
- if (!canonicalScope || canonicalScope === fixedScope) return [expanded];
321
- const suffix = expanded.slice(lastSlash).replace(/^\/+/, "");
322
- const canonicalPattern = canonicalScope === "/"
323
- ? `/${suffix}`
324
- : `${withoutTrailingSlash(canonicalScope)}/${suffix}`;
325
- return canonicalPattern === expanded
326
- ? [expanded]
327
- : [expanded, canonicalPattern];
328
- }
329
-
330
- function withoutTrailingSlash(path: string): string {
331
- if (path === "/" || /^[A-Za-z]:\/$/.test(path)) return path;
332
- return path.replace(/\/+$/, "");
333
- }
334
-
335
- function wildcardCanMatchDescendant(root: string, pattern: string): boolean {
336
- const normalizedRoot = withoutTrailingSlash(
337
- canonicalizeCase(root.replace(/\\/g, "/").normalize("NFC")),
338
- );
339
- const prefix = normalizedRoot === "/" || /^[A-Za-z]:\/$/.test(normalizedRoot)
340
- ? normalizedRoot
341
- : `${normalizedRoot}/`;
342
- const normalizedPattern = canonicalizeCase(pattern.normalize("NFC"));
343
- const wildcardIndex = normalizedPattern.indexOf("*");
344
- if (wildcardIndex < 0) {
345
- return normalizedPattern.length > prefix.length &&
346
- normalizedPattern.startsWith(prefix);
347
- }
348
-
349
- const fixedPrefix = normalizedPattern.slice(0, wildcardIndex);
350
- return prefix.startsWith(fixedPrefix) || fixedPrefix.startsWith(prefix);
351
- }
352
-
353
- /**
354
- * Whether a recursive search scope can contain a path matched by `deniedPaths`.
355
- *
356
- * The check asks whether the wildcard pattern can match any path beginning
357
- * with the search-root prefix. It does not scan the search tree.
358
- */
359
- export function recursiveSearchMayReachDeniedPath(
360
- resolvedRoot: string,
361
- deniedPaths: string[],
362
- ): boolean {
363
- if (resolvedRoot.length > MAX_WILDCARD_INPUT_LENGTH) {
364
- return deniedPaths.length > 0;
365
- }
366
- return deniedPaths.some((pattern) => {
367
- if (pattern.length > MAX_WILDCARD_PATTERN_LENGTH) return true;
368
- return deniedPatternVariants(pattern).some((expanded) =>
369
- wildcardCanMatchDescendant(resolvedRoot, expanded)
370
- );
371
- });
372
- }
373
-
374
- function normalizedBashArgumentPattern(pattern: ToolPattern): string {
375
- const patternAnalysis = bashPatternAnalyses.get(pattern);
376
- if (
377
- patternAnalysis &&
378
- patternAnalysis.errors.length === 0 &&
379
- patternAnalysis.redirects.length === 0 &&
380
- isStructurallyPlainSingleCommand(patternAnalysis)
381
- ) {
382
- return patternAnalysis.commands[0]?.text ?? pattern.argumentPattern ?? "";
383
- }
384
- return pattern.argumentPattern ?? "";
385
- }
386
-
387
- function matchesBashArgumentPattern(
388
- argumentPattern: string,
389
- candidate: string,
390
- overflowPolicy: WildcardOverflowPolicy,
391
- ): boolean {
392
- if (matchesWildcardPattern(argumentPattern, candidate, overflowPolicy)) {
393
- return true;
394
- }
395
- if (overflowPolicy !== "match" || !argumentPattern.endsWith(" *")) {
396
- return false;
397
- }
398
- return matchesWildcardPattern(
399
- argumentPattern.slice(0, -2),
400
- candidate,
401
- overflowPolicy,
402
- );
403
- }
404
-
405
- /** Match a scoped permission rule against a concrete tool call. */
406
- export function matchesToolPattern(
407
- pattern: ToolPattern,
408
- toolName: string,
409
- input: Record<string, unknown>,
410
- cwd: string,
411
- overflowPolicy: WildcardOverflowPolicy = "match",
412
- bashAnalysis?: BashAnalysis,
413
- ): boolean {
414
- if (!pattern.toolName) return overflowPolicy === "match";
415
- if (pattern.toolName !== normalizeToolName(toolName)) return false;
416
- if (pattern.argumentPattern === undefined) return true;
417
- if (pattern.argumentPattern.trim() === "") {
418
- return overflowPolicy === "match";
419
- }
420
- if (
421
- toolName === "bash" &&
422
- (bashPatternAnalyses.get(pattern)?.errors.length ?? 0) > 0
423
- ) {
424
- return overflowPolicy === "match";
425
- }
426
- if (toolName === "bash" && bashAnalysis) {
427
- if (bashAnalysis.errors.length > 0) return overflowPolicy === "match";
428
- const candidates = overflowPolicy === "match"
429
- ? [bashAnalysis.source, ...bashAnalysis.commands.map((command) => command.text)]
430
- : [bashAnalysis.source];
431
- const argumentPattern = normalizedBashArgumentPattern(pattern);
432
- return candidates.some((candidate) =>
433
- matchesBashArgumentPattern(argumentPattern, candidate, overflowPolicy)
434
- );
435
- }
436
- const argumentPatterns = isPermissionPathTool(toolName)
437
- ? permissionPathPatternVariants(pattern.argumentPattern, cwd)
438
- : [pattern.argumentPattern];
439
- const primaryArguments = getPrimaryArguments(
440
- toolName,
441
- input,
442
- cwd,
443
- overflowPolicy,
444
- );
445
- return argumentPatterns.some((argumentPattern) =>
446
- primaryArguments.some((primary) =>
447
- matchesWildcardPattern(argumentPattern, primary, overflowPolicy)
448
- )
449
- );
450
- }
451
-
452
- /** Return the normalized Bash command that matched a scoped permission rule. */
453
- export function matchingBashCommandText(
454
- pattern: ToolPattern,
455
- bashAnalysis: BashAnalysis | undefined,
456
- overflowPolicy: WildcardOverflowPolicy = "match",
457
- ): string | undefined {
458
- if (!bashAnalysis || pattern.toolName !== "bash") return undefined;
459
- if (bashAnalysis.errors.length > 0) return undefined;
460
- if (!pattern.argumentPattern) return undefined;
461
- const argumentPattern = normalizedBashArgumentPattern(pattern);
462
- const command = bashAnalysis.commands.find((candidate) =>
463
- matchesBashArgumentPattern(argumentPattern, candidate.text, overflowPolicy)
464
- );
465
- if (command) return command.text;
466
- return matchesBashArgumentPattern(argumentPattern, bashAnalysis.source, overflowPolicy)
467
- ? bashAnalysis.source
468
- : undefined;
469
- }
470
-
471
- function redirectListsMatch(
472
- patternRedirects: BashRedirectAnalysis[],
473
- inputRedirects: BashRedirectAnalysis[],
474
- ): boolean {
475
- if (patternRedirects.length !== inputRedirects.length) return false;
476
- return patternRedirects.every((pattern, index) => {
477
- const input = inputRedirects[index];
478
- if (!input || pattern.heredoc || input.heredoc || input.targetDynamic) {
479
- return false;
480
- }
481
- if (
482
- pattern.operator !== input.operator ||
483
- pattern.fileDescriptor !== input.fileDescriptor ||
484
- pattern.variableName !== input.variableName
485
- ) {
486
- return false;
487
- }
488
- if (pattern.target === undefined) return input.target === undefined;
489
- if (input.target === undefined) return false;
490
- return matchesWildcardPattern(pattern.target, input.target, "no-match");
491
- });
492
- }
493
-
494
- function commandMatchesAllowPattern(
495
- patternCommand: BashCommandAnalysis,
496
- inputCommand: BashCommandAnalysis,
497
- ): boolean {
498
- return matchesWildcardPattern(
499
- patternCommand.text,
500
- inputCommand.text,
501
- "no-match",
502
- ) && redirectListsMatch(patternCommand.redirects, inputCommand.redirects);
503
- }
504
-
505
- function structuresMatch(pattern: BashAnalysis, input: BashAnalysis): boolean {
506
- return pattern.structure.length === input.structure.length &&
507
- pattern.structure.every((token, index) => token === input.structure[index]);
508
- }
509
-
510
- function allRedirectsAreCommandRedirects(analysis: BashAnalysis): boolean {
511
- return analysis.redirects.length === analysis.commands.reduce(
512
- (count, command) => count + command.redirects.length,
513
- 0,
514
- );
515
- }
516
-
517
- function isStructurallyPlainSingleCommand(analysis: BashAnalysis): boolean {
518
- if (analysis.commands.length !== 1) return false;
519
- if (analysis.structure.length !== 3 + analysis.redirects.length) return false;
520
- if (analysis.structure[0] !== "script:1") return false;
521
- if (analysis.structure[1] !== "node:Statement:foreground:0") return false;
522
- if (!/^node:Command:\d+:\d+$/.test(analysis.structure[2] ?? "")) {
523
- return false;
524
- }
525
- return analysis.structure.slice(3).every((token) =>
526
- token.startsWith("redirect:")
527
- );
528
- }
529
-
530
- function supportsPerCommandAllowPatterns(analysis: BashAnalysis): boolean {
531
- return analysis.structure.every((token) =>
532
- token.startsWith("script:") ||
533
- token.startsWith("node:Statement:foreground:") ||
534
- token.startsWith("node:Command:") ||
535
- token.startsWith("node:AndOr:") ||
536
- token.startsWith("node:Pipeline:plain:plain:") ||
537
- token.startsWith("redirect:")
538
- );
539
- }
540
-
541
- /** Whether permission allow rules cover the complete tool call. */
542
- export function matchesAllowedToolPatterns(
543
- patterns: ToolPattern[],
544
- toolName: string,
545
- input: Record<string, unknown>,
546
- cwd: string,
547
- bashAnalysis?: BashAnalysis,
548
- ): boolean {
549
- if (toolName !== "bash" || !bashAnalysis) {
550
- return patterns.some((pattern) =>
551
- matchesToolPattern(pattern, toolName, input, cwd, "no-match")
552
- );
553
- }
554
- if (
555
- bashAnalysis.errors.length > 0 ||
556
- bashAnalysis.commands.length === 0 ||
557
- !bashAnalysis.allowStructureSafe
558
- ) {
559
- return false;
560
- }
561
- if (
562
- bashAnalysis.commands.some((command) =>
563
- command.dynamicName || command.dynamicShellScript
564
- )
565
- ) {
566
- return false;
567
- }
568
-
569
- for (const pattern of patterns) {
570
- if (pattern.toolName !== "bash") continue;
571
- const patternAnalysis = bashPatternAnalyses.get(pattern);
572
- if (
573
- !patternAnalysis ||
574
- patternAnalysis.errors.length > 0 ||
575
- !patternAnalysis.allowStructureSafe ||
576
- isStructurallyPlainSingleCommand(patternAnalysis) ||
577
- patternAnalysis.commands.length !== bashAnalysis.commands.length ||
578
- !structuresMatch(patternAnalysis, bashAnalysis) ||
579
- !redirectListsMatch(patternAnalysis.redirects, bashAnalysis.redirects)
580
- ) {
581
- continue;
582
- }
583
- if (
584
- patternAnalysis.commands.every((patternCommand, index) => {
585
- const inputCommand = bashAnalysis.commands[index];
586
- return !!inputCommand &&
587
- commandMatchesAllowPattern(patternCommand, inputCommand);
588
- })
589
- ) {
590
- return true;
591
- }
592
- }
593
-
594
- const hasBareBashPattern = patterns.some((pattern) =>
595
- pattern.toolName === "bash" && pattern.argumentPattern === undefined
596
- );
597
- if (
598
- !hasBareBashPattern &&
599
- !supportsPerCommandAllowPatterns(bashAnalysis)
600
- ) {
601
- return false;
602
- }
603
- if (!allRedirectsAreCommandRedirects(bashAnalysis)) return false;
604
- return bashAnalysis.commands.every((command) =>
605
- patterns.some((pattern) => {
606
- if (pattern.toolName !== "bash") return false;
607
- if (pattern.argumentPattern === undefined) {
608
- return command.redirects.length === 0;
609
- }
610
- const patternAnalysis = bashPatternAnalyses.get(pattern);
611
- if (
612
- !patternAnalysis ||
613
- patternAnalysis.errors.length > 0 ||
614
- !isStructurallyPlainSingleCommand(patternAnalysis)
615
- ) {
616
- return false;
617
- }
618
- const patternCommand = patternAnalysis.commands[0];
619
- return !!patternCommand &&
620
- commandMatchesAllowPattern(patternCommand, command);
621
- })
622
- );
623
- }