@animus-labs/cortex 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +73 -0
  3. package/dist/budget-guard.d.ts +75 -0
  4. package/dist/budget-guard.d.ts.map +1 -0
  5. package/dist/budget-guard.js +142 -0
  6. package/dist/budget-guard.js.map +1 -0
  7. package/dist/compaction/compaction.d.ts +99 -0
  8. package/dist/compaction/compaction.d.ts.map +1 -0
  9. package/dist/compaction/compaction.js +302 -0
  10. package/dist/compaction/compaction.js.map +1 -0
  11. package/dist/compaction/failsafe.d.ts +57 -0
  12. package/dist/compaction/failsafe.d.ts.map +1 -0
  13. package/dist/compaction/failsafe.js +135 -0
  14. package/dist/compaction/failsafe.js.map +1 -0
  15. package/dist/compaction/index.d.ts +381 -0
  16. package/dist/compaction/index.d.ts.map +1 -0
  17. package/dist/compaction/index.js +979 -0
  18. package/dist/compaction/index.js.map +1 -0
  19. package/dist/compaction/microcompaction.d.ts +219 -0
  20. package/dist/compaction/microcompaction.d.ts.map +1 -0
  21. package/dist/compaction/microcompaction.js +536 -0
  22. package/dist/compaction/microcompaction.js.map +1 -0
  23. package/dist/compaction/observational/buffering.d.ts +225 -0
  24. package/dist/compaction/observational/buffering.d.ts.map +1 -0
  25. package/dist/compaction/observational/buffering.js +354 -0
  26. package/dist/compaction/observational/buffering.js.map +1 -0
  27. package/dist/compaction/observational/constants.d.ts +70 -0
  28. package/dist/compaction/observational/constants.d.ts.map +1 -0
  29. package/dist/compaction/observational/constants.js +507 -0
  30. package/dist/compaction/observational/constants.js.map +1 -0
  31. package/dist/compaction/observational/index.d.ts +219 -0
  32. package/dist/compaction/observational/index.d.ts.map +1 -0
  33. package/dist/compaction/observational/index.js +641 -0
  34. package/dist/compaction/observational/index.js.map +1 -0
  35. package/dist/compaction/observational/observer.d.ts +97 -0
  36. package/dist/compaction/observational/observer.d.ts.map +1 -0
  37. package/dist/compaction/observational/observer.js +424 -0
  38. package/dist/compaction/observational/observer.js.map +1 -0
  39. package/dist/compaction/observational/recall-tool.d.ts +27 -0
  40. package/dist/compaction/observational/recall-tool.d.ts.map +1 -0
  41. package/dist/compaction/observational/recall-tool.js +93 -0
  42. package/dist/compaction/observational/recall-tool.js.map +1 -0
  43. package/dist/compaction/observational/reflector.d.ts +94 -0
  44. package/dist/compaction/observational/reflector.d.ts.map +1 -0
  45. package/dist/compaction/observational/reflector.js +167 -0
  46. package/dist/compaction/observational/reflector.js.map +1 -0
  47. package/dist/compaction/observational/types.d.ts +271 -0
  48. package/dist/compaction/observational/types.d.ts.map +1 -0
  49. package/dist/compaction/observational/types.js +15 -0
  50. package/dist/compaction/observational/types.js.map +1 -0
  51. package/dist/context-manager.d.ts +134 -0
  52. package/dist/context-manager.d.ts.map +1 -0
  53. package/dist/context-manager.js +170 -0
  54. package/dist/context-manager.js.map +1 -0
  55. package/dist/cortex-agent.d.ts +1020 -0
  56. package/dist/cortex-agent.d.ts.map +1 -0
  57. package/dist/cortex-agent.js +3589 -0
  58. package/dist/cortex-agent.js.map +1 -0
  59. package/dist/error-classifier.d.ts +48 -0
  60. package/dist/error-classifier.d.ts.map +1 -0
  61. package/dist/error-classifier.js +152 -0
  62. package/dist/error-classifier.js.map +1 -0
  63. package/dist/event-bridge.d.ts +166 -0
  64. package/dist/event-bridge.d.ts.map +1 -0
  65. package/dist/event-bridge.js +381 -0
  66. package/dist/event-bridge.js.map +1 -0
  67. package/dist/index.d.ts +55 -0
  68. package/dist/index.d.ts.map +1 -0
  69. package/dist/index.js +57 -0
  70. package/dist/index.js.map +1 -0
  71. package/dist/mcp-client.d.ts +119 -0
  72. package/dist/mcp-client.d.ts.map +1 -0
  73. package/dist/mcp-client.js +474 -0
  74. package/dist/mcp-client.js.map +1 -0
  75. package/dist/model-wrapper.d.ts +58 -0
  76. package/dist/model-wrapper.d.ts.map +1 -0
  77. package/dist/model-wrapper.js +86 -0
  78. package/dist/model-wrapper.js.map +1 -0
  79. package/dist/noop-logger.d.ts +4 -0
  80. package/dist/noop-logger.d.ts.map +1 -0
  81. package/dist/noop-logger.js +8 -0
  82. package/dist/noop-logger.js.map +1 -0
  83. package/dist/prompt-diagnostics.d.ts +47 -0
  84. package/dist/prompt-diagnostics.d.ts.map +1 -0
  85. package/dist/prompt-diagnostics.js +230 -0
  86. package/dist/prompt-diagnostics.js.map +1 -0
  87. package/dist/provider-manager.d.ts +224 -0
  88. package/dist/provider-manager.d.ts.map +1 -0
  89. package/dist/provider-manager.js +563 -0
  90. package/dist/provider-manager.js.map +1 -0
  91. package/dist/provider-registry.d.ts +115 -0
  92. package/dist/provider-registry.d.ts.map +1 -0
  93. package/dist/provider-registry.js +305 -0
  94. package/dist/provider-registry.js.map +1 -0
  95. package/dist/schema-converter.d.ts +20 -0
  96. package/dist/schema-converter.d.ts.map +1 -0
  97. package/dist/schema-converter.js +48 -0
  98. package/dist/schema-converter.js.map +1 -0
  99. package/dist/skill-preprocessor.d.ts +46 -0
  100. package/dist/skill-preprocessor.d.ts.map +1 -0
  101. package/dist/skill-preprocessor.js +237 -0
  102. package/dist/skill-preprocessor.js.map +1 -0
  103. package/dist/skill-registry.d.ts +107 -0
  104. package/dist/skill-registry.d.ts.map +1 -0
  105. package/dist/skill-registry.js +330 -0
  106. package/dist/skill-registry.js.map +1 -0
  107. package/dist/skill-tool.d.ts +54 -0
  108. package/dist/skill-tool.d.ts.map +1 -0
  109. package/dist/skill-tool.js +88 -0
  110. package/dist/skill-tool.js.map +1 -0
  111. package/dist/sub-agent-manager.d.ts +90 -0
  112. package/dist/sub-agent-manager.d.ts.map +1 -0
  113. package/dist/sub-agent-manager.js +192 -0
  114. package/dist/sub-agent-manager.js.map +1 -0
  115. package/dist/token-estimator.d.ts +23 -0
  116. package/dist/token-estimator.d.ts.map +1 -0
  117. package/dist/token-estimator.js +27 -0
  118. package/dist/token-estimator.js.map +1 -0
  119. package/dist/tool-contract.d.ts +68 -0
  120. package/dist/tool-contract.d.ts.map +1 -0
  121. package/dist/tool-contract.js +35 -0
  122. package/dist/tool-contract.js.map +1 -0
  123. package/dist/tool-result-persistence.d.ts +89 -0
  124. package/dist/tool-result-persistence.d.ts.map +1 -0
  125. package/dist/tool-result-persistence.js +152 -0
  126. package/dist/tool-result-persistence.js.map +1 -0
  127. package/dist/tools/bash/index.d.ts +71 -0
  128. package/dist/tools/bash/index.d.ts.map +1 -0
  129. package/dist/tools/bash/index.js +485 -0
  130. package/dist/tools/bash/index.js.map +1 -0
  131. package/dist/tools/bash/interactive.d.ts +47 -0
  132. package/dist/tools/bash/interactive.d.ts.map +1 -0
  133. package/dist/tools/bash/interactive.js +262 -0
  134. package/dist/tools/bash/interactive.js.map +1 -0
  135. package/dist/tools/bash/safety.d.ts +149 -0
  136. package/dist/tools/bash/safety.d.ts.map +1 -0
  137. package/dist/tools/bash/safety.js +1116 -0
  138. package/dist/tools/bash/safety.js.map +1 -0
  139. package/dist/tools/edit.d.ts +57 -0
  140. package/dist/tools/edit.d.ts.map +1 -0
  141. package/dist/tools/edit.js +310 -0
  142. package/dist/tools/edit.js.map +1 -0
  143. package/dist/tools/glob.d.ts +34 -0
  144. package/dist/tools/glob.d.ts.map +1 -0
  145. package/dist/tools/glob.js +268 -0
  146. package/dist/tools/glob.js.map +1 -0
  147. package/dist/tools/grep.d.ts +53 -0
  148. package/dist/tools/grep.d.ts.map +1 -0
  149. package/dist/tools/grep.js +673 -0
  150. package/dist/tools/grep.js.map +1 -0
  151. package/dist/tools/index.d.ts +62 -0
  152. package/dist/tools/index.d.ts.map +1 -0
  153. package/dist/tools/index.js +52 -0
  154. package/dist/tools/index.js.map +1 -0
  155. package/dist/tools/read.d.ts +43 -0
  156. package/dist/tools/read.d.ts.map +1 -0
  157. package/dist/tools/read.js +459 -0
  158. package/dist/tools/read.js.map +1 -0
  159. package/dist/tools/runtime.d.ts +62 -0
  160. package/dist/tools/runtime.d.ts.map +1 -0
  161. package/dist/tools/runtime.js +116 -0
  162. package/dist/tools/runtime.js.map +1 -0
  163. package/dist/tools/shared/cwd-tracker.d.ts +32 -0
  164. package/dist/tools/shared/cwd-tracker.d.ts.map +1 -0
  165. package/dist/tools/shared/cwd-tracker.js +44 -0
  166. package/dist/tools/shared/cwd-tracker.js.map +1 -0
  167. package/dist/tools/shared/edit-history.d.ts +55 -0
  168. package/dist/tools/shared/edit-history.d.ts.map +1 -0
  169. package/dist/tools/shared/edit-history.js +72 -0
  170. package/dist/tools/shared/edit-history.js.map +1 -0
  171. package/dist/tools/shared/edit-matcher.d.ts +83 -0
  172. package/dist/tools/shared/edit-matcher.d.ts.map +1 -0
  173. package/dist/tools/shared/edit-matcher.js +359 -0
  174. package/dist/tools/shared/edit-matcher.js.map +1 -0
  175. package/dist/tools/shared/file-mutation-lock.d.ts +22 -0
  176. package/dist/tools/shared/file-mutation-lock.d.ts.map +1 -0
  177. package/dist/tools/shared/file-mutation-lock.js +35 -0
  178. package/dist/tools/shared/file-mutation-lock.js.map +1 -0
  179. package/dist/tools/shared/gitignore.d.ts +17 -0
  180. package/dist/tools/shared/gitignore.d.ts.map +1 -0
  181. package/dist/tools/shared/gitignore.js +59 -0
  182. package/dist/tools/shared/gitignore.js.map +1 -0
  183. package/dist/tools/shared/pdf-extractor.d.ts +96 -0
  184. package/dist/tools/shared/pdf-extractor.d.ts.map +1 -0
  185. package/dist/tools/shared/pdf-extractor.js +196 -0
  186. package/dist/tools/shared/pdf-extractor.js.map +1 -0
  187. package/dist/tools/shared/read-registry.d.ts +66 -0
  188. package/dist/tools/shared/read-registry.d.ts.map +1 -0
  189. package/dist/tools/shared/read-registry.js +65 -0
  190. package/dist/tools/shared/read-registry.js.map +1 -0
  191. package/dist/tools/shared/safe-env.d.ts +18 -0
  192. package/dist/tools/shared/safe-env.d.ts.map +1 -0
  193. package/dist/tools/shared/safe-env.js +70 -0
  194. package/dist/tools/shared/safe-env.js.map +1 -0
  195. package/dist/tools/sub-agent.d.ts +91 -0
  196. package/dist/tools/sub-agent.d.ts.map +1 -0
  197. package/dist/tools/sub-agent.js +89 -0
  198. package/dist/tools/sub-agent.js.map +1 -0
  199. package/dist/tools/task-output.d.ts +38 -0
  200. package/dist/tools/task-output.d.ts.map +1 -0
  201. package/dist/tools/task-output.js +186 -0
  202. package/dist/tools/task-output.js.map +1 -0
  203. package/dist/tools/tool-search/index.d.ts +40 -0
  204. package/dist/tools/tool-search/index.d.ts.map +1 -0
  205. package/dist/tools/tool-search/index.js +110 -0
  206. package/dist/tools/tool-search/index.js.map +1 -0
  207. package/dist/tools/tool-search/registry.d.ts +82 -0
  208. package/dist/tools/tool-search/registry.d.ts.map +1 -0
  209. package/dist/tools/tool-search/registry.js +238 -0
  210. package/dist/tools/tool-search/registry.js.map +1 -0
  211. package/dist/tools/undo-edit.d.ts +51 -0
  212. package/dist/tools/undo-edit.d.ts.map +1 -0
  213. package/dist/tools/undo-edit.js +231 -0
  214. package/dist/tools/undo-edit.js.map +1 -0
  215. package/dist/tools/web-fetch/cache.d.ts +49 -0
  216. package/dist/tools/web-fetch/cache.d.ts.map +1 -0
  217. package/dist/tools/web-fetch/cache.js +89 -0
  218. package/dist/tools/web-fetch/cache.js.map +1 -0
  219. package/dist/tools/web-fetch/index.d.ts +53 -0
  220. package/dist/tools/web-fetch/index.d.ts.map +1 -0
  221. package/dist/tools/web-fetch/index.js +513 -0
  222. package/dist/tools/web-fetch/index.js.map +1 -0
  223. package/dist/tools/write.d.ts +59 -0
  224. package/dist/tools/write.d.ts.map +1 -0
  225. package/dist/tools/write.js +316 -0
  226. package/dist/tools/write.js.map +1 -0
  227. package/dist/types.d.ts +881 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +16 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/working-tags.d.ts +44 -0
  232. package/dist/working-tags.d.ts.map +1 -0
  233. package/dist/working-tags.js +103 -0
  234. package/dist/working-tags.js.map +1 -0
  235. package/package.json +87 -0
  236. package/src/budget-guard.ts +170 -0
  237. package/src/compaction/compaction.ts +386 -0
  238. package/src/compaction/failsafe.ts +185 -0
  239. package/src/compaction/index.ts +1199 -0
  240. package/src/compaction/microcompaction.ts +709 -0
  241. package/src/compaction/observational/buffering.ts +430 -0
  242. package/src/compaction/observational/constants.ts +532 -0
  243. package/src/compaction/observational/index.ts +837 -0
  244. package/src/compaction/observational/observer.ts +510 -0
  245. package/src/compaction/observational/recall-tool.ts +130 -0
  246. package/src/compaction/observational/reflector.ts +221 -0
  247. package/src/compaction/observational/types.ts +343 -0
  248. package/src/context-manager.ts +237 -0
  249. package/src/cortex-agent.ts +4297 -0
  250. package/src/error-classifier.ts +199 -0
  251. package/src/event-bridge.ts +508 -0
  252. package/src/index.ts +292 -0
  253. package/src/mcp-client.ts +582 -0
  254. package/src/model-wrapper.ts +128 -0
  255. package/src/noop-logger.ts +9 -0
  256. package/src/prompt-diagnostics.ts +296 -0
  257. package/src/provider-manager.ts +823 -0
  258. package/src/provider-registry.ts +386 -0
  259. package/src/schema-converter.ts +51 -0
  260. package/src/skill-preprocessor.ts +314 -0
  261. package/src/skill-registry.ts +378 -0
  262. package/src/skill-tool.ts +130 -0
  263. package/src/sub-agent-manager.ts +236 -0
  264. package/src/token-estimator.ts +26 -0
  265. package/src/tool-contract.ts +113 -0
  266. package/src/tool-result-persistence.ts +197 -0
  267. package/src/tools/bash/index.ts +633 -0
  268. package/src/tools/bash/interactive.ts +302 -0
  269. package/src/tools/bash/safety.ts +1297 -0
  270. package/src/tools/edit.ts +422 -0
  271. package/src/tools/glob.ts +330 -0
  272. package/src/tools/grep.ts +819 -0
  273. package/src/tools/index.ts +110 -0
  274. package/src/tools/read.ts +580 -0
  275. package/src/tools/runtime.ts +173 -0
  276. package/src/tools/shared/cwd-tracker.ts +50 -0
  277. package/src/tools/shared/edit-history.ts +96 -0
  278. package/src/tools/shared/edit-matcher.ts +457 -0
  279. package/src/tools/shared/file-mutation-lock.ts +40 -0
  280. package/src/tools/shared/gitignore.ts +61 -0
  281. package/src/tools/shared/pdf-extractor.ts +290 -0
  282. package/src/tools/shared/read-registry.ts +93 -0
  283. package/src/tools/shared/safe-env.ts +82 -0
  284. package/src/tools/sub-agent.ts +171 -0
  285. package/src/tools/task-output.ts +236 -0
  286. package/src/tools/tool-search/index.ts +167 -0
  287. package/src/tools/tool-search/registry.ts +278 -0
  288. package/src/tools/undo-edit.ts +314 -0
  289. package/src/tools/web-fetch/cache.ts +112 -0
  290. package/src/tools/web-fetch/index.ts +604 -0
  291. package/src/tools/write.ts +385 -0
  292. package/src/types.ts +1057 -0
  293. package/src/working-tags.ts +118 -0
