@piagent/platform 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/.gitattributes +7 -0
  2. package/AGENTS.md +31 -0
  3. package/CHANGELOG.md +296 -0
  4. package/CODE_OF_CONDUCT.md +36 -0
  5. package/CONTRIBUTING.md +66 -0
  6. package/LICENSE +21 -0
  7. package/README.md +588 -0
  8. package/SECURITY.md +38 -0
  9. package/adapters/backend-api/profile.json +88 -0
  10. package/adapters/be-readonly-fe/profile.json +120 -0
  11. package/adapters/data/profile.json +88 -0
  12. package/adapters/devops/profile.json +92 -0
  13. package/adapters/docs/profile.json +74 -0
  14. package/adapters/fullstack/profile.json +94 -0
  15. package/adapters/generic/profile.json +95 -0
  16. package/adapters/mobile/profile.json +87 -0
  17. package/adapters/node-typescript/profile.json +76 -0
  18. package/adapters/python/profile.json +75 -0
  19. package/adapters/web-frontend/profile.json +101 -0
  20. package/catalog/capabilities.json +206 -0
  21. package/docs/architecture.md +64 -0
  22. package/docs/auto-delegation-policy.md +192 -0
  23. package/docs/capability-packs.md +234 -0
  24. package/docs/command-reference-vietnamese.md +484 -0
  25. package/docs/context-window-policy.md +85 -0
  26. package/docs/distribution-standard.md +229 -0
  27. package/docs/herdr-workflow.md +58 -0
  28. package/docs/mcp-and-tools.md +316 -0
  29. package/docs/memory-policy.md +164 -0
  30. package/docs/model-options.md +165 -0
  31. package/docs/oauth-providers.md +53 -0
  32. package/docs/operator-manual-vietnamese.md +1080 -0
  33. package/docs/package-architecture-notes.md +101 -0
  34. package/docs/project-adapters.md +174 -0
  35. package/docs/project-onboarding.md +135 -0
  36. package/docs/publishing-for-teams.md +64 -0
  37. package/docs/quality-benchmark.md +74 -0
  38. package/docs/quickstart-vietnamese.md +350 -0
  39. package/docs/release-install-policy.md +223 -0
  40. package/docs/runtime-harness-standard.md +118 -0
  41. package/docs/runtime-policy-design.md +132 -0
  42. package/docs/runtime-quality-baseline.md +257 -0
  43. package/docs/security-redaction-benchmark.md +56 -0
  44. package/docs/security-threat-model.md +66 -0
  45. package/docs/subagent-orchestration-capabilities.md +104 -0
  46. package/docs/subagents-and-multiagent.md +346 -0
  47. package/docs/task-implementation-contract.md +200 -0
  48. package/docs/team-onboarding.md +333 -0
  49. package/docs/usage-observability.md +130 -0
  50. package/docs/vercel-docs-site.md +115 -0
  51. package/docs/workflow-recipes.md +125 -0
  52. package/docs-site/assets/piagent-logo.svg +35 -0
  53. package/docs-site/favicon.svg +18 -0
  54. package/docs-site/index.html +1613 -0
  55. package/docs-site/vercel.json +24 -0
  56. package/evals/fixtures/action-proposal.invalid.json +34 -0
  57. package/evals/fixtures/action-proposal.valid.json +34 -0
  58. package/evals/fixtures/capability-pack.invalid.json +123 -0
  59. package/evals/fixtures/capability-pack.valid.json +123 -0
  60. package/evals/fixtures/capability-recipe.invalid.json +81 -0
  61. package/evals/fixtures/capability-recipe.valid.json +82 -0
  62. package/evals/fixtures/context-index.invalid.json +25 -0
  63. package/evals/fixtures/context-index.valid.json +25 -0
  64. package/evals/fixtures/eval-scenario.invalid.json +40 -0
  65. package/evals/fixtures/eval-scenario.valid.json +45 -0
  66. package/evals/fixtures/project-profile.invalid.json +88 -0
  67. package/evals/fixtures/project-profile.valid.json +95 -0
  68. package/evals/fixtures/task-contract.invalid.json +73 -0
  69. package/evals/fixtures/task-contract.valid.json +74 -0
  70. package/evals/golden/enforcement-decisions.json +329 -0
  71. package/evals/scenarios/capability-resolution.json +45 -0
  72. package/package.json +132 -0
  73. package/packages/piagent-core/README.md +147 -0
  74. package/packages/piagent-core/capabilities/capability-core.js +1023 -0
  75. package/packages/piagent-core/capabilities/capability-sources.js +265 -0
  76. package/packages/piagent-core/extensions/guard-io.js +45 -0
  77. package/packages/piagent-core/extensions/guard-shell-analysis.ts +558 -0
  78. package/packages/piagent-core/extensions/guard-types.ts +406 -0
  79. package/packages/piagent-core/extensions/piagent-guard.ts +5760 -0
  80. package/packages/piagent-core/extensions/policy-core.js +1070 -0
  81. package/packages/piagent-core/extensions/redaction-core.js +4 -0
  82. package/packages/piagent-core/extensions/runtime-evidence.js +215 -0
  83. package/packages/piagent-core/package.json +46 -0
  84. package/packages/piagent-core/policies/base-policy.json +249 -0
  85. package/packages/piagent-core/prompts/be-to-fe.md +82 -0
  86. package/packages/piagent-core/prompts/commit.md +27 -0
  87. package/packages/piagent-core/prompts/discuss.md +35 -0
  88. package/packages/piagent-core/prompts/memory-policy.md +52 -0
  89. package/packages/piagent-core/prompts/model-options.md +68 -0
  90. package/packages/piagent-core/prompts/onboard-project.md +147 -0
  91. package/packages/piagent-core/prompts/piagent-commands.md +68 -0
  92. package/packages/piagent-core/prompts/plan.md +35 -0
  93. package/packages/piagent-core/prompts/platform-improve.md +69 -0
  94. package/packages/piagent-core/prompts/pr.md +26 -0
  95. package/packages/piagent-core/prompts/review.md +38 -0
  96. package/packages/piagent-core/prompts/scout.md +50 -0
  97. package/packages/piagent-core/prompts/task.md +69 -0
  98. package/packages/piagent-core/security/sensitive-data.js +187 -0
  99. package/packages/piagent-core/skills/piagent-ops/SKILL.md +44 -0
  100. package/packages/piagent-core/skills/piagent-source-cache/SKILL.md +59 -0
  101. package/packages/piagent-core/skills/piagent-source-cache/checkout-source-repo.sh +144 -0
  102. package/packages/piagent-core/subagents/piagent-oracle.md +29 -0
  103. package/packages/piagent-core/subagents/piagent-planner.md +46 -0
  104. package/packages/piagent-core/subagents/piagent-reviewer.md +48 -0
  105. package/packages/piagent-core/subagents/piagent-scout.md +37 -0
  106. package/packages/piagent-core/subagents/piagent-worker.md +44 -0
  107. package/packs/engineering-base/pack.json +123 -0
  108. package/packs/engineering-base/recipes/bounded-change.json +82 -0
  109. package/packs/web-delivery/pack.json +72 -0
  110. package/packs/web-delivery/recipes/verified-web-change.json +57 -0
  111. package/schemas/action-proposal.schema.json +90 -0
  112. package/schemas/capability-pack.schema.json +190 -0
  113. package/schemas/capability-recipe.schema.json +72 -0
  114. package/schemas/context-index.schema.json +258 -0
  115. package/schemas/eval-scenario.schema.json +67 -0
  116. package/schemas/project-profile.schema.json +555 -0
  117. package/schemas/task-contract.schema.json +314 -0
  118. package/scripts/audit-runtime-host.sh +78 -0
  119. package/scripts/bootstrap-local.sh +25 -0
  120. package/scripts/capability-catalog.mjs +290 -0
  121. package/scripts/check-runtime-advisories.mjs +114 -0
  122. package/scripts/configure-mcp.sh +301 -0
  123. package/scripts/configure-model-scope.sh +143 -0
  124. package/scripts/configure-subagents.sh +241 -0
  125. package/scripts/import-agent-instructions.mjs +199 -0
  126. package/scripts/init-project.sh +369 -0
  127. package/scripts/install-global.sh +532 -0
  128. package/scripts/link-project.sh +25 -0
  129. package/scripts/migrate-project-state.mjs +176 -0
  130. package/scripts/pi-auto.sh +79 -0
  131. package/scripts/pi-model-catalog.sh +154 -0
  132. package/scripts/pi-session-stats.sh +135 -0
  133. package/scripts/piagent-cli.mjs +53 -0
  134. package/scripts/profile-doctor.sh +238 -0
  135. package/scripts/quality-benchmark.sh +185 -0
  136. package/scripts/redaction-benchmark.mjs +227 -0
  137. package/scripts/runtime-policy-smoke.sh +48 -0
  138. package/scripts/setup.sh +425 -0
  139. package/scripts/team-doctor.sh +461 -0
  140. package/scripts/verify-local.sh +540 -0
  141. package/scripts/verify-release-identity.mjs +85 -0
  142. package/scripts/verify-vercel-link.mjs +64 -0
  143. package/templates/global/mcp.json +9 -0
  144. package/templates/global/settings.json +37 -0
  145. package/templates/project/.mcp.json +9 -0
  146. package/templates/project/.pi/context-index.json +25 -0
  147. package/templates/project/.pi/gitignore.template +22 -0
  148. package/templates/project/.pi/mcp.json +9 -0
  149. package/templates/project/.pi/memory/MEMORY.md +11 -0
  150. package/templates/project/.pi/memory/memory_summary.md +9 -0
  151. package/templates/project/.pi/piagent-profile.json +107 -0
  152. package/templates/project/.pi/project-context.md +30 -0
  153. package/templates/project/.pi/settings.json +17 -0
  154. package/templates/project/.pi/task-contract.template.json +74 -0
  155. package/templates/project/.pi/tech-context/README.md +7 -0
  156. package/templates/project/.pi/tech-stack.json +10 -0
  157. package/templates/project/AGENTS.md +34 -0
  158. package/templates/project/REVIEW_GUIDELINES.md +23 -0
  159. package/types/pi-runtime-shims.d.ts +13 -0
@@ -0,0 +1,1070 @@
1
+ const PATH_REDIRECT_OPERATORS = new Set(["<", ">", ">>", "1>", "1>>", "2>", "2>>", "&>", "&>>"]);
2
+ const DATA_ONLY_COMMANDS = new Set(["echo", "printf"]);
3
+ const SEARCH_COMMANDS = new Set(["grep", "egrep", "fgrep", "rg", "ripgrep"]);
4
+ const SEARCH_FILE_OPTIONS = new Set(["-f", "--file", "--exclude-from", "--ignore-file"]);
5
+ const SEARCH_GLOB_OPTIONS = new Set(["-g", "--glob", "--iglob", "--include", "--exclude"]);
6
+ const SEARCH_PATTERN_OPTIONS = new Set(["-e", "--regexp", "--pattern"]);
7
+ const ASSIGNMENT_BUILTINS = new Set(["export", "readonly", "declare", "typeset", "local"]);
8
+ const SHELL_COMMANDS = new Set(["bash", "sh", "zsh"]);
9
+ const SIMPLE_WRAPPERS = new Set(["sudo", "nohup", "time", "nice", "ionice", "command"]);
10
+
11
+ function escapeRegex(value) {
12
+ return value.replace(/[.+^${}()|[\]\\]/g, "\\$&");
13
+ }
14
+
15
+ export function normalizePathCandidate(candidate) {
16
+ if (typeof candidate !== "string") return "";
17
+ const raw = candidate.trim().replace(/^['"]|['"]$/g, "");
18
+ if (/^\/+$/.test(raw)) return "/";
19
+ return candidate
20
+ .trim()
21
+ .replace(/^['"]|['"]$/g, "")
22
+ .replace(/\\/g, "/")
23
+ .replace(/^\.\//, "")
24
+ .replace(/\/+/g, "/")
25
+ .replace(/\/$/, "");
26
+ }
27
+
28
+ function commandBasename(command) {
29
+ const normalized = normalizePathCandidate(command);
30
+ if (!normalized || normalized.startsWith("$")) return normalized;
31
+ const parts = normalized.split("/");
32
+ return parts[parts.length - 1] || normalized;
33
+ }
34
+
35
+ function segmentMatches(patternSegment, candidateSegment) {
36
+ if (patternSegment === "*") return candidateSegment.length > 0;
37
+ const regex = new RegExp(`^${escapeRegex(patternSegment).replace(/\*/g, "[^/]*")}$`);
38
+ return regex.test(candidateSegment);
39
+ }
40
+
41
+ export function globMatchesPath(pattern, candidate) {
42
+ const normalizedPattern = normalizePathCandidate(pattern);
43
+ const normalizedCandidate = normalizePathCandidate(candidate);
44
+ if (!normalizedPattern || !normalizedCandidate) return false;
45
+
46
+ const patternSegments = normalizedPattern.split("/").filter(Boolean);
47
+ const candidateSegments = normalizedCandidate.split("/").filter(Boolean);
48
+
49
+ function match(patternIndex, candidateIndex) {
50
+ if (patternIndex === patternSegments.length) return candidateIndex === candidateSegments.length;
51
+
52
+ const patternSegment = patternSegments[patternIndex];
53
+ if (patternSegment === "**") {
54
+ if (match(patternIndex + 1, candidateIndex)) return true;
55
+ for (let nextCandidate = candidateIndex; nextCandidate < candidateSegments.length; nextCandidate += 1) {
56
+ if (match(patternIndex + 1, nextCandidate + 1)) return true;
57
+ }
58
+ return false;
59
+ }
60
+
61
+ if (candidateIndex >= candidateSegments.length) return false;
62
+ if (!segmentMatches(patternSegment, candidateSegments[candidateIndex])) return false;
63
+ return match(patternIndex + 1, candidateIndex + 1);
64
+ }
65
+
66
+ return match(0, 0);
67
+ }
68
+
69
+ export function matchesAnyPath(candidate, patterns) {
70
+ const normalizedCandidate = normalizePathCandidate(candidate);
71
+ return patterns.find((pattern) => {
72
+ if (globMatchesPath(pattern, normalizedCandidate)) return true;
73
+ if (pattern.endsWith("/**")) {
74
+ const basePattern = pattern.slice(0, -3);
75
+ return globMatchesPath(basePattern, normalizedCandidate);
76
+ }
77
+ return false;
78
+ });
79
+ }
80
+
81
+ export function matchesProtectedPath(candidate, patterns) {
82
+ const normalizedCandidate = normalizePathCandidate(candidate).toLocaleLowerCase("en-US");
83
+ if (!normalizedCandidate) return undefined;
84
+ return patterns.find((pattern) => {
85
+ const normalizedPattern = normalizePathCandidate(pattern).toLocaleLowerCase("en-US");
86
+ if (globMatchesPath(normalizedPattern, normalizedCandidate)) return true;
87
+ if (normalizedPattern.endsWith("/**")) {
88
+ return globMatchesPath(normalizedPattern.slice(0, -3), normalizedCandidate);
89
+ }
90
+ return false;
91
+ });
92
+ }
93
+
94
+ export function splitShellSegments(command) {
95
+ const segments = [];
96
+ let current = "";
97
+ let quote;
98
+ let escaped = false;
99
+ for (let index = 0; index < command.length; index += 1) {
100
+ const char = command[index];
101
+ const next = command[index + 1];
102
+ if (escaped) {
103
+ current += char;
104
+ escaped = false;
105
+ continue;
106
+ }
107
+ if (char === "\\") {
108
+ current += char;
109
+ escaped = true;
110
+ continue;
111
+ }
112
+ if ((char === "'" || char === "\"") && !quote) {
113
+ quote = char;
114
+ current += char;
115
+ continue;
116
+ }
117
+ if (quote === char) {
118
+ quote = undefined;
119
+ current += char;
120
+ continue;
121
+ }
122
+ const previous = command[index - 1];
123
+ const ampersandIsSeparator = char === "&" && next !== "&" && previous !== ">" && previous !== "<";
124
+ if (!quote && (char === "\n" || char === ";" || char === "|" || ampersandIsSeparator || (char === "&" && next === "&") || (char === "|" && next === "|"))) {
125
+ const segment = current.trim();
126
+ if (segment) segments.push(segment);
127
+ current = "";
128
+ if ((char === "&" && next === "&") || (char === "|" && next === "|")) index += 1;
129
+ continue;
130
+ }
131
+ current += char;
132
+ }
133
+ const tail = current.trim();
134
+ if (tail) segments.push(tail);
135
+ return segments.length ? segments : [command.trim()].filter(Boolean);
136
+ }
137
+
138
+ function shellWordTokens(segment) {
139
+ const tokens = [];
140
+ let current = "";
141
+ let quote;
142
+ let escaped = false;
143
+ let activeGlob = false;
144
+ let unquotedVariable = false;
145
+ let variableActive = false;
146
+ const flush = () => {
147
+ if (!current) return;
148
+ tokens.push({ value: current, activeGlob, unquotedVariable, variableActive });
149
+ current = "";
150
+ activeGlob = false;
151
+ unquotedVariable = false;
152
+ variableActive = false;
153
+ };
154
+ for (const char of segment.trim()) {
155
+ if (escaped) {
156
+ current += char;
157
+ escaped = false;
158
+ continue;
159
+ }
160
+ if (char === "\\") {
161
+ escaped = true;
162
+ continue;
163
+ }
164
+ if ((char === "'" || char === "\"") && !quote) {
165
+ quote = char;
166
+ continue;
167
+ }
168
+ if (quote === char) {
169
+ quote = undefined;
170
+ continue;
171
+ }
172
+ if (!quote && /\s/.test(char)) {
173
+ flush();
174
+ continue;
175
+ }
176
+ if (!quote && /[*?{\[]/.test(char)) activeGlob = true;
177
+ if (char === "$" && quote !== "'") {
178
+ variableActive = true;
179
+ if (!quote) unquotedVariable = true;
180
+ }
181
+ current += char;
182
+ }
183
+ flush();
184
+ return tokens;
185
+ }
186
+
187
+ export function shellWords(segment) {
188
+ return shellWordTokens(segment).map((token) => token.value);
189
+ }
190
+
191
+ export function arrayStartsWith(items, prefix) {
192
+ return prefix.length > 0 && prefix.every((item, index) => items[index] === item);
193
+ }
194
+
195
+ function commandRuleMatches(rule, segment, words) {
196
+ if (rule.match === "prefix") {
197
+ const prefix = Array.isArray(rule.value) ? rule.value : shellWords(rule.value);
198
+ return arrayStartsWith(words, prefix);
199
+ }
200
+ const raw = Array.isArray(rule.value) ? rule.value.join(" ") : rule.value;
201
+ if (rule.match === "contains") return segment.toLowerCase().includes(raw.toLowerCase());
202
+ try {
203
+ return new RegExp(raw, "i").test(segment);
204
+ } catch {
205
+ return false;
206
+ }
207
+ }
208
+
209
+ function flagChars(word) {
210
+ if (!word.startsWith("-") || word.startsWith("--")) return "";
211
+ return word.slice(1);
212
+ }
213
+
214
+ function isRootOrHomeTarget(word) {
215
+ const raw = typeof word === "string" ? word.trim().replace(/^['"]|['"]$/g, "") : "";
216
+ if (/^\/+(?:\*)?$/.test(raw)) return true;
217
+ if (/^~(?:\/\*)?$/.test(raw)) return true;
218
+ if (/^\$HOME(?:\/\*)?$/.test(raw) || /^\$\{HOME\}(?:\/\*)?$/.test(raw)) return true;
219
+ const normalized = normalizePathCandidate(word);
220
+ return normalized === "/" || normalized === "~" || normalized === "$HOME" || normalized === "${HOME}";
221
+ }
222
+
223
+ function isAssignment(word) {
224
+ return /^[A-Za-z_][A-Za-z0-9_]*=.*/.test(word);
225
+ }
226
+
227
+ function stripWrapper(words) {
228
+ let current = [...words];
229
+ let changed = true;
230
+
231
+ while (changed && current.length > 0) {
232
+ changed = false;
233
+ while (current.length > 0 && isAssignment(current[0])) {
234
+ current = current.slice(1);
235
+ changed = true;
236
+ }
237
+
238
+ const command = commandBasename(current[0] ?? "");
239
+ if (command === "env") {
240
+ current = current.slice(1);
241
+ while (current.length > 0 && (current[0].startsWith("-") || isAssignment(current[0]))) {
242
+ current = current.slice(1);
243
+ }
244
+ changed = true;
245
+ continue;
246
+ }
247
+
248
+ if (SIMPLE_WRAPPERS.has(command)) {
249
+ current = current.slice(1);
250
+ while (current.length > 0 && current[0].startsWith("-")) {
251
+ current = current.slice(1);
252
+ }
253
+ changed = true;
254
+ }
255
+ }
256
+
257
+ if (current.length > 0) current[0] = commandBasename(current[0]);
258
+ return current;
259
+ }
260
+
261
+ function rmFinding(words) {
262
+ const command = commandBasename(words[0] ?? "");
263
+ const dynamicCommand = command.startsWith("$");
264
+ if (command !== "rm" && !dynamicCommand) return undefined;
265
+
266
+ let recursive = false;
267
+ let force = false;
268
+ const targets = [];
269
+ for (const word of words.slice(1)) {
270
+ if (word === "--recursive") {
271
+ recursive = true;
272
+ continue;
273
+ }
274
+ if (word === "--force") {
275
+ force = true;
276
+ continue;
277
+ }
278
+ if (word.startsWith("-") && word !== "-") {
279
+ const chars = flagChars(word);
280
+ if (chars.includes("r") || chars.includes("R")) recursive = true;
281
+ if (chars.includes("f")) force = true;
282
+ continue;
283
+ }
284
+ targets.push(word);
285
+ }
286
+
287
+ if (targets.some(isRootOrHomeTarget) && (recursive || force || dynamicCommand)) {
288
+ return "Refusing recursive/forced removal of root or home target.";
289
+ }
290
+ return undefined;
291
+ }
292
+
293
+ function findDeleteFinding(words) {
294
+ if (commandBasename(words[0] ?? "") !== "find") return undefined;
295
+ if (!words.includes("-delete")) return undefined;
296
+ const firstTarget = words.slice(1).find((word) => !word.startsWith("-"));
297
+ if (firstTarget && isRootOrHomeTarget(firstTarget)) {
298
+ return "Refusing find -delete against root or home target.";
299
+ }
300
+ return undefined;
301
+ }
302
+
303
+ function ddFinding(words) {
304
+ if (commandBasename(words[0] ?? "") !== "dd") return undefined;
305
+ const out = words.find((word) => /^of=\/dev\/(?:sd[a-z]\d*|hd[a-z]\d*|xvd[a-z]\d*|nvme\d+n\d+(?:p\d+)?|disk\d+|rdisk\d+|mapper\/.+)$/i.test(word));
306
+ return out ? `Refusing dd write to block device ${out}.` : undefined;
307
+ }
308
+
309
+ function semanticCommandFindings(words) {
310
+ const normalizedWords = stripWrapper(words);
311
+ return [
312
+ rmFinding(normalizedWords),
313
+ findDeleteFinding(normalizedWords),
314
+ ddFinding(normalizedWords),
315
+ xargsFinding(normalizedWords)
316
+ ].filter(Boolean);
317
+ }
318
+
319
+ function xargsFinding(words) {
320
+ if (commandBasename(words[0] ?? "") !== "xargs") return undefined;
321
+ const commandIndex = words.findIndex((word, index) => index > 0 && !word.startsWith("-"));
322
+ if (commandIndex < 0) return undefined;
323
+ const nested = words.slice(commandIndex);
324
+ if (commandBasename(nested[0] ?? "") !== "rm") return undefined;
325
+ const hasRecursiveOrForce = nested.slice(1).some((word) => {
326
+ if (word === "--recursive" || word === "--force") return true;
327
+ if (!word.startsWith("-") || word.startsWith("--")) return false;
328
+ const chars = flagChars(word);
329
+ return chars.includes("r") || chars.includes("R") || chars.includes("f");
330
+ });
331
+ return hasRecursiveOrForce ? "Refusing xargs with recursive/forced rm; input target is not visible to policy." : undefined;
332
+ }
333
+
334
+ function legacyPatternMatchesSegment(pattern, words) {
335
+ const patternWords = shellWords(pattern);
336
+ return arrayStartsWith(words, patternWords);
337
+ }
338
+
339
+ export function evaluateExecPolicyCore(command, options) {
340
+ const policy = options.policy ?? {};
341
+ const mode = options.mode ?? policy.execPolicy?.defaultMode ?? "enforce";
342
+ const execPolicy = {
343
+ bannedPrefixSuggestions: policy.execPolicy?.bannedPrefixSuggestions ?? [],
344
+ rules: policy.execPolicy?.rules ?? []
345
+ };
346
+ const reasons = [];
347
+ const pending = splitShellSegments(command).map((segment) => ({ segment, depth: 0 }));
348
+ const segments = [];
349
+
350
+ while (pending.length > 0) {
351
+ const { segment, depth } = pending.shift();
352
+ const words = shellWords(segment);
353
+ const matches = [];
354
+ const warnings = [];
355
+
356
+ for (const prefix of execPolicy.bannedPrefixSuggestions) {
357
+ if (arrayStartsWith(words, prefix)) {
358
+ warnings.push(`Do not persist broad approval prefix: ${prefix.join(" ")}`);
359
+ }
360
+ }
361
+
362
+ for (const rule of execPolicy.rules) {
363
+ if (!commandRuleMatches(rule, segment, words)) continue;
364
+ matches.push(`${rule.action}:${rule.id}`);
365
+ if (rule.action === "forbid") reasons.push(`Forbidden by exec policy ${rule.id}: ${rule.reason}`);
366
+ if (rule.action === "prompt") reasons.push(`Prompt required by exec policy ${rule.id}: ${rule.reason}`);
367
+ }
368
+
369
+ for (const finding of semanticCommandFindings(words)) {
370
+ matches.push("forbid:semantic-shell-safety");
371
+ reasons.push(finding);
372
+ }
373
+
374
+ for (const pattern of policy.blockedCommandPatterns ?? []) {
375
+ if (!legacyPatternMatchesSegment(pattern, words)) continue;
376
+ matches.push(`forbid:legacy:${pattern}`);
377
+ reasons.push(`Blocked by legacy policy pattern: ${pattern}`);
378
+ }
379
+
380
+ segments.push({ command: segment, words, matches, warnings });
381
+ if (depth < 4) {
382
+ for (const nestedCommand of extractNestedCommands(segment, words)) {
383
+ for (const nestedSegment of splitShellSegments(nestedCommand)) {
384
+ pending.push({ segment: nestedSegment, depth: depth + 1 });
385
+ }
386
+ }
387
+ }
388
+ }
389
+
390
+ const normalizedCommand = command.toLowerCase();
391
+ for (const pattern of policy.requireConfirmationPatterns ?? []) {
392
+ if (normalizedCommand.includes(String(pattern).toLowerCase())) {
393
+ reasons.push(`Confirmation required by legacy policy pattern: ${pattern}`);
394
+ break;
395
+ }
396
+ }
397
+
398
+ const hasForbid = reasons.some((reason) => reason.startsWith("Forbidden") || reason.startsWith("Blocked") || reason.startsWith("Refusing"));
399
+ const hasPrompt = reasons.some((reason) => reason.startsWith("Prompt") || reason.startsWith("Confirmation"));
400
+ return {
401
+ mode,
402
+ decision: mode === "off" ? "allow" : hasForbid ? "forbid" : hasPrompt ? "prompt" : "allow",
403
+ reasons,
404
+ segments
405
+ };
406
+ }
407
+
408
+ function findBalanced(text, startIndex, openChar, closeChar) {
409
+ let depth = 0;
410
+ let quote;
411
+ let escaped = false;
412
+ for (let index = startIndex; index < text.length; index += 1) {
413
+ const char = text[index];
414
+ if (escaped) {
415
+ escaped = false;
416
+ continue;
417
+ }
418
+ if (char === "\\") {
419
+ escaped = true;
420
+ continue;
421
+ }
422
+ if ((char === "'" || char === "\"") && !quote) {
423
+ quote = char;
424
+ continue;
425
+ }
426
+ if (quote === char) {
427
+ quote = undefined;
428
+ continue;
429
+ }
430
+ if (quote) continue;
431
+ if (char === openChar) depth += 1;
432
+ if (char === closeChar) {
433
+ depth -= 1;
434
+ if (depth === 0) return { end: index, body: text.slice(startIndex + 1, index) };
435
+ }
436
+ }
437
+ return undefined;
438
+ }
439
+
440
+ function shellCPayloads(words) {
441
+ const normalizedWords = stripWrapper(words);
442
+ const rootCommand = commandBasename(normalizedWords[0] ?? "");
443
+ const carrier = SHELL_COMMANDS.has(rootCommand) || rootCommand === "xargs" || rootCommand === "find";
444
+ if (!carrier) return [];
445
+
446
+ const payloads = [];
447
+ for (let commandIndex = 0; commandIndex < normalizedWords.length; commandIndex += 1) {
448
+ if (!SHELL_COMMANDS.has(commandBasename(normalizedWords[commandIndex] ?? ""))) continue;
449
+ for (let index = commandIndex + 1; index < normalizedWords.length - 1; index += 1) {
450
+ const word = normalizedWords[index];
451
+ if (word === "-c" || (/^-[A-Za-z]+$/.test(word) && word.includes("c"))) {
452
+ payloads.push(normalizedWords[index + 1]);
453
+ break;
454
+ }
455
+ }
456
+ }
457
+ return payloads;
458
+ }
459
+
460
+ function extractNestedCommands(segment, words = shellWords(segment)) {
461
+ const nested = [];
462
+ nested.push(...shellCPayloads(words));
463
+ const normalizedWords = stripWrapper(words);
464
+ const rootCommand = commandBasename(normalizedWords[0] ?? "");
465
+ if (rootCommand === "eval" && normalizedWords.length > 1) nested.push(normalizedWords.slice(1).join(" "));
466
+ if (commandBasename(words[0] ?? "") === "env") {
467
+ const splitIndex = words.indexOf("-S");
468
+ if (splitIndex >= 0 && words[splitIndex + 1]) nested.push(words[splitIndex + 1]);
469
+ }
470
+ if (SHELL_COMMANDS.has(rootCommand)) {
471
+ const hereStringIndex = normalizedWords.indexOf("<<<");
472
+ if (hereStringIndex >= 0 && normalizedWords[hereStringIndex + 1]) nested.push(normalizedWords[hereStringIndex + 1]);
473
+ }
474
+
475
+ let quote;
476
+ let escaped = false;
477
+ for (let index = 0; index < segment.length; index += 1) {
478
+ const char = segment[index];
479
+ const next = segment[index + 1];
480
+ if (escaped) {
481
+ escaped = false;
482
+ continue;
483
+ }
484
+ if (char === "\\") {
485
+ escaped = true;
486
+ continue;
487
+ }
488
+ if ((char === "'" || char === '"') && !quote) {
489
+ quote = char;
490
+ continue;
491
+ }
492
+ if (quote === char) {
493
+ quote = undefined;
494
+ continue;
495
+ }
496
+ if (quote === "'") continue;
497
+ if (char === "$" && next === "(") {
498
+ const match = findBalanced(segment, index + 1, "(", ")");
499
+ if (match) {
500
+ nested.push(match.body);
501
+ index = match.end;
502
+ }
503
+ continue;
504
+ }
505
+ if (char === "`") {
506
+ const end = segment.indexOf("`", index + 1);
507
+ if (end > index) {
508
+ nested.push(segment.slice(index + 1, end));
509
+ index = end;
510
+ }
511
+ continue;
512
+ }
513
+ if (char === "(") {
514
+ if (quote) continue;
515
+ const previous = segment[index - 1];
516
+ if (previous && /[A-Za-z0-9_]/.test(previous)) continue;
517
+ const match = findBalanced(segment, index, "(", ")");
518
+ if (match) {
519
+ nested.push(match.body);
520
+ index = match.end;
521
+ }
522
+ }
523
+ }
524
+
525
+ return nested.filter((item) => typeof item === "string" && item.trim().length > 0);
526
+ }
527
+
528
+ function pathCandidateFromOption(word) {
529
+ const atMatch = word.match(/^@(.+)$/) ?? word.match(/=@(.+)$/);
530
+ if (atMatch) return atMatch[1];
531
+ const equalsMatch = word.match(/^(?:--?[A-Za-z0-9-]+)=(.+)$/);
532
+ return equalsMatch ? equalsMatch[1] : undefined;
533
+ }
534
+
535
+ function isPathLike(word) {
536
+ if (!word || word === "-") return false;
537
+ if (word.startsWith("http://") || word.startsWith("https://")) return false;
538
+ return word.startsWith(".")
539
+ || word.startsWith("/")
540
+ || word.startsWith("~/")
541
+ || word.startsWith("$HOME/")
542
+ || word.includes("/")
543
+ || word === "auth.json";
544
+ }
545
+
546
+ function isFilesystemArgument(word) {
547
+ if (!word || word === "-") return false;
548
+ if (word.startsWith("http://") || word.startsWith("https://")) return false;
549
+ if (PATH_REDIRECT_OPERATORS.has(word)) return false;
550
+ if (isAssignment(word)) return false;
551
+ return isPathLike(word) || !word.startsWith("-");
552
+ }
553
+
554
+ function rememberLeadingShellAssignments(words, assignments) {
555
+ const remember = (word) => {
556
+ const match = word.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
557
+ if (match) assignments.set(match[1], expandKnownShellVariables(match[2], assignments));
558
+ };
559
+ let start = 0;
560
+ while (start < words.length && isAssignment(words[start])) start += 1;
561
+ if (start === words.length) {
562
+ for (const word of words) remember(word);
563
+ return;
564
+ }
565
+ if (!ASSIGNMENT_BUILTINS.has(commandBasename(words[start] ?? ""))) return;
566
+ for (const word of words.slice(start + 1)) remember(word);
567
+ }
568
+
569
+ function expandKnownShellVariables(value, assignments, resolving = new Set(), depth = 0) {
570
+ if (depth >= 8) return String(value ?? "");
571
+ return String(value ?? "").replace(/\$(?:\{([A-Za-z_][A-Za-z0-9_]*)\}|([A-Za-z_][A-Za-z0-9_]*))/g, (match, braced, plain) => {
572
+ const name = braced ?? plain;
573
+ const resolved = assignments.get(name);
574
+ if (resolved === undefined || resolving.has(name)) return match;
575
+ const nextResolving = new Set(resolving);
576
+ nextResolving.add(name);
577
+ return expandKnownShellVariables(resolved, assignments, nextResolving, depth + 1);
578
+ });
579
+ }
580
+
581
+ function executableArgumentTokens(tokens, words, commandName) {
582
+ const commandIndex = words.findIndex((word) => commandBasename(word) === commandName);
583
+ return commandIndex >= 0 ? tokens.slice(commandIndex + 1) : tokens.slice(1);
584
+ }
585
+
586
+ function quotedShellLiterals(segment) {
587
+ const literals = [];
588
+ for (let index = 0; index < segment.length; index += 1) {
589
+ const quote = segment[index];
590
+ if (quote !== "'" && quote !== '"') continue;
591
+ let value = "";
592
+ let escaped = false;
593
+ for (index += 1; index < segment.length; index += 1) {
594
+ const char = segment[index];
595
+ if (escaped) {
596
+ value += `\\${char}`;
597
+ escaped = false;
598
+ continue;
599
+ }
600
+ if (char === "\\" && quote === '"') {
601
+ escaped = true;
602
+ continue;
603
+ }
604
+ if (char === quote) break;
605
+ value += char;
606
+ }
607
+ literals.push(value);
608
+ }
609
+ return literals;
610
+ }
611
+
612
+ function decodeShellDataEscapes(value) {
613
+ const controlIndex = value.indexOf("\\c");
614
+ const bounded = controlIndex >= 0 ? value.slice(0, controlIndex) : value;
615
+ return bounded.replace(/\\(?:x([0-9A-Fa-f]{1,2})|0([0-7]{1,3})|([0-7]{1,3})|u([0-9A-Fa-f]{4})|U([0-9A-Fa-f]{8})|([abefnrtv\\'\"]))/g,
616
+ (match, hex, zeroOctal, octal, shortUnicode, longUnicode, simple) => {
617
+ if (hex) return String.fromCodePoint(Number.parseInt(hex, 16));
618
+ if (zeroOctal || octal) return String.fromCodePoint(Number.parseInt(zeroOctal ?? octal, 8));
619
+ if (shortUnicode || longUnicode) {
620
+ const codePoint = Number.parseInt(shortUnicode ?? longUnicode, 16);
621
+ return codePoint <= 0x10FFFF ? String.fromCodePoint(codePoint) : match;
622
+ }
623
+ return {
624
+ a: "\u0007",
625
+ b: "\b",
626
+ e: "\u001B",
627
+ f: "\f",
628
+ n: "\n",
629
+ r: "\r",
630
+ t: "\t",
631
+ v: "\v",
632
+ "\\": "\\",
633
+ "'": "'",
634
+ '"': '"'
635
+ }[simple] ?? match;
636
+ });
637
+ }
638
+
639
+ function escapedLiteralCandidates(segment) {
640
+ const candidates = [];
641
+ for (const literal of quotedShellLiterals(segment)) {
642
+ const decoded = decodeShellDataEscapes(literal);
643
+ candidates.push(...decoded.split(/\s+/).filter(Boolean));
644
+ }
645
+ return candidates;
646
+ }
647
+
648
+ function shellDataTokens(segment) {
649
+ const tokens = [];
650
+ let current = "";
651
+ let quote;
652
+ let tokenStarted = false;
653
+ let variableActive = false;
654
+ const flush = () => {
655
+ if (!tokenStarted) return;
656
+ tokens.push({ value: current, variableActive });
657
+ current = "";
658
+ tokenStarted = false;
659
+ variableActive = false;
660
+ };
661
+
662
+ for (let index = 0; index < segment.length; index += 1) {
663
+ const char = segment[index];
664
+ const next = segment[index + 1];
665
+ if (quote === "'") {
666
+ tokenStarted = true;
667
+ if (char === "'") quote = undefined;
668
+ else current += char;
669
+ continue;
670
+ }
671
+ if (quote === '"') {
672
+ tokenStarted = true;
673
+ if (char === '"') {
674
+ quote = undefined;
675
+ } else if (char === "\\" && next !== undefined && /[$`"\\\n]/.test(next)) {
676
+ current += next;
677
+ index += 1;
678
+ } else {
679
+ if (char === "$" && next !== undefined) variableActive = true;
680
+ current += char;
681
+ }
682
+ continue;
683
+ }
684
+ if (/\s/.test(char)) {
685
+ flush();
686
+ continue;
687
+ }
688
+ tokenStarted = true;
689
+ if (char === "'" || char === '"') {
690
+ quote = char;
691
+ continue;
692
+ }
693
+ if (char === "\\" && next !== undefined) {
694
+ current += next;
695
+ index += 1;
696
+ continue;
697
+ }
698
+ if (char === "$" && next !== undefined) variableActive = true;
699
+ current += char;
700
+ }
701
+ flush();
702
+ return tokens;
703
+ }
704
+
705
+ function renderStaticPrintf(format, values) {
706
+ let valueIndex = 0;
707
+ let rendered = "";
708
+ const decodedFormat = decodeShellDataEscapes(format);
709
+ const conversion = /%(?:\d+\$)?[-+#0 ']*(?:\d+|\*)?(?:\.(?:\d+|\*))?[hlL]?[bcdeEfgGiosuxXaA]/g;
710
+ let cursor = 0;
711
+ for (const match of decodedFormat.matchAll(conversion)) {
712
+ if (rendered.length > 8192 || valueIndex >= 32) break;
713
+ const start = match.index ?? 0;
714
+ rendered += decodedFormat.slice(cursor, start).replace(/%%/g, "%");
715
+ const specifier = match[0].slice(-1);
716
+ const argument = values[valueIndex] ?? "";
717
+ rendered += specifier === "b" ? decodeShellDataEscapes(argument) : argument;
718
+ valueIndex += 1;
719
+ cursor = start + match[0].length;
720
+ }
721
+ rendered += decodedFormat.slice(cursor).replace(/%%/g, "%");
722
+ return rendered.slice(0, 8192);
723
+ }
724
+
725
+ function staticDataOutputCandidates(segment, assignments, producerCommand) {
726
+ const tokens = shellDataTokens(segment);
727
+ const commandIndex = tokens.findIndex((token) => commandBasename(token.value) === producerCommand);
728
+ if (commandIndex < 0) return [];
729
+ const args = tokens.slice(commandIndex + 1).map((token) => token.variableActive
730
+ ? expandKnownShellVariables(token.value, assignments)
731
+ : token.value);
732
+ let output = "";
733
+ if (producerCommand === "echo") {
734
+ let argumentIndex = 0;
735
+ let escapes = false;
736
+ while (argumentIndex < args.length && /^-[eEn]+$/.test(args[argumentIndex])) {
737
+ if (args[argumentIndex].slice(1).includes("e")) escapes = true;
738
+ if (args[argumentIndex].slice(1).includes("E")) escapes = false;
739
+ argumentIndex += 1;
740
+ }
741
+ output = args.slice(argumentIndex).join(" ");
742
+ if (escapes) output = decodeShellDataEscapes(output);
743
+ } else if (producerCommand === "printf" && args.length > 0) {
744
+ output = renderStaticPrintf(args[0], args.slice(1));
745
+ }
746
+ return output.split(/\s+/).filter(Boolean);
747
+ }
748
+
749
+ function echoEscapeMode(words, commandIndex) {
750
+ for (const word of words.slice(commandIndex + 1)) {
751
+ if (!/^-[eEn]+$/.test(word)) break;
752
+ if (word.slice(1).includes("e")) return true;
753
+ }
754
+ return false;
755
+ }
756
+
757
+ const RG_VALUE_SHORT_OPTIONS = new Set(["A", "B", "C", "E", "e", "f", "g", "j", "M", "m", "r", "t", "T"]);
758
+
759
+ function attachedRgShortOption(word) {
760
+ if (!/^-[^-]/.test(word)) return undefined;
761
+ const cluster = word.slice(1);
762
+ for (let index = 0; index < cluster.length; index += 1) {
763
+ const option = cluster[index];
764
+ if (!RG_VALUE_SHORT_OPTIONS.has(option)) continue;
765
+ const value = cluster.slice(index + 1);
766
+ return value ? { option: `-${option}`, value } : undefined;
767
+ }
768
+ return undefined;
769
+ }
770
+
771
+ function xargsPipelineInputCandidates(command) {
772
+ const segments = splitShellSegments(command);
773
+ const candidates = [];
774
+ const assignments = new Map();
775
+ for (let index = 0; index < segments.length; index += 1) {
776
+ const producerTokens = shellWordTokens(segments[index]);
777
+ const producerWords = producerTokens.map((token) => token.value);
778
+ rememberLeadingShellAssignments(producerWords, assignments);
779
+ if (index + 1 >= segments.length) continue;
780
+ const consumerWords = shellWords(segments[index + 1]);
781
+ const consumerCommand = commandBasename(stripWrapper(consumerWords)[0] ?? "");
782
+ if (consumerCommand !== "xargs") continue;
783
+ const producerCommand = commandBasename(stripWrapper(producerWords)[0] ?? "");
784
+ if (!DATA_ONLY_COMMANDS.has(producerCommand)) continue;
785
+ const commandIndex = producerWords.findIndex((word) => commandBasename(word) === producerCommand);
786
+ for (const token of producerTokens.slice(commandIndex + 1)) {
787
+ const word = token.variableActive ? expandKnownShellVariables(token.value, assignments) : token.value;
788
+ if (word.startsWith("-") || word === "%s" || word === "%s\\n") continue;
789
+ candidates.push(word);
790
+ }
791
+ if (producerCommand === "printf" || (producerCommand === "echo" && echoEscapeMode(producerWords, commandIndex))) {
792
+ candidates.push(...escapedLiteralCandidates(segments[index]));
793
+ }
794
+ candidates.push(...staticDataOutputCandidates(segments[index], assignments, producerCommand));
795
+ }
796
+ return candidates;
797
+ }
798
+
799
+ function extractAttachedRedirectionPaths(segment) {
800
+ const paths = [];
801
+ let quote;
802
+ let escaped = false;
803
+
804
+ for (let index = 0; index < segment.length; index += 1) {
805
+ const char = segment[index];
806
+ if (escaped) {
807
+ escaped = false;
808
+ continue;
809
+ }
810
+ if (char === "\\") {
811
+ escaped = true;
812
+ continue;
813
+ }
814
+ if ((char === "'" || char === '"') && !quote) {
815
+ quote = char;
816
+ continue;
817
+ }
818
+ if (quote === char) {
819
+ quote = undefined;
820
+ continue;
821
+ }
822
+ if (quote || (char !== "<" && char !== ">")) continue;
823
+
824
+ const next = segment[index + 1];
825
+ if (char === "<" && next === "<") {
826
+ while (segment[index + 1] === "<") index += 1;
827
+ continue;
828
+ }
829
+ if (next === "&") {
830
+ index += 1;
831
+ continue;
832
+ }
833
+
834
+ if ((char === ">" && (next === ">" || next === "|")) || (char === "<" && next === ">")) index += 1;
835
+ let cursor = index + 1;
836
+ while (/\s/.test(segment[cursor] ?? "")) cursor += 1;
837
+ if (!segment[cursor] || segment[cursor] === "&" || segment[cursor] === "-") continue;
838
+
839
+ let target = "";
840
+ let targetQuote;
841
+ let targetEscaped = false;
842
+ for (; cursor < segment.length; cursor += 1) {
843
+ const targetChar = segment[cursor];
844
+ if (targetEscaped) {
845
+ target += targetChar;
846
+ targetEscaped = false;
847
+ continue;
848
+ }
849
+ if (targetChar === "\\") {
850
+ targetEscaped = true;
851
+ continue;
852
+ }
853
+ if ((targetChar === "'" || targetChar === '"') && !targetQuote) {
854
+ targetQuote = targetChar;
855
+ continue;
856
+ }
857
+ if (targetQuote === targetChar) {
858
+ targetQuote = undefined;
859
+ continue;
860
+ }
861
+ if (!targetQuote && (/\s/.test(targetChar) || /[;&|<>]/.test(targetChar))) break;
862
+ target += targetChar;
863
+ }
864
+ if (target) paths.push(target);
865
+ index = Math.max(index, cursor - 1);
866
+ }
867
+
868
+ return paths;
869
+ }
870
+
871
+ export function extractShellPathCandidates(command) {
872
+ const candidates = [...xargsPipelineInputCandidates(command)];
873
+ const assignments = new Map();
874
+ const addCandidate = (candidate, variableActive = true) => {
875
+ if (typeof candidate !== "string" || !candidate) return;
876
+ candidates.push(variableActive ? expandKnownShellVariables(candidate, assignments) : candidate);
877
+ };
878
+ const pending = splitShellSegments(command).map((segment) => ({ segment, depth: 0 }));
879
+ while (pending.length > 0) {
880
+ const { segment, depth } = pending.shift();
881
+ const tokens = shellWordTokens(segment);
882
+ const words = tokens.map((token) => token.value);
883
+ rememberLeadingShellAssignments(words, assignments);
884
+ const commandName = commandBasename(stripWrapper(words)[0] ?? "");
885
+ for (const redirectPath of extractAttachedRedirectionPaths(segment)) addCandidate(redirectPath);
886
+ if (!DATA_ONLY_COMMANDS.has(commandName) || depth > 0) {
887
+ const argumentTokens = executableArgumentTokens(tokens, words, commandName);
888
+ let searchPatternPending = SEARCH_COMMANDS.has(commandName);
889
+ for (let index = 0; index < argumentTokens.length; index += 1) {
890
+ const word = argumentTokens[index].value;
891
+ if (word === "<<" || word === "<<<") {
892
+ if (commandName === "xargs" && word === "<<<" && argumentTokens[index + 1]) {
893
+ const input = argumentTokens[index + 1];
894
+ addCandidate(input.value, input.variableActive);
895
+ }
896
+ index += 1;
897
+ continue;
898
+ }
899
+ if (PATH_REDIRECT_OPERATORS.has(word) && argumentTokens[index + 1]) {
900
+ const target = argumentTokens[index + 1];
901
+ addCandidate(target.value, target.variableActive);
902
+ index += 1;
903
+ continue;
904
+ }
905
+ if (/^(?:\d*)>>?/.test(word) || /^&>>?/.test(word)) {
906
+ const pathPart = word.replace(/^(?:\d*|&)>>?/, "");
907
+ if (pathPart) addCandidate(pathPart, argumentTokens[index].variableActive);
908
+ continue;
909
+ }
910
+ const optionEquals = word.indexOf("=");
911
+ const optionName = optionEquals > 0 ? word.slice(0, optionEquals) : word;
912
+ const inlineOptionValue = optionEquals > 0 ? word.slice(optionEquals + 1) : undefined;
913
+ if (SEARCH_COMMANDS.has(commandName) && SEARCH_PATTERN_OPTIONS.has(optionName)) {
914
+ searchPatternPending = false;
915
+ if (inlineOptionValue === undefined) index += 1;
916
+ continue;
917
+ }
918
+ const attachedGrepFile = ["grep", "egrep", "fgrep"].includes(commandName) && /^-f.+/.test(word)
919
+ ? word.slice(2)
920
+ : undefined;
921
+ if (attachedGrepFile) {
922
+ addCandidate(attachedGrepFile, argumentTokens[index].variableActive);
923
+ searchPatternPending = false;
924
+ continue;
925
+ }
926
+ const attachedRgOption = ["rg", "ripgrep"].includes(commandName) ? attachedRgShortOption(word) : undefined;
927
+ if (attachedRgOption?.option === "-e") {
928
+ searchPatternPending = false;
929
+ continue;
930
+ }
931
+ if (attachedRgOption?.option === "-f") {
932
+ addCandidate(attachedRgOption.value, argumentTokens[index].variableActive);
933
+ searchPatternPending = false;
934
+ continue;
935
+ }
936
+ if (attachedRgOption?.option === "-g") {
937
+ addCandidate(attachedRgOption.value, argumentTokens[index].variableActive);
938
+ continue;
939
+ }
940
+ if (SEARCH_COMMANDS.has(commandName) && (SEARCH_FILE_OPTIONS.has(optionName) || SEARCH_GLOB_OPTIONS.has(optionName))) {
941
+ if (inlineOptionValue !== undefined) {
942
+ addCandidate(inlineOptionValue, argumentTokens[index].variableActive);
943
+ } else {
944
+ const target = argumentTokens[index + 1];
945
+ if (target) addCandidate(target.value, target.variableActive);
946
+ index += 1;
947
+ }
948
+ if (SEARCH_FILE_OPTIONS.has(optionName) && (optionName === "-f" || optionName === "--file")) searchPatternPending = false;
949
+ continue;
950
+ }
951
+ const optionPath = pathCandidateFromOption(word);
952
+ if (optionPath) {
953
+ addCandidate(optionPath, argumentTokens[index].variableActive);
954
+ continue;
955
+ }
956
+ if (word.startsWith("-")) continue;
957
+ if (searchPatternPending) {
958
+ searchPatternPending = false;
959
+ continue;
960
+ }
961
+ if (isFilesystemArgument(word)) addCandidate(word, argumentTokens[index].variableActive);
962
+ }
963
+ }
964
+ if (depth < 4) {
965
+ for (const nestedCommand of extractNestedCommands(segment, words)) {
966
+ for (const nestedSegment of splitShellSegments(nestedCommand)) {
967
+ pending.push({ segment: nestedSegment, depth: depth + 1 });
968
+ }
969
+ }
970
+ }
971
+ }
972
+ return [...new Set(candidates.map(normalizePathCandidate).filter(Boolean))];
973
+ }
974
+
975
+ export function extractShellGlobCandidates(command) {
976
+ const candidates = [];
977
+ const assignments = new Map();
978
+ const pending = splitShellSegments(command).map((segment) => ({ segment, depth: 0 }));
979
+ while (pending.length > 0) {
980
+ const { segment, depth } = pending.shift();
981
+ const tokens = shellWordTokens(segment);
982
+ const words = tokens.map((token) => token.value);
983
+ rememberLeadingShellAssignments(words, assignments);
984
+ const commandName = commandBasename(stripWrapper(words)[0] ?? "");
985
+ if (!DATA_ONLY_COMMANDS.has(commandName) || depth > 0) {
986
+ const argumentTokens = executableArgumentTokens(tokens, words, commandName);
987
+ let searchPatternPending = SEARCH_COMMANDS.has(commandName);
988
+ for (let index = 0; index < argumentTokens.length; index += 1) {
989
+ const token = argumentTokens[index];
990
+ if (token.value === "<<" || token.value === "<<<") {
991
+ if (commandName === "xargs" && token.value === "<<<" && argumentTokens[index + 1]) {
992
+ const input = argumentTokens[index + 1];
993
+ const expanded = expandKnownShellVariables(input.value, assignments);
994
+ if (input.activeGlob || (input.unquotedVariable && /[*?{\[]/.test(expanded))) candidates.push(expanded);
995
+ }
996
+ index += 1;
997
+ continue;
998
+ }
999
+ if (PATH_REDIRECT_OPERATORS.has(token.value)) {
1000
+ const target = argumentTokens[index + 1];
1001
+ if (target) {
1002
+ const expanded = expandKnownShellVariables(target.value, assignments);
1003
+ if (target.activeGlob || (target.unquotedVariable && /[*?{\[]/.test(expanded))) candidates.push(expanded);
1004
+ }
1005
+ index += 1;
1006
+ continue;
1007
+ }
1008
+ const optionEquals = token.value.indexOf("=");
1009
+ const optionName = optionEquals > 0 ? token.value.slice(0, optionEquals) : token.value;
1010
+ const inlineOptionValue = optionEquals > 0 ? token.value.slice(optionEquals + 1) : undefined;
1011
+ if (SEARCH_COMMANDS.has(commandName) && SEARCH_PATTERN_OPTIONS.has(optionName)) {
1012
+ searchPatternPending = false;
1013
+ if (inlineOptionValue === undefined) index += 1;
1014
+ continue;
1015
+ }
1016
+ const attachedRgOption = ["rg", "ripgrep"].includes(commandName) ? attachedRgShortOption(token.value) : undefined;
1017
+ if (attachedRgOption?.option === "-e") {
1018
+ searchPatternPending = false;
1019
+ continue;
1020
+ }
1021
+ if (attachedRgOption?.option === "-f") {
1022
+ searchPatternPending = false;
1023
+ continue;
1024
+ }
1025
+ if (attachedRgOption?.option === "-g") {
1026
+ const expanded = token.variableActive
1027
+ ? expandKnownShellVariables(attachedRgOption.value, assignments)
1028
+ : attachedRgOption.value;
1029
+ candidates.push(expanded);
1030
+ continue;
1031
+ }
1032
+ if (SEARCH_COMMANDS.has(commandName) && (SEARCH_FILE_OPTIONS.has(optionName) || SEARCH_GLOB_OPTIONS.has(optionName))) {
1033
+ const target = inlineOptionValue === undefined ? argumentTokens[index + 1] : undefined;
1034
+ const targetValue = inlineOptionValue ?? target?.value;
1035
+ if (targetValue !== undefined) {
1036
+ const targetVariableActive = inlineOptionValue === undefined ? target?.variableActive : token.variableActive;
1037
+ const expanded = targetVariableActive ? expandKnownShellVariables(targetValue, assignments) : targetValue;
1038
+ if (SEARCH_GLOB_OPTIONS.has(optionName) || target?.activeGlob || (target?.unquotedVariable && /[*?{\[]/.test(expanded))) {
1039
+ candidates.push(expanded);
1040
+ }
1041
+ }
1042
+ if (SEARCH_FILE_OPTIONS.has(optionName) && (optionName === "-f" || optionName === "--file")) searchPatternPending = false;
1043
+ if (inlineOptionValue === undefined) index += 1;
1044
+ continue;
1045
+ }
1046
+ if (token.value.startsWith("-")) continue;
1047
+ if (searchPatternPending) {
1048
+ searchPatternPending = false;
1049
+ continue;
1050
+ }
1051
+ const expanded = expandKnownShellVariables(token.value, assignments);
1052
+ if (token.activeGlob || (token.unquotedVariable && /[*?{\[]/.test(expanded))) candidates.push(expanded);
1053
+ }
1054
+ }
1055
+ if (depth < 4) {
1056
+ for (const nestedCommand of extractNestedCommands(segment, words)) {
1057
+ for (const nestedSegment of splitShellSegments(nestedCommand)) pending.push({ segment: nestedSegment, depth: depth + 1 });
1058
+ }
1059
+ }
1060
+ }
1061
+ return [...new Set(candidates.map(normalizePathCandidate).filter(Boolean))];
1062
+ }
1063
+
1064
+ export function findProtectedPathInCommand(command, protectedPatterns) {
1065
+ for (const candidate of extractShellPathCandidates(command)) {
1066
+ const pattern = matchesProtectedPath(candidate, protectedPatterns);
1067
+ if (pattern) return { candidate, pattern };
1068
+ }
1069
+ return undefined;
1070
+ }