@@ -0,0 +1,1116 @@
1
+ /**
2
+ * Bash tool safety layers.
3
+ *
4
+ * Seven layers of defense-in-depth for shell command execution, plus a
5
+ * final UX gate:
6
+ * 1. Environment variable stripping
7
+ * 2. Critical path protection
8
+ * 3. Command classification
9
+ * 4. Path validation for write commands
10
+ * 5. Obfuscation and injection detection
11
+ * 6. Script preflight
12
+ * 6.5. Interactive command detection (UX gate — prevents silent timeouts
13
+ * on editors, pagers, REPLs, and interactive DB clients)
14
+ * 7. Auto-mode classifier (utility model LLM call)
15
+ *
16
+ * Reference: docs/cortex/tools/bash.md (Safety Architecture)
17
+ */
18
+ import * as path from 'node:path';
19
+ import * as fs from 'node:fs';
20
+ import { buildSafeEnv as buildSafeEnvShared } from '../shared/safe-env.js';
21
+ import { checkInteractive } from './interactive.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Layer 1: Environment Variable Security
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * Build a safe environment for child processes by stripping dangerous variables.
27
+ * Adds CORTEX_SHELL=exec as a context marker.
28
+ *
29
+ * Delegates to the shared buildSafeEnv utility so that both the Bash tool
30
+ * and the MCP client use the same blocklist.
31
+ *
32
+ * @param parentEnv - The source environment (typically process.env)
33
+ * @param overrides - Optional env var overrides that bypass the blocklist
34
+ */
35
+ export function buildSafeEnv(parentEnv, overrides) {
36
+ return buildSafeEnvShared(parentEnv, 'exec', overrides);
37
+ }
38
+ // ---------------------------------------------------------------------------
39
+ // Layer 2: Critical Path Protection
40
+ // ---------------------------------------------------------------------------
41
+ const UNIX_CRITICAL_PATHS = [
42
+ '/',
43
+ '/usr',
44
+ '/etc',
45
+ '/boot',
46
+ '/sbin',
47
+ '/var',
48
+ '/System',
49
+ '/proc',
50
+ '/sys',
51
+ ];
52
+ const MACOS_CRITICAL_PATHS = [
53
+ path.join(process.env['HOME'] ?? '', 'Library'),
54
+ ];
55
+ const WINDOWS_CRITICAL_PATHS = [
56
+ 'C:\\Windows',
57
+ 'C:\\Windows\\System32',
58
+ 'C:\\Program Files',
59
+ 'C:\\Program Files (x86)',
60
+ 'C:\\ProgramData',
61
+ ];
62
+ /**
63
+ * Check if a target path resolves to a critical system directory.
64
+ */
65
+ export function isCriticalPath(targetPath) {
66
+ const resolved = path.resolve(targetPath);
67
+ const normalized = resolved.replace(/\\/g, '/').replace(/\/+$/, '');
68
+ const criticalPaths = process.platform === 'win32'
69
+ ? WINDOWS_CRITICAL_PATHS
70
+ : [...UNIX_CRITICAL_PATHS, ...(process.platform === 'darwin' ? MACOS_CRITICAL_PATHS : [])];
71
+ for (const cp of criticalPaths) {
72
+ const normalizedCp = cp.replace(/\\/g, '/').replace(/\/+$/, '');
73
+ if (normalized === normalizedCp || normalized.toLowerCase() === normalizedCp.toLowerCase()) {
74
+ return true;
75
+ }
76
+ }
77
+ // Check for Windows AppData
78
+ if (process.platform === 'win32') {
79
+ const userProfile = process.env['USERPROFILE'];
80
+ if (userProfile) {
81
+ const appDataPath = path.join(userProfile, 'AppData').replace(/\\/g, '/');
82
+ if (normalized.toLowerCase().startsWith(appDataPath.toLowerCase())) {
83
+ return true;
84
+ }
85
+ }
86
+ }
87
+ return false;
88
+ }
89
+ // ---------------------------------------------------------------------------
90
+ // Layer 3: Command Classification
91
+ // ---------------------------------------------------------------------------
92
+ const UNIX_READ_COMMANDS = new Set([
93
+ 'cd', 'ls', 'find', 'cat', 'head', 'tail', 'sort', 'wc', 'diff',
94
+ 'grep', 'echo', 'pwd', 'env', 'which', 'file', 'stat', 'strings',
95
+ 'hexdump', 'less', 'more', 'tree',
96
+ ]);
97
+ const UNIX_WRITE_COMMANDS = new Set([
98
+ 'rm', 'rmdir', 'mv', 'cp', 'chmod', 'chown',
99
+ ]);
100
+ const UNIX_CREATE_COMMANDS = new Set([
101
+ 'mkdir', 'touch', 'tee',
102
+ ]);
103
+ const UNIX_NETWORK_COMMANDS = new Set([
104
+ 'curl', 'wget', 'ssh', 'scp', 'rsync', 'nc', 'nmap',
105
+ ]);
106
+ const UNIX_SAFE_STDIN_COMMANDS = new Set([
107
+ 'jq', 'cut', 'uniq', 'head', 'tail', 'tr', 'wc',
108
+ ]);
109
+ const PS_READ_COMMANDS = new Set([
110
+ 'get-content', 'get-childitem', 'get-item', 'get-location',
111
+ 'select-string', 'compare-object', 'test-path', 'get-process',
112
+ 'dir', 'type', 'where',
113
+ ]);
114
+ const PS_WRITE_COMMANDS = new Set([
115
+ 'remove-item', 'move-item', 'copy-item', 'set-content',
116
+ 'rename-item', 'set-itemproperty',
117
+ ]);
118
+ const PS_CREATE_COMMANDS = new Set([
119
+ 'new-item', 'out-file', 'add-content',
120
+ ]);
121
+ const PS_NETWORK_COMMANDS = new Set([
122
+ 'invoke-webrequest', 'invoke-restmethod', 'test-netconnection', 'ssh',
123
+ ]);
124
+ /**
125
+ * Git subcommands that are read-only.
126
+ */
127
+ const GIT_READ_SUBCOMMANDS = new Set([
128
+ 'status', 'log', 'diff', 'show', 'branch', 'tag', 'remote', 'stash',
129
+ 'blame', 'shortlog', 'describe', 'rev-parse', 'ls-files', 'ls-tree',
130
+ ]);
131
+ /**
132
+ * Safe-stdin denied flags per binary.
133
+ */
134
+ const SAFE_STDIN_DENIED_FLAGS = {
135
+ grep: new Set(['-r', '-R', '-d', '-f', '--recursive', '--dereference-recursive', '--directories', '--file', '--exclude-from']),
136
+ jq: new Set(['-f', '-L', '--from-file', '--library-path', '--argfile', '--rawfile', '--slurpfile']),
137
+ sort: new Set(['-o', '-T', '--output', '--temporary-directory', '--compress-program', '--files0-from', '--random-source']),
138
+ wc: new Set(['--files0-from']),
139
+ };
140
+ /**
141
+ * Split a command string on shell operators (; && || |) while respecting
142
+ * quoted strings. Returns the individual sub-commands.
143
+ */
144
+ export function splitOnShellOperators(command) {
145
+ const subCommands = [];
146
+ let current = '';
147
+ let inSingle = false;
148
+ let inDouble = false;
149
+ let escaped = false;
150
+ let i = 0;
151
+ while (i < command.length) {
152
+ const ch = command[i];
153
+ if (escaped) {
154
+ current += ch;
155
+ escaped = false;
156
+ i++;
157
+ continue;
158
+ }
159
+ if (ch === '\\') {
160
+ escaped = true;
161
+ current += ch;
162
+ i++;
163
+ continue;
164
+ }
165
+ if (ch === "'" && !inDouble) {
166
+ inSingle = !inSingle;
167
+ current += ch;
168
+ i++;
169
+ continue;
170
+ }
171
+ if (ch === '"' && !inSingle) {
172
+ inDouble = !inDouble;
173
+ current += ch;
174
+ i++;
175
+ continue;
176
+ }
177
+ // Only split when outside quotes
178
+ if (!inSingle && !inDouble) {
179
+ // Check for && or ||
180
+ if ((ch === '&' && command[i + 1] === '&') || (ch === '|' && command[i + 1] === '|')) {
181
+ if (current.trim())
182
+ subCommands.push(current.trim());
183
+ current = '';
184
+ i += 2;
185
+ continue;
186
+ }
187
+ // Check for single pipe (not ||) or semicolon
188
+ if (ch === ';' || (ch === '|' && command[i + 1] !== '|')) {
189
+ if (current.trim())
190
+ subCommands.push(current.trim());
191
+ current = '';
192
+ i++;
193
+ continue;
194
+ }
195
+ }
196
+ current += ch;
197
+ i++;
198
+ }
199
+ if (current.trim())
200
+ subCommands.push(current.trim());
201
+ return subCommands;
202
+ }
203
+ /**
204
+ * Extract the command name from a single (non-compound) command string.
205
+ */
206
+ function extractCommandName(singleCommand) {
207
+ const trimmed = singleCommand.trim();
208
+ // Handle 'sed -i' specifically
209
+ if (/^sed\s+.*-i/.test(trimmed))
210
+ return 'sed-i';
211
+ // Get the first token (the command name)
212
+ const tokens = trimmed.split(/\s+/);
213
+ return (tokens[0] ?? '').toLowerCase();
214
+ }
215
+ /**
216
+ * Risk ordering from lowest to highest. Used to pick the most dangerous
217
+ * classification when a compound command contains multiple sub-commands.
218
+ */
219
+ const CLASSIFICATION_RISK_ORDER = [
220
+ 'read',
221
+ 'safe-stdin',
222
+ 'create',
223
+ 'write',
224
+ 'network',
225
+ 'unknown',
226
+ ];
227
+ /**
228
+ * Return the higher-risk classification of two values.
229
+ */
230
+ function higherRisk(a, b) {
231
+ const aIdx = CLASSIFICATION_RISK_ORDER.indexOf(a);
232
+ const bIdx = CLASSIFICATION_RISK_ORDER.indexOf(b);
233
+ return aIdx >= bIdx ? a : b;
234
+ }
235
+ /**
236
+ * Classify a single (non-compound) command by its potential impact.
237
+ */
238
+ function classifySingleCommand(singleCommand) {
239
+ const cmdName = extractCommandName(singleCommand);
240
+ const isWindows = process.platform === 'win32';
241
+ if (isWindows) {
242
+ const psCmd = cmdName.toLowerCase();
243
+ if (PS_READ_COMMANDS.has(psCmd))
244
+ return 'read';
245
+ if (PS_WRITE_COMMANDS.has(psCmd))
246
+ return 'write';
247
+ if (PS_CREATE_COMMANDS.has(psCmd))
248
+ return 'create';
249
+ if (PS_NETWORK_COMMANDS.has(psCmd))
250
+ return 'network';
251
+ // Handle PS aliases
252
+ if (psCmd === 'curl' || psCmd === 'wget')
253
+ return 'network';
254
+ return 'unknown';
255
+ }
256
+ // Unix
257
+ // Handle git subcommands
258
+ if (cmdName === 'git') {
259
+ const parts = singleCommand.trim().split(/\s+/);
260
+ const subcommand = parts[1]?.toLowerCase();
261
+ if (subcommand && GIT_READ_SUBCOMMANDS.has(subcommand))
262
+ return 'read';
263
+ return 'unknown';
264
+ }
265
+ // Handle sed -i (write)
266
+ if (cmdName === 'sed-i')
267
+ return 'write';
268
+ if (UNIX_READ_COMMANDS.has(cmdName))
269
+ return 'read';
270
+ if (UNIX_WRITE_COMMANDS.has(cmdName))
271
+ return 'write';
272
+ if (UNIX_CREATE_COMMANDS.has(cmdName))
273
+ return 'create';
274
+ if (UNIX_NETWORK_COMMANDS.has(cmdName))
275
+ return 'network';
276
+ // Check safe-stdin
277
+ if (UNIX_SAFE_STDIN_COMMANDS.has(cmdName)) {
278
+ // Verify no denied flags and no file args
279
+ const tokens = singleCommand.trim().split(/\s+/);
280
+ const deniedFlags = SAFE_STDIN_DENIED_FLAGS[cmdName];
281
+ if (deniedFlags) {
282
+ for (const token of tokens.slice(1)) {
283
+ if (deniedFlags.has(token))
284
+ return 'unknown';
285
+ }
286
+ }
287
+ // Check for path-like positional arguments (simple heuristic)
288
+ const args = tokens.slice(1).filter((t) => !t.startsWith('-'));
289
+ const hasPathArgs = args.some((a) => a.includes('/') || a.includes('.'));
290
+ if (hasPathArgs)
291
+ return 'unknown';
292
+ return 'safe-stdin';
293
+ }
294
+ return 'unknown';
295
+ }
296
+ /**
297
+ * Classify a command (potentially compound) by its potential impact.
298
+ * For compound commands, returns the highest-risk classification
299
+ * among all sub-commands.
300
+ */
301
+ export function classifyCommand(command) {
302
+ const subCommands = splitOnShellOperators(command);
303
+ if (subCommands.length === 0)
304
+ return 'unknown';
305
+ let result = classifySingleCommand(subCommands[0]);
306
+ for (let i = 1; i < subCommands.length; i++) {
307
+ result = higherRisk(result, classifySingleCommand(subCommands[i]));
308
+ }
309
+ return result;
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // Layer 4: Path Validation
313
+ // ---------------------------------------------------------------------------
314
+ /**
315
+ * Extract target paths from write/create commands in a single sub-command.
316
+ */
317
+ function extractWritePathsFromSingle(singleCommand) {
318
+ const paths = [];
319
+ const tokens = singleCommand.trim().split(/\s+/);
320
+ const cmd = (tokens[0] ?? '').toLowerCase();
321
+ if (['rm', 'rmdir', 'mv', 'cp', 'touch', 'mkdir'].includes(cmd)) {
322
+ // Last argument(s) that aren't flags
323
+ for (let i = tokens.length - 1; i > 0; i--) {
324
+ const token = tokens[i];
325
+ if (!token.startsWith('-')) {
326
+ paths.push(token);
327
+ // For rm, rmdir, touch, mkdir - all non-flag args are targets
328
+ // For mv, cp - last arg is destination
329
+ if (['mv', 'cp'].includes(cmd))
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ return paths;
335
+ }
336
+ /**
337
+ * Extract target paths from write/create commands.
338
+ * Returns the paths that would be modified by the command.
339
+ * Handles compound commands by extracting paths from all sub-commands.
340
+ */
341
+ export function extractWritePaths(command) {
342
+ const subCommands = splitOnShellOperators(command);
343
+ const paths = [];
344
+ for (const sub of subCommands) {
345
+ paths.push(...extractWritePathsFromSingle(sub));
346
+ }
347
+ return paths;
348
+ }
349
+ /**
350
+ * Resolve a path, following symlinks when the target exists.
351
+ * Falls back to path.resolve() if the path does not yet exist.
352
+ */
353
+ function resolveWithSymlinks(targetPath) {
354
+ try {
355
+ return fs.realpathSync(targetPath);
356
+ }
357
+ catch {
358
+ // Path does not exist yet (e.g., mkdir for a new directory), fall back
359
+ return path.resolve(targetPath);
360
+ }
361
+ }
362
+ /**
363
+ * Validate that write paths are within the allowed working directory.
364
+ */
365
+ export function validateWritePaths(command, workingDirectory, currentCwd) {
366
+ const classification = classifyCommand(command);
367
+ if (classification !== 'write' && classification !== 'create') {
368
+ return { allowed: true, classification };
369
+ }
370
+ const writePaths = extractWritePaths(command);
371
+ for (const wp of writePaths) {
372
+ // Resolve relative to current CWD, then resolve symlinks
373
+ const rawResolved = path.resolve(currentCwd, wp);
374
+ const resolved = resolveWithSymlinks(rawResolved);
375
+ // Check critical paths
376
+ if (isCriticalPath(resolved)) {
377
+ return {
378
+ allowed: false,
379
+ reason: 'This command would modify a critical system directory. This cannot be auto-allowed.',
380
+ classification,
381
+ };
382
+ }
383
+ }
384
+ return { allowed: true, classification };
385
+ }
386
+ // ---------------------------------------------------------------------------
387
+ // Layer 5: Obfuscation and Injection Detection
388
+ // ---------------------------------------------------------------------------
389
+ /**
390
+ * Strip invisible Unicode characters that could be used for obfuscation.
391
+ */
392
+ export function stripInvisibleChars(command) {
393
+ // Zero-width characters, BiDi markers, variation selectors, tag characters
394
+ return command.replace(/[\u200B-\u200F\u2028-\u202F\uFEFF\u00AD\u034F\u061C\u180E\u2060-\u2069\uFFF9-\uFFFB\u{E0001}-\u{E007F}\u{FE00}-\u{FE0F}]/gu, '');
395
+ }
396
+ /**
397
+ * Safe URL allowlist for download-and-execute patterns.
398
+ */
399
+ const SAFE_DOWNLOAD_URLS = [
400
+ { host: 'brew.sh' },
401
+ { host: 'get.pnpm.io' },
402
+ { host: 'bun.sh', pathPrefix: '/install' },
403
+ { host: 'sh.rustup.rs' },
404
+ { host: 'get.docker.com' },
405
+ { host: 'install.python-poetry.org' },
406
+ { host: 'raw.githubusercontent.com', pathPrefix: '/Homebrew/' },
407
+ { host: 'raw.githubusercontent.com', pathPrefix: '/nvm-sh/nvm/' },
408
+ ];
409
+ /**
410
+ * Check if a URL is in the safe download allowlist.
411
+ */
412
+ function isSafeDownloadUrl(url) {
413
+ try {
414
+ const parsed = new URL(url);
415
+ // Reject URLs with credentials
416
+ if (parsed.username || parsed.password)
417
+ return false;
418
+ const host = parsed.hostname.toLowerCase();
419
+ const pathname = parsed.pathname;
420
+ for (const entry of SAFE_DOWNLOAD_URLS) {
421
+ if (host === entry.host || host === `www.${entry.host}`) {
422
+ if (!entry.pathPrefix || pathname.startsWith(entry.pathPrefix)) {
423
+ return true;
424
+ }
425
+ }
426
+ }
427
+ }
428
+ catch {
429
+ // Invalid URL
430
+ }
431
+ return false;
432
+ }
433
+ /**
434
+ * Extract URLs from a command string.
435
+ */
436
+ function extractUrls(command) {
437
+ const urlRegex = /https?:\/\/[^\s'"]+/g;
438
+ return command.match(urlRegex) ?? [];
439
+ }
440
+ /**
441
+ * Unix obfuscation and injection patterns.
442
+ */
443
+ const UNIX_OBFUSCATION_PATTERNS = [
444
+ // Encoded execution
445
+ { pattern: /base64\s+(-d|--decode)\s*\|.*\b(ba)?sh\b/i, description: 'Base64 decode piped to shell' },
446
+ { pattern: /xxd\s+-r\s*\|.*\b(ba)?sh\b/i, description: 'Hex decode piped to shell' },
447
+ { pattern: /printf\s+.*\\x.*\|.*\b(ba)?sh\b/i, description: 'Printf escape sequences piped to shell' },
448
+ // Eval injection
449
+ { pattern: /\beval\s+.*(\$\(|`|base64|\\x|\\[0-7])/i, description: 'Eval with encoded/obfuscated input' },
450
+ // Heredoc execution
451
+ { pattern: /<<\s*['"]?\w+['"]?\s*\n.*\b(ba)?sh\b/is, description: 'Heredoc used to construct and execute commands' },
452
+ // Escape sequences
453
+ { pattern: /\$'\\[0-7]{3}.*\\[0-7]{3}'/, description: 'Bash octal escape sequences constructing commands' },
454
+ { pattern: /\$'\\x[0-9a-f]{2}.*\\x[0-9a-f]{2}'/i, description: 'Bash hex escape sequences constructing commands' },
455
+ // Polyglot injection
456
+ { pattern: /python[23]?\s+-c\s+.*(?:base64|eval|exec|__import__)/i, description: 'Python with obfuscation patterns' },
457
+ { pattern: /perl\s+-e\s+.*(?:eval|unpack|decode_base64)/i, description: 'Perl with obfuscation patterns' },
458
+ { pattern: /ruby\s+-e\s+.*(?:eval|Base64|decode64)/i, description: 'Ruby with obfuscation patterns' },
459
+ // Variable obfuscation
460
+ { pattern: /\w+=[^;]*;\s*\w+=[^;]*;\s*\$\{?\w+\}?\$\{?\w+\}?/i, description: 'Variable assignment chains constructing commands' },
461
+ // Process substitution with remote content
462
+ { pattern: /<\(.*(?:curl|wget|nc)\s+/i, description: 'Remote content via process substitution' },
463
+ // Shell metacharacters — uses quote-aware matching in checkObfuscation()
464
+ // so that legitimate regex patterns inside quotes (e.g., grep "foo\|bar") are not flagged.
465
+ { pattern: /\\[;&|]/, description: 'Backslash-escaped operators or whitespace', quoteAware: true },
466
+ { pattern: /[\u200B\u200C\u200D\uFEFF\u00A0]/, description: 'Unicode whitespace characters' },
467
+ { pattern: /[\x00-\x08\x0E-\x1F]/, description: 'Control characters in command' },
468
+ { pattern: /\w#\w/, description: 'Mid-word hash (potential comment injection)' },
469
+ { pattern: /['"]-+\w/, description: 'Obfuscated flags via quotes' },
470
+ // Structural
471
+ { pattern: /#.*['"].*\n/, description: 'Comment/quote desync pattern' },
472
+ { pattern: /'[^']*\n[^']*'/, description: 'Embedded newlines in single-quoted strings' },
473
+ { pattern: /[|;&]\s*$/, description: 'Incomplete command (trailing pipe or semicolon)' },
474
+ // NOTE: IFS manipulation and /proc access are handled by dedicated
475
+ // quote-aware validators below (checkIfsInjection, checkProcSysAccess).
476
+ ];
477
+ /**
478
+ * PowerShell obfuscation patterns.
479
+ */
480
+ const PS_OBFUSCATION_PATTERNS = [
481
+ { pattern: /-EncodedCommand\b/i, description: 'PowerShell encoded command' },
482
+ { pattern: /\[Convert\]::FromBase64String.*\|\s*iex/i, description: 'Base64 decode piped to Invoke-Expression' },
483
+ { pattern: /Invoke-Expression\s+.*(\+|\[char\]|\.Replace)/i, description: 'Invoke-Expression with constructed strings' },
484
+ { pattern: /Net\.WebClient.*DownloadString.*\|\s*iex/i, description: 'Download cradle piped to iex' },
485
+ { pattern: /Invoke-WebRequest.*\|\s*iex/i, description: 'Web request piped to Invoke-Expression' },
486
+ { pattern: /Start-Process.*-WindowStyle\s+Hidden/i, description: 'Hidden process execution' },
487
+ { pattern: /\[Reflection\.Assembly\]::Load/i, description: 'Reflection-based assembly loading' },
488
+ { pattern: /-ExecutionPolicy\s+Bypass/i, description: 'Execution policy bypass' },
489
+ ];
490
+ /**
491
+ * Strip the content of single-quoted and double-quoted strings from a command,
492
+ * preserving the quotes themselves. This lets obfuscation patterns check only
493
+ * the unquoted portions of a command so that legitimate regex syntax inside
494
+ * quotes (e.g., grep "foo\|bar") is not flagged as shell obfuscation.
495
+ */
496
+ function stripQuotedContent(command) {
497
+ return command.replace(/"[^"]*"|'[^']*'/g, (match) => {
498
+ // Preserve the quote characters but empty the content
499
+ const quote = match[0];
500
+ return `${quote}${quote}`;
501
+ });
502
+ }
503
+ /**
504
+ * Analyze the quoting context of each character in a shell command.
505
+ * Returns an array of QuoteContext values, one per character, indicating
506
+ * whether that position is inside single quotes, double quotes, backticks,
507
+ * escaped, or unquoted. Handles nested escapes correctly (e.g., `\"` inside
508
+ * double quotes keeps the next character as "double", not "escaped").
509
+ */
510
+ export function analyzeQuoteState(command) {
511
+ const states = new Array(command.length);
512
+ let context = 'none';
513
+ // Track the context to return to when a backtick closes (for backtick-in-double-quote)
514
+ let returnContext = 'none';
515
+ for (let i = 0; i < command.length; i++) {
516
+ const ch = command[i];
517
+ if (context === 'single') {
518
+ // Inside single quotes, only a closing single quote ends the context.
519
+ // No escape processing at all inside single quotes.
520
+ if (ch === "'") {
521
+ states[i] = 'none';
522
+ context = 'none';
523
+ }
524
+ else {
525
+ states[i] = 'single';
526
+ }
527
+ continue;
528
+ }
529
+ if (context === 'double') {
530
+ // Inside double quotes, backslash only escapes: $, `, ", \, and newline.
531
+ if (ch === '\\' && i + 1 < command.length) {
532
+ const next = command[i + 1];
533
+ if ('$`"\\'.includes(next) || next === '\n') {
534
+ states[i] = 'escaped';
535
+ states[i + 1] = 'escaped';
536
+ i++; // skip the escaped character
537
+ continue;
538
+ }
539
+ }
540
+ if (ch === '"') {
541
+ states[i] = 'none';
542
+ context = 'none';
543
+ }
544
+ else if (ch === '`') {
545
+ // Backticks nest inside double quotes. Track return context so we
546
+ // resume double-quote context when the backtick closes.
547
+ states[i] = 'backtick';
548
+ returnContext = 'double';
549
+ context = 'backtick';
550
+ }
551
+ else {
552
+ states[i] = 'double';
553
+ }
554
+ continue;
555
+ }
556
+ if (context === 'backtick') {
557
+ if (ch === '\\' && i + 1 < command.length) {
558
+ states[i] = 'escaped';
559
+ states[i + 1] = 'escaped';
560
+ i++;
561
+ continue;
562
+ }
563
+ if (ch === '`') {
564
+ states[i] = returnContext === 'double' ? 'double' : 'none';
565
+ context = returnContext;
566
+ returnContext = 'none';
567
+ }
568
+ else {
569
+ states[i] = 'backtick';
570
+ }
571
+ continue;
572
+ }
573
+ // context === 'none' (unquoted)
574
+ if (ch === '\\' && i + 1 < command.length) {
575
+ states[i] = 'escaped';
576
+ states[i + 1] = 'escaped';
577
+ i++;
578
+ continue;
579
+ }
580
+ if (ch === "'") {
581
+ states[i] = 'single'; // the quote character itself is "in" single-quote context
582
+ context = 'single';
583
+ continue;
584
+ }
585
+ if (ch === '"') {
586
+ states[i] = 'double';
587
+ context = 'double';
588
+ continue;
589
+ }
590
+ if (ch === '`') {
591
+ states[i] = 'backtick';
592
+ context = 'backtick';
593
+ continue;
594
+ }
595
+ states[i] = 'none';
596
+ }
597
+ return states;
598
+ }
599
+ /**
600
+ * Extract the unquoted portions of a command using the quote state machine.
601
+ * Returns a string where quoted characters are replaced with spaces (preserving
602
+ * positions) so that regex matches on the result correspond to unquoted regions.
603
+ */
604
+ function getUnquotedText(command, states) {
605
+ const chars = [];
606
+ for (let i = 0; i < command.length; i++) {
607
+ chars.push(states[i] === 'none' ? command[i] : ' ');
608
+ }
609
+ return chars.join('');
610
+ }
611
+ // ---------------------------------------------------------------------------
612
+ // Validator 2: Enhanced IFS Injection
613
+ // ---------------------------------------------------------------------------
614
+ /**
615
+ * Detect IFS variable manipulation in unquoted context.
616
+ * `IFS=` inside quotes is harmless (just a string literal).
617
+ * Unquoted `IFS=` is a shell variable assignment that can enable attacks.
618
+ */
619
+ export function checkIfsInjection(command, states) {
620
+ const unquoted = getUnquotedText(command, states);
621
+ if (/\bIFS\s*=/.test(unquoted)) {
622
+ return {
623
+ allowed: false,
624
+ reason: 'Obfuscation pattern detected: IFS variable manipulation',
625
+ };
626
+ }
627
+ return { allowed: true };
628
+ }
629
+ // ---------------------------------------------------------------------------
630
+ // Validator 3: Enhanced proc/sys Access
631
+ // ---------------------------------------------------------------------------
632
+ /**
633
+ * Sensitive paths under /proc and /sys that can be used for exfiltration
634
+ * or system introspection attacks.
635
+ */
636
+ const PROC_SYS_PATTERNS = [
637
+ // /proc exfiltration vectors
638
+ /\/proc\/[^/]*\/environ/,
639
+ /\/proc\/[^/]*\/cmdline/,
640
+ /\/proc\/[^/]*\/maps/,
641
+ /\/proc\/[^/]*\/mem\b/,
642
+ /\/proc\/[^/]*\/fd\//,
643
+ /\/proc\/[^/]*\/exe\b/,
644
+ /\/proc\/[^/]*\/cwd\b/,
645
+ /\/proc\/[^/]*\/root\b/,
646
+ /\/proc\/[^/]*\/mountinfo/,
647
+ /\/proc\/[^/]*\/status/,
648
+ // /sys sensitive paths
649
+ /\/sys\/class\/net\b/,
650
+ /\/sys\/kernel\//,
651
+ /\/sys\/firmware\//,
652
+ /\/sys\/fs\/cgroup\//,
653
+ ];
654
+ /**
655
+ * Detect access to sensitive /proc and /sys paths in unquoted context.
656
+ * Quoted references (e.g., `echo "/proc/self/environ"`) are harmless string
657
+ * literals. Unquoted references indicate actual filesystem access attempts.
658
+ */
659
+ export function checkProcSysAccess(command, states) {
660
+ const unquoted = getUnquotedText(command, states);
661
+ for (const pattern of PROC_SYS_PATTERNS) {
662
+ if (pattern.test(unquoted)) {
663
+ return {
664
+ allowed: false,
665
+ reason: 'Obfuscation pattern detected: Access to sensitive /proc or /sys path',
666
+ };
667
+ }
668
+ }
669
+ return { allowed: true };
670
+ }
671
+ // ---------------------------------------------------------------------------
672
+ // Validator 4: jq system() Blocking
673
+ // ---------------------------------------------------------------------------
674
+ /**
675
+ * Detect jq command abuse: system() calls, @sh filter for shell injection,
676
+ * and -n with module imports that could load malicious jq modules.
677
+ */
678
+ export function checkJqAbuse(command) {
679
+ // Only check commands that invoke jq
680
+ if (!/\bjq\b/.test(command)) {
681
+ return { allowed: true };
682
+ }
683
+ // Block jq filters containing system( -- executes shell commands from jq
684
+ // Use dotAll (s) flag so multi-line jq filters are caught
685
+ if (/\bjq\b.*\bsystem\s*\(/s.test(command)) {
686
+ return {
687
+ allowed: false,
688
+ reason: 'Obfuscation pattern detected: jq system() call can execute arbitrary shell commands',
689
+ };
690
+ }
691
+ // Block @sh filter used for shell injection
692
+ if (/\bjq\b.*@sh\b/s.test(command)) {
693
+ return {
694
+ allowed: false,
695
+ reason: 'Obfuscation pattern detected: jq @sh filter can be used for shell injection',
696
+ };
697
+ }
698
+ // Block jq -n with import/include (module loading)
699
+ if (/\bjq\b\s+.*-n\b.*\b(import|include)\b/s.test(command) || /\bjq\b\s+.*\b(import|include)\b.*-n\b/s.test(command)) {
700
+ return {
701
+ allowed: false,
702
+ reason: 'Obfuscation pattern detected: jq module import with -n flag',
703
+ };
704
+ }
705
+ return { allowed: true };
706
+ }
707
+ // ---------------------------------------------------------------------------
708
+ // Validator 5: ANSI-C Quoting Detection
709
+ // ---------------------------------------------------------------------------
710
+ /**
711
+ * Detect ANSI-C quoting ($'...') with hex or octal escape sequences that
712
+ * encode potentially dangerous content. Simple escapes like $'\n' and $'\t'
713
+ * are legitimate and allowed.
714
+ */
715
+ export function checkAnsiCQuoting(command) {
716
+ // Match $'...' patterns. We need to find all ANSI-C quoted strings and
717
+ // check if they contain hex (\xHH) or octal (\0NNN or \NNN with 3 digits) escapes.
718
+ const ansiCPattern = /\$'([^'\\]*(?:\\.[^'\\]*)*)'/g;
719
+ let match;
720
+ while ((match = ansiCPattern.exec(command)) !== null) {
721
+ const content = match[1] ?? '';
722
+ // Check for hex escapes (\xHH)
723
+ const hasHex = /\\x[0-9a-fA-F]{2}/.test(content);
724
+ // Check for octal escapes (\0NNN or \NNN where N are 3 octal digits)
725
+ const hasOctal = /\\0[0-7]{1,3}/.test(content) || /\\[1-3][0-7]{2}/.test(content);
726
+ if (hasHex || hasOctal) {
727
+ return {
728
+ allowed: false,
729
+ reason: 'Obfuscation pattern detected: ANSI-C quoting with hex/octal escapes can encode hidden commands',
730
+ };
731
+ }
732
+ }
733
+ return { allowed: true };
734
+ }
735
+ // ---------------------------------------------------------------------------
736
+ // Validator 6: Enhanced Heredoc Validation
737
+ // ---------------------------------------------------------------------------
738
+ /**
739
+ * Detect heredoc patterns and validate their content. Unquoted heredoc
740
+ * delimiters (<<EOF) allow variable expansion and command substitution in
741
+ * the body, which can be used for injection. Quoted delimiters (<<'EOF')
742
+ * are treated as literal text and are safe.
743
+ */
744
+ export function checkHeredocInjection(command) {
745
+ // Match heredoc operators: <<[-]?DELIMITER or <<[-]?"DELIMITER" or <<[-]?'DELIMITER'
746
+ // We look for the delimiter, then try to find the body if it is inline (multi-line command).
747
+ const heredocPattern = /<<-?\s*(["']?)(\w+)\1/g;
748
+ let match;
749
+ while ((match = heredocPattern.exec(command)) !== null) {
750
+ const quoteChar = match[1] ?? '';
751
+ const delimiter = match[2] ?? '';
752
+ const isQuoted = quoteChar !== '';
753
+ if (isQuoted || !delimiter) {
754
+ // Quoted heredocs are safe (no expansion), skip
755
+ continue;
756
+ }
757
+ // For unquoted heredocs, check if the body (text after the delimiter line
758
+ // and before the closing delimiter) contains injection patterns.
759
+ const afterMatch = command.substring(match.index + match[0].length);
760
+ // The body starts after a newline following the heredoc operator
761
+ const newlineIdx = afterMatch.indexOf('\n');
762
+ if (newlineIdx === -1)
763
+ continue; // no body present in the command string
764
+ const bodyAndRest = afterMatch.substring(newlineIdx + 1);
765
+ const closingPattern = new RegExp(`^${delimiter}\\s*$`, 'm');
766
+ const closingMatch = closingPattern.exec(bodyAndRest);
767
+ const body = closingMatch ? bodyAndRest.substring(0, closingMatch.index) : bodyAndRest;
768
+ // Check the heredoc body for injection patterns
769
+ const injectionPatterns = [
770
+ /\$\(/, // command substitution
771
+ /`[^`]+`/, // backtick command substitution
772
+ /\$\{.*[^}]*\}/, // parameter expansion with manipulation
773
+ ];
774
+ for (const pattern of injectionPatterns) {
775
+ if (pattern.test(body)) {
776
+ return {
777
+ allowed: false,
778
+ reason: 'Obfuscation pattern detected: Unquoted heredoc with command substitution in body',
779
+ };
780
+ }
781
+ }
782
+ }
783
+ return { allowed: true };
784
+ }
785
+ // ---------------------------------------------------------------------------
786
+ // Validator 7: Brace Expansion Detection
787
+ // ---------------------------------------------------------------------------
788
+ /**
789
+ * Detect brace expansion patterns ({a,b} or {1..N}) in unquoted context
790
+ * that target suspicious paths or combine with dangerous commands.
791
+ */
792
+ export function checkBraceExpansion(command, states) {
793
+ const unquoted = getUnquotedText(command, states);
794
+ // Find {x,y} patterns in unquoted text. Only flag when combined with
795
+ // destructive commands or when referencing sensitive system paths.
796
+ // Benign patterns like `diff {old,new}/config.ts` should pass.
797
+ const commaExpansion = /\{[^}]*,[^}]*\}/g;
798
+ let match;
799
+ // Extract leading command for context-aware decisions
800
+ const firstToken = unquoted.trim().split(/\s+/)[0] ?? '';
801
+ const destructiveCommands = ['rm', 'chmod', 'chown', 'mv', 'rmdir', 'dd', 'shred'];
802
+ while ((match = commaExpansion.exec(unquoted)) !== null) {
803
+ const content = match[0];
804
+ // Flag if expansion references sensitive paths
805
+ if (/\betc\b|passwd|shadow|authorized_keys|\bssh\b|\bproc\b|\bsys\b/.test(content)) {
806
+ return {
807
+ allowed: false,
808
+ reason: 'Obfuscation pattern detected: Brace expansion referencing sensitive paths',
809
+ };
810
+ }
811
+ // Flag if any element starts with absolute path AND command is destructive
812
+ if (/\{\/|,\s*\//.test(content) && destructiveCommands.includes(firstToken.toLowerCase())) {
813
+ return {
814
+ allowed: false,
815
+ reason: 'Obfuscation pattern detected: Brace expansion with absolute paths in destructive command',
816
+ };
817
+ }
818
+ }
819
+ // Check for range expansion {N..M} combined with dangerous commands.
820
+ // Look at the first token of the overall command to determine context.
821
+ const rangeExpansion = /\{[^}]*\.\.[^}]*\}/g;
822
+ if (rangeExpansion.test(unquoted)) {
823
+ // Extract the leading command name from the unquoted text
824
+ const firstToken = unquoted.trim().split(/\s+/)[0] ?? '';
825
+ const destructiveCommands = ['rm', 'chmod', 'chown', 'mv', 'cp'];
826
+ if (destructiveCommands.includes(firstToken.toLowerCase())) {
827
+ return {
828
+ allowed: false,
829
+ reason: 'Obfuscation pattern detected: Brace range expansion with destructive command',
830
+ };
831
+ }
832
+ }
833
+ return { allowed: true };
834
+ }
835
+ // ---------------------------------------------------------------------------
836
+ // Validator 8: Enhanced Escaped Character Detection
837
+ // ---------------------------------------------------------------------------
838
+ /**
839
+ * Detect escape chains and printf hex/octal patterns that can hide dangerous
840
+ * commands from string-level pattern matching.
841
+ */
842
+ export function checkEnhancedEscapes(command, states) {
843
+ // Detect double-backslash before shell operators in unquoted context.
844
+ // In "echo hello\\;rm", the state machine marks both backslashes as escaped
845
+ // (the first escapes the second), leaving ";" as unquoted (none). We look
846
+ // for an escaped pair where the raw characters are both backslashes, followed
847
+ // immediately by an unquoted shell operator.
848
+ const shellOps = new Set([';', '&', '|']);
849
+ for (let i = 0; i + 2 < command.length; i++) {
850
+ if (states[i] === 'escaped' &&
851
+ states[i + 1] === 'escaped' &&
852
+ command[i] === '\\' &&
853
+ command[i + 1] === '\\' &&
854
+ states[i + 2] === 'none' &&
855
+ shellOps.has(command[i + 2])) {
856
+ return {
857
+ allowed: false,
858
+ reason: 'Obfuscation pattern detected: Double-escaped shell operator (live operator hidden behind escape chain)',
859
+ };
860
+ }
861
+ }
862
+ // Detect printf with hex/octal that spells dangerous commands.
863
+ // We look for printf calls with multiple escape sequences.
864
+ const printfMatch = command.match(/\bprintf\s+(['"])((?:\\x[0-9a-fA-F]{2}|\\[0-7]{3}){3,})\1/);
865
+ if (printfMatch) {
866
+ return {
867
+ allowed: false,
868
+ reason: 'Obfuscation pattern detected: printf with encoded character sequences',
869
+ };
870
+ }
871
+ // Also catch printf with %b and hex/octal in a variable
872
+ if (/\bprintf\s+['"]?%b['"]?\s+.*(?:\\x[0-9a-fA-F]{2}|\\[0-7]{3}){3,}/.test(command)) {
873
+ return {
874
+ allowed: false,
875
+ reason: 'Obfuscation pattern detected: printf %b with encoded character sequences',
876
+ };
877
+ }
878
+ return { allowed: true };
879
+ }
880
+ // ---------------------------------------------------------------------------
881
+ // Main obfuscation check
882
+ // ---------------------------------------------------------------------------
883
+ /**
884
+ * Check a command for obfuscation and injection patterns.
885
+ */
886
+ export function checkObfuscation(command) {
887
+ // Strip invisible characters first
888
+ const cleaned = stripInvisibleChars(command);
889
+ // Check if the cleaned command differs significantly (invisible chars were present)
890
+ if (cleaned.length < command.length) {
891
+ return {
892
+ allowed: false,
893
+ reason: 'Command contains invisible Unicode characters that may be used for obfuscation.',
894
+ };
895
+ }
896
+ // Length check
897
+ if (command.length > 10000) {
898
+ return {
899
+ allowed: false,
900
+ reason: 'Command exceeds maximum length (10,000 characters).',
901
+ };
902
+ }
903
+ // Check download-and-execute pattern (curl | bash)
904
+ const hasPipeToShell = /\|\s*(ba)?sh\b/i.test(command) || /\|\s*\bsh\b/.test(command);
905
+ if (hasPipeToShell && /(curl|wget)\s+/i.test(command)) {
906
+ const urls = extractUrls(command);
907
+ if (urls.length === 1 && isSafeDownloadUrl(urls[0])) {
908
+ // Safe URL, allow
909
+ }
910
+ else {
911
+ return {
912
+ allowed: false,
913
+ reason: 'Download-and-execute pattern detected (curl/wget piped to shell). This requires explicit approval.',
914
+ };
915
+ }
916
+ }
917
+ // Platform-specific patterns
918
+ const patterns = process.platform === 'win32'
919
+ ? PS_OBFUSCATION_PATTERNS
920
+ : UNIX_OBFUSCATION_PATTERNS;
921
+ // Quote-stripped version for patterns where matches inside quoted strings
922
+ // are benign (e.g., backslash-escaped operators in grep regex patterns).
923
+ const unquotedCommand = stripQuotedContent(command);
924
+ for (const { pattern, description, quoteAware } of patterns) {
925
+ // Quote-aware patterns only match against unquoted portions of the command.
926
+ // e.g., "echo test\;rm -rf /" is obfuscation (unquoted), but
927
+ // "grep 'foo\|bar'" is legitimate grep regex (inside quotes).
928
+ const target = quoteAware ? unquotedCommand : command;
929
+ if (pattern.test(target)) {
930
+ return {
931
+ allowed: false,
932
+ reason: `Obfuscation pattern detected: ${description}`,
933
+ };
934
+ }
935
+ }
936
+ // --- Enhanced validators (quote-state-machine-powered) ---
937
+ const quoteStates = analyzeQuoteState(command);
938
+ // Validator 2: Enhanced IFS injection (quote-aware)
939
+ const ifsResult = checkIfsInjection(command, quoteStates);
940
+ if (!ifsResult.allowed)
941
+ return ifsResult;
942
+ // Validator 3: Enhanced proc/sys access (quote-aware)
943
+ const procResult = checkProcSysAccess(command, quoteStates);
944
+ if (!procResult.allowed)
945
+ return procResult;
946
+ // Validator 4: jq system() blocking
947
+ const jqResult = checkJqAbuse(command);
948
+ if (!jqResult.allowed)
949
+ return jqResult;
950
+ // Validator 5: ANSI-C quoting detection
951
+ const ansiCResult = checkAnsiCQuoting(command);
952
+ if (!ansiCResult.allowed)
953
+ return ansiCResult;
954
+ // Validator 6: Enhanced heredoc validation
955
+ const heredocResult = checkHeredocInjection(command);
956
+ if (!heredocResult.allowed)
957
+ return heredocResult;
958
+ // Validator 7: Brace expansion detection
959
+ const braceResult = checkBraceExpansion(command, quoteStates);
960
+ if (!braceResult.allowed)
961
+ return braceResult;
962
+ // Validator 8: Enhanced escaped character detection
963
+ const escapeResult = checkEnhancedEscapes(command, quoteStates);
964
+ if (!escapeResult.allowed)
965
+ return escapeResult;
966
+ return { allowed: true };
967
+ }
968
+ // ---------------------------------------------------------------------------
969
+ // Layer 6: Script Preflight
970
+ // ---------------------------------------------------------------------------
971
+ /**
972
+ * Check if a command is running a script file, and if so,
973
+ * scan the script for shell syntax bleed.
974
+ */
975
+ export async function checkScriptPreflight(command, cwd) {
976
+ // Detect script execution patterns
977
+ const scriptPatterns = [
978
+ /^python[23]?\s+(\S+)/i,
979
+ /^node\s+(\S+)/i,
980
+ /^ts-node\s+(\S+)/i,
981
+ /^ruby\s+(\S+)/i,
982
+ /^perl\s+(\S+)/i,
983
+ ];
984
+ for (const pattern of scriptPatterns) {
985
+ const match = command.match(pattern);
986
+ if (!match?.[1])
987
+ continue;
988
+ const scriptPath = path.resolve(cwd, match[1]);
989
+ try {
990
+ const content = await fs.promises.readFile(scriptPath, 'utf8');
991
+ const firstLines = content.split('\n').slice(0, 10);
992
+ // Check for bare $VARS in Python/JS files
993
+ const ext = path.extname(scriptPath).toLowerCase();
994
+ if (['.py', '.js', '.ts', '.mjs', '.cjs'].includes(ext)) {
995
+ for (const line of firstLines) {
996
+ // Shell variable patterns that don't belong in Python/JS
997
+ if (/^\s*\$[A-Z_]+\b/.test(line) && !/^\s*\/\//.test(line) && !/^\s*#/.test(line)) {
998
+ return {
999
+ allowed: false,
1000
+ reason: `Script ${scriptPath} contains shell variable syntax ($VAR) that may indicate shell syntax bleed.`,
1001
+ };
1002
+ }
1003
+ }
1004
+ }
1005
+ // Check for shell commands at start of script
1006
+ if (['.py', '.js', '.ts'].includes(ext)) {
1007
+ const firstLine = (firstLines[0] ?? '').trim();
1008
+ if (/^(cd|ls|cat|echo|export|source|alias)\s/.test(firstLine) && !firstLine.startsWith('#!')) {
1009
+ return {
1010
+ allowed: false,
1011
+ reason: `Script ${scriptPath} starts with shell commands, suggesting mixed file contexts.`,
1012
+ };
1013
+ }
1014
+ }
1015
+ }
1016
+ catch {
1017
+ // Can't read script file, skip check
1018
+ }
1019
+ }
1020
+ return { allowed: true };
1021
+ }
1022
+ // ---------------------------------------------------------------------------
1023
+ // Layer 7: Auto-Mode Classifier (Stub)
1024
+ // ---------------------------------------------------------------------------
1025
+ /**
1026
+ * Auto-mode classifier that uses the utility model to classify whether
1027
+ * a command should be blocked in autonomous mode.
1028
+ *
1029
+ * The full implementation will:
1030
+ * 1. Fast check (256 max tokens): quick classification
1031
+ * 2. Full analysis (4096 max tokens): if fast check is uncertain
1032
+ *
1033
+ * Fail-safe behavior: when auto-approve mode is active (isAutoApprove=true)
1034
+ * but no classifier function is available, this layer BLOCKS the command.
1035
+ * When auto-approve is not active, the consumer's permission resolver
1036
+ * (beforeToolCall) has already approved, so this layer passes through.
1037
+ */
1038
+ export async function checkAutoModeClassifier(_command, _description, _utilityComplete, isAutoApprove) {
1039
+ // When auto-approve is not active, the consumer's permission system has
1040
+ // already handled approval. Layer 7 is defense-in-depth for auto mode only.
1041
+ if (!isAutoApprove) {
1042
+ return { allowed: true };
1043
+ }
1044
+ // Auto-approve is active but no classifier function is available.
1045
+ // Fail-safe: block until the classifier is fully implemented.
1046
+ if (!_utilityComplete) {
1047
+ return {
1048
+ allowed: false,
1049
+ reason: 'Auto-mode classifier not yet implemented. Command requires manual approval.',
1050
+ };
1051
+ }
1052
+ // TODO: Full implementation will call utilityComplete for classification.
1053
+ // For now, block in auto-approve mode even with a utility model, since
1054
+ // the classification prompt/logic is not yet built.
1055
+ return {
1056
+ allowed: false,
1057
+ reason: 'Auto-mode classifier not yet implemented. Command requires manual approval.',
1058
+ };
1059
+ }
1060
+ // ---------------------------------------------------------------------------
1061
+ // Composite safety check
1062
+ // ---------------------------------------------------------------------------
1063
+ /**
1064
+ * Run all safety layers on a command.
1065
+ * Returns the first failure or { allowed: true } if all pass.
1066
+ */
1067
+ export async function runSafetyChecks(command, workingDirectory, currentCwd, options) {
1068
+ // Layer 2: Critical path protection
1069
+ // Check each sub-command independently for critical path access
1070
+ const subCommands = splitOnShellOperators(command);
1071
+ for (const sub of subCommands) {
1072
+ const subTokens = sub.split(/\s+/);
1073
+ for (const token of subTokens) {
1074
+ if (token.startsWith('/') || token.startsWith('~') || (process.platform === 'win32' && /^[A-Za-z]:\\/.test(token))) {
1075
+ if (isCriticalPath(token)) {
1076
+ const subClassification = classifySingleCommand(sub);
1077
+ if (subClassification === 'write' || subClassification === 'create' || subClassification === 'unknown') {
1078
+ return {
1079
+ allowed: false,
1080
+ reason: 'This command would modify a critical system directory. This cannot be auto-allowed.',
1081
+ classification: classifyCommand(command),
1082
+ };
1083
+ }
1084
+ }
1085
+ }
1086
+ }
1087
+ }
1088
+ // Layer 4: Path validation for write commands (handles all sub-commands)
1089
+ const pathResult = validateWritePaths(command, workingDirectory, currentCwd);
1090
+ if (!pathResult.allowed)
1091
+ return pathResult;
1092
+ // Layer 5: Obfuscation detection
1093
+ const obfuscationResult = checkObfuscation(command);
1094
+ if (!obfuscationResult.allowed)
1095
+ return obfuscationResult;
1096
+ // Layer 6: Script preflight
1097
+ const scriptResult = await checkScriptPreflight(command, currentCwd);
1098
+ if (!scriptResult.allowed)
1099
+ return scriptResult;
1100
+ // Layer 6.5: Interactive command detection (UX gate, not security).
1101
+ // Sits after all security layers so obviously-malicious commands are
1102
+ // blocked first; prevents silent timeout loss on editors, pagers, and
1103
+ // REPLs.
1104
+ const interactiveResult = checkInteractive(command);
1105
+ if (!interactiveResult.allowed)
1106
+ return interactiveResult;
1107
+ // Layer 7: Auto-mode classifier
1108
+ const classifierResult = await checkAutoModeClassifier(command, options?.description, options?.utilityComplete, options?.isAutoApprove);
1109
+ if (!classifierResult.allowed)
1110
+ return classifierResult;
1111
+ return {
1112
+ allowed: true,
1113
+ classification: classifyCommand(command),
1114
+ };
1115
+ }
1116
+ //# sourceMappingURL=safety.js.map